//! Account-attributed activity only; no passwords, game logs, chat, or local paths. use crate::{accounts, state::AppState}; use scopenet_shared::LaunchEvent; use std::time::Duration; /// Capture the identity when work starts, so switching accounts cannot /// attribute a running game's exit to the newly selected player. #[derive(Clone)] pub struct Recorder { http: reqwest::Client, panel: String, token: Option, username: Option, } pub fn capture(state: &AppState) -> Option { let panel = state.panel_url()?; let accounts = state.accounts.read().unwrap(); let active = accounts.active()?; let token = accounts::panel_token(state); Some(Recorder { http: state.http.clone(), panel, token, username: Some(active.username.clone()), }) } impl Recorder { pub async fn send(&self, kind: &str, instance: &str) -> anyhow::Result<()> { let mut req = self .http .post(format!("{}/api/v1/launcher/events", self.panel)) .timeout(Duration::from_secs(5)); if let Some(token) = &self.token { req = req.bearer_auth(token); } req.json(&LaunchEvent { kind: kind.into(), instance_id: instance.into(), username: self.username.clone(), }) .send() .await? .error_for_status()?; Ok(()) } pub fn background(&self, kind: &str, instance: &str) { let recorder = self.clone(); let kind = kind.to_owned(); let instance = instance.to_owned(); tauri::async_runtime::spawn(async move { if let Err(e) = recorder.send(&kind, &instance).await { tracing::warn!("activity report failed: {e}"); } }); } } pub async fn send(state: &AppState, kind: &str, instance: &str) -> anyhow::Result<()> { if let Some(recorder) = capture(state) { recorder.send(kind, instance).await?; } Ok(()) } pub fn background(state: &AppState, kind: &str, instance: &str) { if let Some(recorder) = capture(state) { recorder.background(kind, instance); } }