Implement Nexus integration #54
3 changed files with 154 additions and 56 deletions
|
@ -13,7 +13,7 @@ serde_json = "1.0.94"
|
||||||
thiserror = "1.0.39"
|
thiserror = "1.0.39"
|
||||||
time = { version = "0.3.20", features = ["serde"] }
|
time = { version = "0.3.20", features = ["serde"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
url = "2.3.1"
|
url = { version = "2.3.1", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.26.0", features = ["rt", "macros"] }
|
tokio = { version = "1.26.0", features = ["rt", "macros"] }
|
||||||
|
|
|
@ -2,15 +2,15 @@ use std::convert::Infallible;
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue};
|
use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue};
|
||||||
use reqwest::{Client, Url};
|
use reqwest::{Client, RequestBuilder, Url};
|
||||||
use serde::ser::SerializeTuple;
|
use serde::Deserialize;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
mod types;
|
||||||
|
pub use types::*;
|
||||||
|
|
||||||
// TODO: Add OS information
|
// TODO: Add OS information
|
||||||
const USER_AGENT: &str = concat!("DTMM/", env!("CARGO_PKG_VERSION"));
|
const USER_AGENT: &str = concat!("DTMM/", env!("CARGO_PKG_VERSION"));
|
||||||
const GAME_ID: &str = "warhammer40kdarktide";
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref BASE_URL: Url = Url::parse("https://api.nexusmods.com/v1/").unwrap();
|
static ref BASE_URL: Url = Url::parse("https://api.nexusmods.com/v1/").unwrap();
|
||||||
|
@ -37,44 +37,6 @@ pub enum Error {
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
|
||||||
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 {
|
pub struct Api {
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
@ -93,22 +55,37 @@ impl Api {
|
||||||
Ok(Self { client })
|
Ok(Self { client })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
async fn send<T>(&self, req: RequestBuilder) -> Result<T>
|
||||||
|
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<User> {
|
||||||
|
let url = BASE_URL.join("users/validate.json")?;
|
||||||
|
let req = self.client.get(url);
|
||||||
|
self.send(req).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn mods_updated(&self, period: UpdatePeriod) -> Result<Vec<UpdateInfo>> {
|
pub async fn mods_updated(&self, period: UpdatePeriod) -> Result<Vec<UpdateInfo>> {
|
||||||
let url = BASE_URL_GAME.join("mods/updated.json")?;
|
let url = BASE_URL_GAME.join("mods/updated.json")?;
|
||||||
|
let req = self.client.get(url).query(&[period]);
|
||||||
|
self.send(req).await
|
||||||
|
}
|
||||||
|
|
||||||
let res = self
|
#[tracing::instrument(skip(self))]
|
||||||
.client
|
pub async fn mods_id(&self, id: u64) -> Result<Mod> {
|
||||||
.get(url)
|
let url = BASE_URL_GAME.join(&format!("mods/{}.json", id))?;
|
||||||
.query(&[period])
|
let req = self.client.get(url);
|
||||||
.send()
|
self.send(req).await
|
||||||
.await?
|
|
||||||
.error_for_status()?;
|
|
||||||
|
|
||||||
tracing::trace!(?res);
|
|
||||||
let json = res.text().await?;
|
|
||||||
|
|
||||||
serde_json::from_str(&json).map_err(|error| Error::Deserialize { json, error })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,4 +106,23 @@ mod test {
|
||||||
.await
|
.await
|
||||||
.expect("failed to query 'mods_updated'");
|
.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'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
102
lib/nexusmods/src/types.rs
Normal file
102
lib/nexusmods/src/types.rs
Normal file
|
@ -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<OffsetDateTime>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue