diff --git a/src/api/gog/users.rs b/src/api/gog/users.rs index 77d8ecf..6a6da12 100644 --- a/src/api/gog/users.rs +++ b/src/api/gog/users.rs @@ -26,9 +26,9 @@ pub async fn get_token_for( .get(url) .timeout(time::Duration::from_secs(10)) .send() - .await?; + .await.map_err(reqwest::Error::without_url)?; - let result = result.error_for_status()?; - let token: Token = result.json().await?; + let result = result.error_for_status().map_err(reqwest::Error::without_url)?; + let token: Token = result.json().await.map_err(reqwest::Error::without_url)?; Ok(token) } diff --git a/src/api/handlers.rs b/src/api/handlers.rs index 43a8180..fbbba1b 100644 --- a/src/api/handlers.rs +++ b/src/api/handlers.rs @@ -59,6 +59,9 @@ pub async fn entry_point( let mut topic_receiver_clone = topic_receiver.resubscribe(); let mut overlay_event_receiver_clone = overlay_event_receiver.resubscribe(); let main_socket = tokio::spawn(async move { + let mut sync_interval = time::interval(time::Duration::from_secs(10)); + sync_interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip); + sync_interval.tick().await; loop { tokio::select! { size_read = context_clone.socket_read_u16() => { @@ -101,7 +104,7 @@ pub async fn entry_point( } } - _ = time::sleep(time::Duration::from_secs(10)) => { + _ = sync_interval.tick() => { sync_routine(&context_clone, &reqwest_clone, user_clone.clone()).await }, @@ -338,6 +341,7 @@ async fn sync_routine(context: &HandlerContext, reqwest_client: &Client, user_in let Some(client_id) = &context.client_id().await else { return; }; + if !crate::client_allowed(client_id) { return; } let Some(client_secret) = &context.client_secret().await else { return; }; @@ -391,6 +395,8 @@ async fn sync_routine(context: &HandlerContext, reqwest_client: &Client, user_in .expect("Failed to get database connection"); let mut transaction = connection.begin().await.unwrap(); + let mut pending = false; + let mut uploaded = 0; for achievement in achievements { debug!("Setting achievement {}", achievement.achievement_key()); let result = gog::achievements::set_achievement( @@ -409,11 +415,15 @@ async fn sync_routine(context: &HandlerContext, reqwest_client: &Client, user_in .execute(&mut *transaction) .await .expect("Failed to update changed status"); + uploaded += 1; + } else { + pending = true; } } transaction.commit().await.expect("Failed to save changes"); - context.set_updated_achievements(false).await; - info!("Uploaded achievements"); + context.set_updated_achievements(pending).await; + if uploaded > 0 { eprintln!("DROIDDECK_COMET_UPLOADED:{uploaded}"); } + if pending { eprintln!("DROIDDECK_COMET_PENDING"); } } Err(err) => error!("Failed to read local database {:?}", err), } diff --git a/src/api/handlers/communication_service.rs b/src/api/handlers/communication_service.rs index c1bed3b..1dda644 100644 --- a/src/api/handlers/communication_service.rs +++ b/src/api/handlers/communication_service.rs @@ -35,6 +35,9 @@ pub async fn entry_point( debug!("Handling in communication service"); let header = &payload.header; + if let Some(client) = context.client_id().await { + if !crate::client_allowed(&client) { return Err(MessageHandlingError::unauthorized()); } + } let message_type: i32 = header.type_().try_into().unwrap(); if message_type == MessageType::LIBRARY_INFO_REQUEST.value() { @@ -132,6 +135,7 @@ async fn auth_info_request( let client_id = request_data.client_id(); let client_secret = request_data.client_secret(); + if !crate::client_allowed(client_id) { return Err(MessageHandlingError::unauthorized()); } let openid = request_data.openid(); let pid = request_data.game_pid(); @@ -551,6 +555,14 @@ async fn unlock_user_achievement( .await .expect("Failed to write achievement to database"); context.set_updated_achievements(true).await; + eprintln!("DROIDDECK_COMET_UNLOCKED:{}", serde_json::json!({ + "client_id": context.client_id().await, + "id": achievement.achievement_key(), + "name": achievement.name(), + "description": achievement.description(), + "icon": achievement.image_url_unlocked(), + "unlocked_at": timestamp, + })); achievement.date_unlocked = timestamp_string; let pid = context.get_pid().await; let _ = context diff --git a/src/lib.rs b/src/lib.rs index 0c539f1..50fdd21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,3 +16,10 @@ lazy_static! { .and_then(|x| if !x.contains("-") { None } else { Some(x) }) .unwrap_or_else(|| String::from("en-US")); } + + +pub fn client_allowed(client_id: &str) -> bool { + let Ok(path) = std::env::var("COMET_ALLOWED_CLIENTS") else { return true; }; + std::fs::read(path).ok().and_then(|data| serde_json::from_slice::>(&data).ok()) + .is_some_and(|clients| clients.iter().any(|id| id == client_id)) +} diff --git a/src/main.rs b/src/main.rs index 109098f..8301edb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,11 +41,13 @@ enum SubCommand { struct Args { #[arg(long, help = "Provide access token (for getting user data)")] access_token: Option, + #[arg(long, help = "Read credentials as JSON from stdin")] + credentials_stdin: bool, #[arg(long, help = "Provide refresh token (for creating game sessions)")] refresh_token: Option, #[arg(long, help = "Galaxy user id from /userData.json")] user_id: Option, - #[arg(long, help = "User name")] + #[arg(long, help = "User name", default_value = "")] username: String, #[arg( long = "from-heroic", @@ -86,7 +88,18 @@ struct Args { #[tokio::main] async fn main() { - let args = Args::parse(); + let mut args = Args::parse(); + if args.credentials_stdin { + use std::io::Read; + let value: serde_json::Value = serde_json::from_reader(std::io::stdin().take(32768)) + .expect("Invalid credential input"); + let field = |key: &str| value.get(key).and_then(|v| v.as_str()) + .filter(|v| !v.is_empty()).expect("Missing credential field").to_owned(); + args.access_token = Some(field("access_token")); + args.refresh_token = Some(field("refresh_token")); + args.user_id = Some(field("user_id")); + args.username = field("username"); + } #[cfg(debug_assertions)] let log_level = "debug"; #[cfg(not(debug_assertions))] @@ -306,6 +319,7 @@ async fn main() { }); info!("Listening on port 9977"); + eprintln!("DROIDDECK_COMET_READY:{}", std::process::id()); let socket_shutdown = cloned_shutdown.clone(); let cloned_user_info = user_info.clone();