Skip to main content

modde_sources/nexus/
mod.rs

1//! Nexus Mods integration: the typed REST/GraphQL client, API-key and OAuth
2//! authentication, CDN download links, update checks, and collection installs.
3
4pub mod api;
5pub mod auth;
6pub mod cdn;
7pub mod graphql;
8pub mod install;
9pub mod oauth;
10pub mod updates;
11
12pub use api::NexusApi;
13
14const DEFAULT_BASE_URL: &str = "https://api.nexusmods.com/v1";
15const DEFAULT_GRAPHQL_URL: &str = "https://api.nexusmods.com/v2/graphql";
16
17/// Base URL for the v1 REST API.
18///
19/// Honours `MODDE_NEXUS_BASE_URL` so integration tests can point the
20/// client at a local mock server (e.g. wiremock). Production code never
21/// sets the var, so it falls through to the official endpoint.
22#[must_use]
23pub fn base_url() -> String {
24    std::env::var("MODDE_NEXUS_BASE_URL").unwrap_or_else(|_| DEFAULT_BASE_URL.to_string())
25}
26
27/// Base URL for the v2 GraphQL API. Same override semantics as
28/// [`base_url`] but via `MODDE_NEXUS_GRAPHQL_URL`.
29#[must_use]
30pub fn graphql_url() -> String {
31    std::env::var("MODDE_NEXUS_GRAPHQL_URL").unwrap_or_else(|_| DEFAULT_GRAPHQL_URL.to_string())
32}
33
34use std::collections::HashMap;
35use std::path::Path;
36
37use anyhow::Result;
38use reqwest::Client;
39use tracing::debug;
40
41use modde_core::manifest::wabbajack::DownloadDirective;
42
43use crate::common::simple_download;
44use crate::error::{SourceError, SourceResult};
45use crate::traits::{DownloadHandle, DownloadSource, ProgressCallback, VerifiedFile};
46
47/// `NexusMods` download source.
48///
49/// Requires a Nexus Premium account and API key.
50pub struct NexusSource {
51    client: Client,
52    api_key: String,
53}
54
55impl NexusSource {
56    /// Create a new `NexusSource`, loading the API key from environment or keyring.
57    pub fn new(client: Client) -> Result<Self> {
58        let api_key = auth::load_api_key()?;
59        Ok(Self { client, api_key })
60    }
61
62    /// Create a new `NexusSource` with an explicit API key.
63    #[must_use]
64    pub fn with_api_key(client: Client, api_key: String) -> Self {
65        Self { client, api_key }
66    }
67}
68
69impl DownloadSource for NexusSource {
70    fn can_handle(&self, directive: &DownloadDirective) -> bool {
71        matches!(directive, DownloadDirective::Nexus { .. })
72    }
73
74    async fn resolve(&self, directive: &DownloadDirective) -> SourceResult<DownloadHandle> {
75        let DownloadDirective::Nexus {
76            game_id,
77            mod_id,
78            file_id,
79            hash,
80        } = directive
81        else {
82            return Err(SourceError::other(anyhow::anyhow!("not a Nexus directive")));
83        };
84
85        let download_url = cdn::generate_download_link(
86            &self.client,
87            &self.api_key,
88            game_id.as_str(),
89            *mod_id,
90            *file_id,
91        )
92        .await?;
93
94        debug!(url = %download_url, "resolved Nexus CDN download URL");
95
96        Ok(DownloadHandle {
97            url: download_url,
98            candidate_urls: Vec::new(),
99            headers: HashMap::new(),
100            expected_hash: *hash,
101            size_hint: None,
102        })
103    }
104
105    async fn download_with_progress(
106        &self,
107        handle: DownloadHandle,
108        dest: &Path,
109        progress: ProgressCallback,
110    ) -> SourceResult<VerifiedFile> {
111        simple_download(&self.client, &handle, dest, &progress).await
112    }
113}