diff --git a/Cargo.lock b/Cargo.lock index a49fa02..d9da6bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1277,6 +1277,7 @@ name = "torznab-toolkit" version = "0.1.0" dependencies = [ "actix-rt", + "lazy_static", "rocket", "serde", "xml-rs", diff --git a/Cargo.toml b/Cargo.toml index ca3ef68..b404c62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,10 @@ edition = "2021" [dependencies] actix-rt = "2.10.0" +lazy_static = "1.5.0" rocket = "0.5.1" serde = { version = "1.0.215", features = ["derive"] } xml-rs = "0.8.23" + +[profile.release] +opt-level = 3 diff --git a/src/api.rs b/src/api.rs index 0ff1284..86a2b90 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,31 +2,43 @@ use std::io::stdout; use crate::data::*; +use crate::dummy::create_empty_config; +use lazy_static::lazy_static; use rocket::http::Status; use rocket::response::status; use rocket::{get, response::content::RawXml}; use xml::writer::{EmitterConfig, XmlEvent}; -/// Holds the config for torznab-toolkit. -/// -/// A search function (`/api?t=search`) and capabilities (`/api?t=caps` - `Caps`) are required, everything else is optional. -/// -///
It's required to be set to something, which is why it's an Option set to None. -/// -/// However, this is NOT optional, and attempting to do anything with CONFIG not set will return an `Err`.
+// Holds the config for torznab-toolkit. +// +// A search function (`/api?t=search`) and capabilities (`/api?t=caps` - `Caps`) are required, everything else is optional. +// +//
It's required to be set to something, which is why it's an Option set to None. +// +// However, this is NOT optional, and attempting to do anything with CONFIG not set will return an `Err`.
+ pub(crate) static mut CONFIG: Option = None; +lazy_static! { + static ref STATUS_CONFIG_NOT_SPECIFIED: status::Custom> = status::Custom( + Status::InternalServerError, + RawXml("500 Internal server error: Config not specified".to_string()), + ); +} /// Capabilities API endpoint (`/api?t=caps`) +/// +/// Note that an apikey is *not* required for this function, regardless of whether it's required for the rest. // FIXME: VERY incomplete -// TODO: Finish it (duh) and add optional apikey #[get("/api?t=caps")] pub(crate) fn caps() -> status::Custom> { + // The compiler won't let you get a field from a struct in the Option here, since the default is None + // So this is needed + let conf = create_empty_config(); unsafe { if CONFIG.is_none() { - return status::Custom( - Status::InternalServerError, - RawXml("500 Internal server error: Config not specified".to_string()), - ); + return (*STATUS_CONFIG_NOT_SPECIFIED).clone(); + } else { + let conf: Config = CONFIG.clone().ok_or("").unwrap(); } } @@ -40,3 +52,74 @@ pub(crate) fn caps() -> status::Custom> { writer.write(XmlEvent::end_element()).unwrap(); return status::Custom(Status::Ok, RawXml(stringify!(writer).to_string())); } + +#[get("/api?t=search&")] +pub(crate) fn search(form: SearchForm) -> status::Custom> { + // The compiler won't let you get a field from a struct in the Option here, since the default is None + // So this is needed + let conf = create_empty_config(); + unsafe { + if CONFIG.is_none() { + return (*STATUS_CONFIG_NOT_SPECIFIED).clone(); + } else { + let conf: Config = CONFIG.clone().ok_or("").unwrap(); + } + } + + // TODO: Clean up this code - split it into a separate function? + let mut apikey: String = "".to_string(); + if !form.apikey.is_none() { + apikey = form.apikey.ok_or("").unwrap(); + } + + let mut categories: Vec = Vec::new(); + if !form.cat.is_none() { + // unholy amalgation of code to make the comma-separated list of strings into a vector of integers + categories = form + .cat + .ok_or("") + .unwrap() + .split(",") + .filter_map(|s| s.parse().ok()) + .collect(); + } + + let mut extended_attribute_names: String = "".to_string(); + if !form.attrs.is_none() { + extended_attribute_names = form.attrs.ok_or("").unwrap().split(",").collect(); + } + + let mut extended_attrs: bool = false; + if !form.extended.is_none() && form.extended.ok_or(false).unwrap() == 1 { + extended_attrs = true; + } + + let mut offset: u32 = 0; + if !form.offset.is_none() { + offset = form.offset.ok_or(0).unwrap(); + } + + let mut limit: u32 = 0; + limit = conf.caps.limits.max; + let wanted_limit = form.limit.ok_or(limit).unwrap(); + if wanted_limit < limit { + limit = wanted_limit + } + + match conf.auth { + Some(auth) => { + if !auth(apikey).unwrap() { + return status::Custom( + Status::Unauthorized, + RawXml("401 Unauthorized".to_string()), + ); + } + } + None => {} + } + + return status::Custom( + Status::NotImplemented, + RawXml("501 Not Implemented: Search function not implemented".to_string()), + ); +} diff --git a/src/data.rs b/src/data.rs index e0bb97c..8f16e25 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,4 +1,6 @@ //! Contains tons of structs used by the library +use rocket::FromForm; + pub(crate) type AuthFunc = fn(String) -> Result; // TODO: Figure out what the arguments should be for a search function and what it should return pub(crate) type SearchFunc = fn(String, Vec) -> Result; @@ -25,14 +27,14 @@ pub struct ServerInfo { /// `default` is the default number of results the program will return pub struct Limits { /* - I don't know why this would possibly need to be a u64, I can't imagine you'll be returning 18 quintillion results or whatever + I don't know why this would possibly need to be a u32, I can't imagine you'll be returning 4 billion results or whatever In fact, I *really* hope you aren't - if you are, you're doing something extremely wrong But hey, it's an option */ /// The maximum number of entries that can be listed in a search query - pub max: u64, + pub max: u32, /// The default number of entries to be listed in a search query - pub default: u64, + pub default: u32, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -52,7 +54,7 @@ pub struct Subcategory { /// The numeric ID of a subcategory /// /// The (de facto?) standard is `xxyy`, xx being the first two digits of the category, and the last two digits specifying the subcategory; see also: Category - pub id: String, + pub id: u32, /// The name of the subcategory, e.g. "Anime" under the "TV" cateogyr pub name: String, } @@ -63,7 +65,7 @@ pub struct Category { /// The numeric ID of a category /// /// The (de facto?) standard is `xxyy`, xx being the first two digits of the category, and the last two digits specifying the subcategory; see also: Subcategory - pub id: String, + pub id: u32, /// The name of the category, e.g. "Movies" pub name: String, /// A vector of all the subcategory in this category @@ -75,10 +77,10 @@ pub struct Category { pub struct Genre { /// The numeric ID of a genre /// - /// I'm not aware of any sure standard for this; the specification for Torznab shows an example with an ID of 1. - pub id: String, + /// I'm not aware of any standard for numbering this; the specification for Torznab shows an example with an ID of 1. + pub id: u32, /// The numeric ID of the category this genre is for. - pub category_id: String, + pub category_id: u32, /// The name of the genre pub name: String, } @@ -93,27 +95,11 @@ pub struct Tag { } #[derive(Debug, Clone, PartialEq, Eq)] -/// Holds the configuration for the capabilities of the Torznab server -/// -/// - server_info: `ServerInfo` -/// - see: `ServerInfo` docs -/// - limits: `Limits` -/// - specifies the max and default items listed when searching -/// - see: `Limits` docs -/// - searching: `Vec` -/// - specifies the capabilities of each search mode -/// - see: `SearchInfo` docs -/// - categories: `Vec` -/// - lists known categories -/// - see: `Category` docs -/// - genres: `Option>` -/// - lists known genres, optional -/// - see: `Genre` docs +/// Holds the configuration for the capabilities of the Torznab server (used in `/api?t=caps`) /// ///
Note that this library might not support all the capabilities listed in yet, so check the README before listing capabilities, or just accept that unsupported capabilities will return error 404. /// -/// It's recommended to add any capabilities you want, and set `available` to `false` in the `Caps` struct for any currently unsupported search types.
-/// +/// It's recommended to add any capabilities you want, and set `available` to `false` in the [`Caps`] struct for any currently unsupported search types. /// /// TODO: Add a way to partially(?) generate automatically from the Config pub struct Caps { @@ -127,7 +113,7 @@ pub struct Caps { #[derive(Debug, Clone, PartialEq, Eq)] /// A struct that holds configuration for torznab-toolkit -/// A search function (/api?t=search) and capabilities (/api?t=caps - struct Caps) required +/// The search function (`/api?t=search`) and capabilities (`/api?t=caps` - struct [`Caps`]) are required /// Everything else is optional pub struct Config { pub search: SearchFunc, @@ -138,3 +124,26 @@ pub struct Config { pub music: Option, pub book: Option, } + +#[derive(Debug, Clone, PartialEq, Eq, FromForm)] +/// A struct used by the API's search functions to hold its query parameters +/// Currently required (AFAIK) because of limitations with rocket +pub(crate) struct SearchForm { + /// The text query for the search + pub(crate) q: Option, + /// The apikey, for authentication + pub(crate) apikey: Option, + /// The list of numeric category IDs to be included in the search results + /// Returned by Rocket.rs as a string of comma-separated values, then split in the function to a `Vec` + pub(crate) cat: Option, + /// The list of extended attribute names to be included in the search results + /// Returned by Rocket.rs as a string of comma-separated values, then split in the function to a `Vec` + pub(crate) attrs: Option, + /// Whether *all* extended attributes should be included in the search results; overrules `attrs` + /// Can be 0 or 1 + pub(crate) extended: Option, + /// How many items to skip/offset by in the results. + pub(crate) offset: Option, + /// The maximum number of items to return - also limited to whatever `limits` is in [`Caps`] + pub(crate) limit: Option, +} diff --git a/src/dummy.rs b/src/dummy.rs index 068dd76..4d61c99 100644 --- a/src/dummy.rs +++ b/src/dummy.rs @@ -10,7 +10,7 @@ fn dummy_auth_func(_a: String) -> Result { } /// Creates a bare-minimum config -fn create_empty_config() -> Config { +pub(crate) fn create_empty_config() -> Config { let mut searching = Vec::new(); searching.push(SearchInfo { search_type: "search".to_string(), @@ -20,21 +20,21 @@ fn create_empty_config() -> Config { let mut subcategories = Vec::new(); subcategories.push(Subcategory { - id: "a".to_string(), + id: 1010, name: "b".to_string(), }); let mut categories = Vec::new(); categories.push(Category { - id: "a".to_string(), + id: 1000, name: "b".to_string(), subcategories: subcategories, }); let mut genres = Vec::new(); genres.push(Genre { - id: "a".to_string(), - category_id: "b".to_string(), + id: 1, + category_id: 1000, name: "c".to_string(), }); @@ -54,10 +54,7 @@ fn create_empty_config() -> Config { image: None, version: Some("1.0".to_string()), }, - limits: Limits { - max: 100, - default: 20, - }, + limits: Limits { max: 1, default: 1 }, searching: searching, categories: categories, genres: Some(genres), @@ -102,7 +99,7 @@ mod tests { // in this case, CONFIG is still None // can't just use run() because that expects a Config, not an Option rocket::build() - .mount("/", rocket::routes![api::caps]) + .mount("/", rocket::routes![api::caps, api::search]) .launch() .await .unwrap(); diff --git a/src/lib.rs b/src/lib.rs index b01bf93..b8c75ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ pub async fn run(conf: data::Config) -> Result { api::CONFIG = Some(conf); } match rocket::build() - .mount("/", rocket::routes![api::caps]) + .mount("/", rocket::routes![api::caps, api::search]) .launch() .await {