diff --git a/lib/nexusmods/Cargo.toml b/lib/nexusmods/Cargo.toml index 93dbd78..613aa7e 100644 --- a/lib/nexusmods/Cargo.toml +++ b/lib/nexusmods/Cargo.toml @@ -13,7 +13,7 @@ serde_json = "1.0.94" thiserror = "1.0.39" time = { version = "0.3.20", features = ["serde"] } tracing = "0.1.37" -url = "2.3.1" +url = { version = "2.3.1", features = ["serde"] } [dev-dependencies] tokio = { version = "1.26.0", features = ["rt", "macros"] } diff --git a/lib/nexusmods/src/lib.rs b/lib/nexusmods/src/lib.rs index c8ea2a6..3f3fb5e 100644 --- a/lib/nexusmods/src/lib.rs +++ b/lib/nexusmods/src/lib.rs @@ -2,15 +2,15 @@ use std::convert::Infallible; use lazy_static::lazy_static; use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue}; -use reqwest::{Client, Url}; -use serde::ser::SerializeTuple; -use serde::{Deserialize, Serialize}; +use reqwest::{Client, RequestBuilder, Url}; +use serde::Deserialize; use thiserror::Error; -use time::OffsetDateTime; + +mod types; +pub use types::*; // TODO: Add OS information const USER_AGENT: &str = concat!("DTMM/", env!("CARGO_PKG_VERSION")); -const GAME_ID: &str = "warhammer40kdarktide"; lazy_static! { static ref BASE_URL: Url = Url::parse("https://api.nexusmods.com/v1/").unwrap(); @@ -37,44 +37,6 @@ pub enum Error { pub type Result = std::result::Result; -#[derive(Clone, Debug, Deserialize)] -pub struct UpdateInfo { - pub mod_id: u64, - #[serde(with = "time::serde::timestamp")] - pub latest_file_update: OffsetDateTime, - #[serde(with = "time::serde::timestamp")] - pub latest_mod_activity: OffsetDateTime, -} - -#[derive(Copy, Clone, Debug)] -pub enum UpdatePeriod { - Day, - Week, - Month, -} - -impl Default for UpdatePeriod { - fn default() -> Self { - Self::Week - } -} - -impl Serialize for UpdatePeriod { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - let mut tup = serializer.serialize_tuple(2)?; - tup.serialize_element("period")?; - tup.serialize_element(match self { - Self::Day => "1d", - Self::Week => "1w", - Self::Month => "1m", - })?; - tup.end() - } -} - pub struct Api { client: Client, } @@ -93,22 +55,37 @@ impl Api { Ok(Self { client }) } + #[tracing::instrument(skip(self))] + async fn send(&self, req: RequestBuilder) -> Result + where + T: for<'a> Deserialize<'a>, + { + let res = req.send().await?.error_for_status()?; + tracing::trace!(?res); + + let json = res.text().await?; + serde_json::from_str(&json).map_err(|error| Error::Deserialize { json, error }) + } + + #[tracing::instrument(skip(self))] + pub async fn user_validate(&self) -> Result { + let url = BASE_URL.join("users/validate.json")?; + let req = self.client.get(url); + self.send(req).await + } + #[tracing::instrument(skip(self))] pub async fn mods_updated(&self, period: UpdatePeriod) -> Result> { let url = BASE_URL_GAME.join("mods/updated.json")?; + let req = self.client.get(url).query(&[period]); + self.send(req).await + } - let res = self - .client - .get(url) - .query(&[period]) - .send() - .await? - .error_for_status()?; - - tracing::trace!(?res); - let json = res.text().await?; - - serde_json::from_str(&json).map_err(|error| Error::Deserialize { json, error }) + #[tracing::instrument(skip(self))] + pub async fn mods_id(&self, id: u64) -> Result { + let url = BASE_URL_GAME.join(&format!("mods/{}.json", id))?; + let req = self.client.get(url); + self.send(req).await } } @@ -129,4 +106,23 @@ mod test { .await .expect("failed to query 'mods_updated'"); } + + #[tokio::test] + async fn user_validate() { + let client = make_api(); + client + .user_validate() + .await + .expect("failed to query 'user_validate'"); + } + + #[tokio::test] + async fn mods_id() { + let client = make_api(); + let dmf_id = 8; + client + .mods_id(dmf_id) + .await + .expect("failed to query 'mods_id'"); + } } diff --git a/lib/nexusmods/src/types.rs b/lib/nexusmods/src/types.rs new file mode 100644 index 0000000..fade6c1 --- /dev/null +++ b/lib/nexusmods/src/types.rs @@ -0,0 +1,102 @@ +use reqwest::Url; +use serde::ser::SerializeTuple; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +#[derive(Debug, Deserialize)] +pub struct User { + pub user_id: u64, + pub name: String, + pub profile_url: Url, + // pub is_premium: bool, + // pub is_supporter: bool, + // pub email: String, +} + +#[derive(Copy, Clone, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ModStatus { + Published, +} + +#[derive(Copy, Clone, Debug, Deserialize)] +pub enum EndorseStatus { + Undecided, +} + +#[derive(Debug, Deserialize)] +pub struct ModEndorsement { + pub endorse_status: EndorseStatus, + #[serde(with = "time::serde::timestamp::option")] + pub timestamp: Option, + pub version: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Mod { + pub name: String, + pub description: String, + pub summary: String, + pub picture_url: Url, + pub uid: u64, + pub mod_id: u64, + pub category_id: u64, + pub version: String, + #[serde(with = "time::serde::timestamp")] + pub created_timestamp: OffsetDateTime, + // created_time: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + pub updated_timestamp: OffsetDateTime, + // updated_time: OffsetDateTime, + pub author: String, + pub uploaded_by: String, + pub uploaded_users_profile_url: Url, + pub status: ModStatus, + pub available: bool, + pub endorsement: ModEndorsement, + // pub mod_downloads: u64, + // pub mod_unique_downloads: u64, + // pub game_id: u64, + // pub allow_rating: bool, + // pub domain_name: String, + // pub endorsement_count: u64, + // pub contains_adult_content: bool, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct UpdateInfo { + pub mod_id: u64, + #[serde(with = "time::serde::timestamp")] + pub latest_file_update: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + pub latest_mod_activity: OffsetDateTime, +} + +#[derive(Copy, Clone, Debug)] +pub enum UpdatePeriod { + Day, + Week, + Month, +} + +impl Default for UpdatePeriod { + fn default() -> Self { + Self::Week + } +} + +impl Serialize for UpdatePeriod { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let mut tup = serializer.serialize_tuple(2)?; + tup.serialize_element("period")?; + tup.serialize_element(match self { + Self::Day => "1d", + Self::Week => "1w", + Self::Month => "1m", + })?; + tup.end() + } +}