diff --git a/packages/core/examples/discord.rs b/packages/core/examples/discord.rs index 896875bc..12fdc294 100644 --- a/packages/core/examples/discord.rs +++ b/packages/core/examples/discord.rs @@ -1,8 +1,12 @@ -use onelauncher_core::{api::proxy::proxy_empty::ProxyEmpty, error::LauncherResult, initialize_core, store::State}; +use discord_rich_presence::activity::Timestamps; +use onelauncher_core::{api::proxy::proxy_empty::ProxyEmpty, error::LauncherResult, initialize_core, store::{CoreOptions, State}}; #[tokio::main] async fn main() -> LauncherResult<()> { - initialize_core(ProxyEmpty::new()).await?; + initialize_core(CoreOptions { + discord_client_id: Some(String::from("1274084193193955408")), + ..Default::default() + }, ProxyEmpty::new()).await?; let state = State::get().await?; let Some(rpc) = &state.rpc else { @@ -10,11 +14,15 @@ async fn main() -> LauncherResult<()> { return Ok(()); }; - rpc.set_message("Hello, Discord!").await; + rpc.set_message("Hello, Discord!", None).await; println!("Set Discord message"); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - rpc.set_message("This user is testing Discord RPC!").await; + rpc.set_message("New timestamp.", Some(Timestamps::new())).await; + println!("Set Discord message"); + + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + rpc.set_message("Reverted back to the old timestamp!", None).await; println!("Set Discord message"); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; diff --git a/packages/core/examples/locate_java.rs b/packages/core/examples/locate_java.rs index 97b959bf..d0296ab5 100644 --- a/packages/core/examples/locate_java.rs +++ b/packages/core/examples/locate_java.rs @@ -1,8 +1,8 @@ -use onelauncher_core::{api::{java, proxy::proxy_empty::ProxyEmpty}, error::LauncherResult, initialize_core}; +use onelauncher_core::{api::{java, proxy::proxy_empty::ProxyEmpty}, error::LauncherResult, initialize_core, store::CoreOptions}; #[tokio::main] async fn main() -> LauncherResult<()> { - initialize_core(ProxyEmpty::new()).await?; + initialize_core(CoreOptions::default(), ProxyEmpty::new()).await?; let java = java::locate_java().await?; diff --git a/packages/core/src/constants.rs b/packages/core/src/constants.rs index ea70bc65..3a761612 100644 --- a/packages/core/src/constants.rs +++ b/packages/core/src/constants.rs @@ -2,19 +2,7 @@ //! //! Public constant variables and strings that are used throughout the launcher. -// =========== Core Metadata =========== -/// The name of the launcher. -pub const NAME: &str = match option_env!("LAUNCHER_NAME") { - Some(name) => name, - None => "OneLauncher", -}; - -/// The version of the launcher (from `../Cargo.toml` env). -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); - // =========== Default Ingress =========== -/// The amount of attempts to fetch a file before a request fails. -pub const FETCH_ATTEMPTS: usize = 3; /// The total ingress capacity of the CLI, provided to [`indicatif`]. #[cfg(feature = "cli")] pub const CLI_TOTAL_INGRESS: u64 = 1000; @@ -22,27 +10,17 @@ pub const CLI_TOTAL_INGRESS: u64 = 1000; pub const DUMMY_REPLACE_NEWLINE: &str = "\n"; // =========== Authentication =========== -/// Our Discord RPC client ID. -pub const DISCORD_RPC_CLIENT_ID: &str = "1274084193193955408"; /// Our Microsoft client ID. pub const MICROSOFT_CLIENT_ID: &str = "9eac3a4e-8cdd-43ef-863e-49cd601b1f03"; -/// Mojang/Microsoft client ID. -pub const MINECRAFT_CLIENT_ID: &str = "00000000402b5328"; -/// Mojang/Microsoft login redirect URI. -pub const MINECRAFT_REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf"; /// Mojang/Microsoft login xboxlive scopes to get tokens. pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL"; // =========== API =========== // !!! URLS must NOT have a trailing slash. !!! /// The Modrinth API base url. -pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2"; -/// The Modrinth V3 API base url. Used for things like fetching information about organizations. -pub const MODRINTH_V3_API_URL: &str = "https://api.modrinth.com/v3"; +pub const MODRINTH_API_URL: &str = "https://api.modrinth.com"; /// The Curseforge API base url. pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com"; -/// The Curseforge API key. Reserved for use in Polyfrost projects only. Do not use in other projects without permission from the Polyfrost team. -pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe"; /// The Minecraft game ID on Curseforge. pub const CURSEFORGE_GAME_ID: u32 = 432; /// Our metadata API base url. diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index 3770e4fa..6ab08336 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -10,9 +10,8 @@ pub enum LauncherError { JavaError(#[from] crate::api::java::JavaError), #[error(transparent)] CryptoError(#[from] crate::utils::crypto::CryptoError), - - #[error("failed to connect to discord rpc socket")] - DiscordError, + #[error(transparent)] + DiscordError(#[from] crate::store::discord::DiscordError), #[error(transparent)] SerdeError(#[from] serde_json::Error), diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 8b8d5ce8..88f63d71 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,5 +1,5 @@ use api::proxy::LauncherProxy; -use store::{proxy::ProxyState, Dirs, State}; +use store::{proxy::ProxyState, Core, CoreOptions, Dirs, State}; use error::LauncherResult; use logger::start_logger; @@ -14,7 +14,8 @@ mod logger; pub use onelauncher_entity as entity; pub use onelauncher_migration as migration; -pub async fn initialize_core(proxy_backend: impl LauncherProxy + 'static) -> LauncherResult<()> { +pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherProxy + 'static) -> LauncherResult<()> { + Core::initialize(options).await?; Dirs::get().await?; start_logger().await; @@ -22,4 +23,4 @@ pub async fn initialize_core(proxy_backend: impl LauncherProxy + 'static) -> Lau State::get().await?; Ok(()) -} \ No newline at end of file +} diff --git a/packages/core/src/store/core_options.rs b/packages/core/src/store/core_options.rs new file mode 100644 index 00000000..d266df81 --- /dev/null +++ b/packages/core/src/store/core_options.rs @@ -0,0 +1,68 @@ +use std::{ops::Deref, sync::Arc}; + +use tokio::sync::OnceCell; + +use crate::error::{LauncherError, LauncherResult}; + +static CORE_STATE: OnceCell> = OnceCell::const_new(); + +pub struct Core(CoreOptions); + +/// Used for "customizable constants" for the core +pub struct CoreOptions { + pub discord_client_id: Option, + pub launcher_name: String, + pub launcher_version: String, + pub launcher_website: String, + pub fetch_attempts: u8, + /// Default client id is the same as the one used by the official launcher + pub msa_client_id: String, + pub msa_redirect_uri: String, + pub curseforge_api_key: Option, +} + +impl Default for CoreOptions { + fn default() -> Self { + Self { + discord_client_id: None, + launcher_name: String::from("Launcher"), + launcher_version: String::from(env!("CARGO_PKG_VERSION")), + launcher_website: String::from("https://polyfrost.org/"), + fetch_attempts: 3, + msa_client_id: String::from("00000000402b5328"), + msa_redirect_uri: String::from("https://login.live.com/oauth20_desktop.srf"), + curseforge_api_key: None, + } + } +} + +impl Core { + #[must_use] + pub fn new(options: CoreOptions) -> Arc { + Arc::new(Self(options)) + } + + pub async fn initialize(options: CoreOptions) -> LauncherResult<()> { + CORE_STATE.get_or_try_init(|| async move { + Ok::<_, LauncherError>(Self::new(options)) + }).await?; + + Ok(()) + } + + pub fn get() -> Arc { + CORE_STATE + .get() + .expect("core was not initialized") + .clone() + } +} + +impl Deref for Core { + type Target = CoreOptions; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + diff --git a/packages/core/src/store/dirs.rs b/packages/core/src/store/dirs.rs index 43d089ac..253384ec 100644 --- a/packages/core/src/store/dirs.rs +++ b/packages/core/src/store/dirs.rs @@ -2,7 +2,9 @@ use std::path::PathBuf; use tokio::sync::OnceCell; -use crate::{constants, utils, LauncherResult}; +use crate::{utils, LauncherResult}; + +use super::Core; /// The static [`OnceCell>`] for storing the global directory state. /// Should be initialized as soon as possible (preferably before logging) @@ -21,7 +23,7 @@ impl Dirs { fn initialize() -> LauncherResult { let base_dir = utils::io::env_path("LAUNCHER_DIR") - .or_else(|| Some(dirs::data_dir()?.join(constants::NAME))) + .or_else(|| Some(dirs::data_dir()?.join(Core::get().launcher_name.clone()))) .ok_or(DirectoryError::BaseDir)?; Ok(Self { diff --git a/packages/core/src/store/discord.rs b/packages/core/src/store/discord.rs index 1bc71e09..02fbf04e 100644 --- a/packages/core/src/store/discord.rs +++ b/packages/core/src/store/discord.rs @@ -1,33 +1,45 @@ use std::sync::{atomic::AtomicBool, Arc}; -use discord_rich_presence::{activity::{Activity, Assets, Button}, DiscordIpc, DiscordIpcClient}; +use discord_rich_presence::{activity::{Activity, Assets, Button, Timestamps}, DiscordIpc, DiscordIpcClient}; use tokio::sync::RwLock; -use crate::{constants, error::{LauncherError, LauncherResult}}; +use crate::error::LauncherResult; + +use super::Core; pub struct DiscordRPC { + started_at: i64, client: Arc>, connected: Arc, } impl DiscordRPC { pub fn initialize() -> LauncherResult { - let mut client = DiscordIpcClient::new(constants::DISCORD_RPC_CLIENT_ID) - .map_err(|_| LauncherError::DiscordError)?; + let client_id = Core::get().discord_client_id.clone() + .ok_or(DiscordError::MissingClientId)?; + + let mut client = DiscordIpcClient::new(client_id.as_str()) + .map_err(|_| DiscordError::ConnectError)?; let connected = client.connect().is_ok(); Ok(Self { + started_at: chrono::Utc::now().timestamp(), client: Arc::new(RwLock::new(client)), connected: Arc::new(AtomicBool::new(connected)), }) } - pub async fn set_message(&self, msg: &str) -> bool { + pub async fn set_message(&self, msg: &str, timestamp: Option) -> bool { self.set_activity( Activity::new() .state(msg) - .buttons(vec![Button::new("Website", "https://polyfrost.org/")]) + .buttons( + vec![Button::new("Website", "https://polyfrost.org/")] + ) + .timestamps( + timestamp.unwrap_or_else(|| Timestamps::new().start(self.started_at)) + ) .assets( Assets::new() .large_image("onelauncher_logo_512") @@ -92,4 +104,12 @@ impl DiscordRPC { fn set_connected(&self, connected: bool) { self.connected.store(connected, std::sync::atomic::Ordering::Relaxed); } +} + +#[derive(Debug, thiserror::Error)] +pub enum DiscordError { + #[error("Discord client ID is missing")] + MissingClientId, + #[error("couldn't connect to Discord IPC")] + ConnectError, } \ No newline at end of file diff --git a/packages/core/src/store/mod.rs b/packages/core/src/store/mod.rs index 4023535d..0b05c652 100644 --- a/packages/core/src/store/mod.rs +++ b/packages/core/src/store/mod.rs @@ -3,10 +3,12 @@ pub mod discord; pub mod ingress; pub mod proxy; pub mod java; +mod core_options; mod dirs; mod settings; mod state; pub use dirs::*; pub use settings::*; -pub use state::*; \ No newline at end of file +pub use state::*; +pub use core_options::*; \ No newline at end of file diff --git a/packages/core/src/store/state.rs b/packages/core/src/store/state.rs index 2978316b..1edfceeb 100644 --- a/packages/core/src/store/state.rs +++ b/packages/core/src/store/state.rs @@ -50,7 +50,7 @@ impl State { let ingress_processor = IngressProcessor::new(); let settings = Settings::new().await; let db = super::db::create_pool().await?; - + let rpc = match DiscordRPC::initialize() { Ok(rpc) => Some(rpc), Err(err) => { diff --git a/packages/core/src/utils/http.rs b/packages/core/src/utils/http.rs index 4e2ce9ce..491e97d9 100644 --- a/packages/core/src/utils/http.rs +++ b/packages/core/src/utils/http.rs @@ -8,7 +8,7 @@ use serde::Deserialize; use tokio::sync::Semaphore; use tokio_stream::StreamExt; -use crate::{api::ingress::send_ingress, constants, error::LauncherResult, store::{ingress::IngressId, State}}; +use crate::{api::ingress::send_ingress, error::LauncherResult, store::{ingress::IngressId, Core, State}}; use super::{crypto::{CryptoError, HashAlgorithm}, io}; @@ -29,9 +29,10 @@ pub fn create_client() -> LauncherResult { .default_headers({ let mut headers = reqwest::header::HeaderMap::new(); let header = reqwest::header::HeaderValue::from_str(&format!( - "{}/{} (https://polyfrost.org)", - constants::NAME, - constants::VERSION, + "{}/{} ({})", + Core::get().launcher_name, + Core::get().launcher_version, + Core::get().launcher_website, )) .expect("failed to build reqwest headers!"); headers.insert(reqwest::header::USER_AGENT, header); @@ -53,7 +54,7 @@ pub async fn fetch_advanced( let _permit = state.fetch_semaphore.0.acquire().await?; let client = &state.client; - for attempt in 0..constants::FETCH_ATTEMPTS { + for attempt in 0..Core::get().fetch_attempts { let mut req = client.request(method.clone(), url); if let Some(body) = body.clone() {