track playtime
This commit is contained in:
parent
7d1460f885
commit
d6dc358e35
6 changed files with 151 additions and 22 deletions
58
Cargo.lock
generated
58
Cargo.lock
generated
|
|
@ -389,6 +389,22 @@ dependencies = [
|
|||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-tungstenite"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async_zip"
|
||||
version = "0.0.17"
|
||||
|
|
@ -1228,6 +1244,12 @@ dependencies = [
|
|||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.9"
|
||||
|
|
@ -3812,6 +3834,7 @@ dependencies = [
|
|||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest-websocket",
|
||||
"sanitize-filename",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
|
|
@ -4909,6 +4932,24 @@ dependencies = [
|
|||
"windows-registry 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-websocket"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd5f79b25f7f17a62cc9337108974431a66ae5a723ac0d9fe78ac1cce2027720"
|
||||
dependencies = [
|
||||
"async-tungstenite",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"reqwest",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tungstenite",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
|
|
@ -7190,6 +7231,23 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.1",
|
||||
"sha1",
|
||||
"thiserror 2.0.12",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.2"
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ reqwest = { version = "0.12", default-features = false, features = [
|
|||
"gzip",
|
||||
"brotli",
|
||||
] }
|
||||
reqwest-websocket = { version = "0.5", default-features = false }
|
||||
webbrowser = { version = "=1.0.4" }
|
||||
|
||||
# io
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ async fn initialize_state(handle: &tauri::AppHandle) -> LauncherResult<()> {
|
|||
tracing::warn!("failed to refresh accounts on startup: {err}");
|
||||
}
|
||||
|
||||
onelauncher_core::store::credentials::spawn_plus_playtime_tracking();
|
||||
|
||||
tracing::info!("initialized launcher successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
const ARGS_MAP = { 'core':'{"searchPackages":["provider","query"],"refreshAccounts":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"syncCluster":["cluster_id"],"removePackage":["cluster_id","package_hash"],"installModpack":["modpack","cluster_id"],"fetchMinecraftProfile":["uuid"],"getProcessLogTail":["id","max_lines"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"changeCape":["access_token","cape_uuid"],"getPackage":["provider","slug"],"setDiscordRPCMessage":["message"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCape":["access_token"],"readSettings":[],"getClusters":[],"launchCluster":["id","uuid","search_for_java"],"getLogs":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getLoadersForVersion":["mc_version"],"getGameVersions":[],"removeCluster":["id"],"getClusterById":["id"],"convertUsernameUUID":["username_uuid"],"getLogByName":["id","name"],"createSettingsProfile":["name"],"updateClusterById":["id","request"],"getProfileOrDefault":["name"],"isClusterRunning":["cluster_id"],"openMsaLogin":[],"setDefaultUser":["uuid"],"writeSettings":["setting"],"getWorlds":["id"],"getUser":["uuid"],"fetchLoggedInProfile":["access_token"],"open":["input"],"getRunningProcessesByClusterId":["cluster_id"],"togglePackage":["cluster_id","package_hash"],"getUsersFromAuthor":["provider","author"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUsers":[],"removeUser":["uuid"],"setClusterStage":["id","stage"],"createCluster":["options"],"killProcess":["pid"],"getLinkedPackages":["cluster_id"],"getDefaultUser":["fallback"],"getMultiplePackages":["provider","slugs"],"getRunningProcesses":[],"getScreenshots":["id"],"getGlobalProfile":[],"getPackageBody":["provider","body"],"updateClusterProfile":["name","profile"],"refreshAccount":["uuid"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'debug':'{"getType":[],"getOsDistro":[],"openDevTools":[],"getFamily":[],"getPackageVersion":[],"getGitCommitHash":[],"getFullDebugInfoParsed":[],"getOsVersion":[],"getArch":[],"getFullDebugInfoParsedString":[],"hasDevSettings":[],"getPlatform":[],"isInDev":[],"getFullDebugInfo":[],"getLocale":[],"getBuildTimestamp":[]}', 'oneclient':'{"refreshArt":["path"],"isBundleSyncing":[],"installUpdate":[],"updateBundlePackages":["cluster_id"],"getBundlesFor":["cluster_id"],"checkForUpdate":[],"getClustersGroupedByMajor":[],"extractBundleOverrides":["bundle_path","cluster_id"],"copyClusterContent":["source_id","target_id"],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"getVersions":[],"cacheArt":["path"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}' }
|
||||
const ARGS_MAP = { 'oneclient':'{"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"copyClusterContent":["source_id","target_id"],"getVersions":[],"isBundleSyncing":[],"installUpdate":[],"cacheArt":["path"],"getBundlesFor":["cluster_id"],"getClustersGroupedByMajor":[],"refreshArt":["path"],"extractBundleOverrides":["bundle_path","cluster_id"],"checkForUpdate":[],"updateBundlePackages":["cluster_id"]}', 'core':'{"getProfileOrDefault":["name"],"getWorlds":["id"],"createCluster":["options"],"setDefaultUser":["uuid"],"refreshAccounts":[],"getPackage":["provider","slug"],"getMultiplePackages":["provider","slugs"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"changeSkin":["access_token","skin_url","skin_variant"],"changeCape":["access_token","cape_uuid"],"installModpack":["modpack","cluster_id"],"open":["input"],"isClusterRunning":["cluster_id"],"killProcess":["pid"],"createSettingsProfile":["name"],"updateClusterProfile":["name","profile"],"getDefaultUser":["fallback"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"syncCluster":["cluster_id"],"fetchMinecraftProfile":["uuid"],"searchPackages":["provider","query"],"removeCape":["access_token"],"getLogs":["id"],"fetchLoggedInProfile":["access_token"],"getClusterById":["id"],"getScreenshots":["id"],"removePackage":["cluster_id","package_hash"],"setClusterStage":["id","stage"],"readSettings":[],"getUsersFromAuthor":["provider","author"],"getUser":["uuid"],"getUsers":[],"updateClusterById":["id","request"],"getPackageBody":["provider","body"],"launchCluster":["id","uuid","search_for_java"],"writeSettings":["setting"],"getLinkedPackages":["cluster_id"],"togglePackage":["cluster_id","package_hash"],"refreshAccount":["uuid"],"getRunningProcesses":[],"getClusters":[],"getLoadersForVersion":["mc_version"],"openMsaLogin":[],"removeCluster":["id"],"removeUser":["uuid"],"getLogByName":["id","name"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"convertUsernameUUID":["username_uuid"],"setDiscordRPCMessage":["message"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getGameVersions":[],"getGlobalProfile":[],"getProcessLogTail":["id","max_lines"],"getRunningProcessesByClusterId":["cluster_id"]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getOsDistro":[],"getPackageVersion":[],"getArch":[],"getOsVersion":[],"hasDevSettings":[],"getLocale":[],"getFamily":[],"getGitCommitHash":[],"getFullDebugInfoParsedString":[],"getFullDebugInfo":[],"getFullDebugInfoParsed":[],"openDevTools":[],"isInDev":[],"getPlatform":[],"getBuildTimestamp":[],"getType":[]}' }
|
||||
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
|
|
@ -380,22 +380,6 @@ removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
|
|||
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
|
||||
setDiscordRPCMessage: (message: string) => Promise<null>,
|
||||
open: (input: string) => Promise<null> },
|
||||
'debug': { openDevTools: () => Promise<void>,
|
||||
isInDev: () => Promise<boolean>,
|
||||
getArch: () => Promise<string>,
|
||||
getFamily: () => Promise<string>,
|
||||
getLocale: () => Promise<string>,
|
||||
getType: () => Promise<string>,
|
||||
getPlatform: () => Promise<string>,
|
||||
getOsVersion: () => Promise<string>,
|
||||
getOsDistro: () => Promise<string>,
|
||||
getGitCommitHash: () => Promise<string>,
|
||||
getBuildTimestamp: () => Promise<string>,
|
||||
getPackageVersion: () => Promise<string>,
|
||||
getFullDebugInfo: () => Promise<DebugInfoData>,
|
||||
getFullDebugInfoParsed: () => Promise<DebugInfoParsedLine[]>,
|
||||
getFullDebugInfoParsedString: () => Promise<string>,
|
||||
hasDevSettings: () => Promise<boolean> },
|
||||
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
|
||||
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
|
||||
getVersions: () => Promise<OnlineClusterManifest>,
|
||||
|
|
@ -412,7 +396,23 @@ copyClusterContent: (sourceId: number, targetId: number) => Promise<CopyClusterR
|
|||
openCluster: (folderName: string) => Promise<null> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'debug': { openDevTools: () => Promise<void>,
|
||||
isInDev: () => Promise<boolean>,
|
||||
getArch: () => Promise<string>,
|
||||
getFamily: () => Promise<string>,
|
||||
getLocale: () => Promise<string>,
|
||||
getType: () => Promise<string>,
|
||||
getPlatform: () => Promise<string>,
|
||||
getOsVersion: () => Promise<string>,
|
||||
getOsDistro: () => Promise<string>,
|
||||
getGitCommitHash: () => Promise<string>,
|
||||
getBuildTimestamp: () => Promise<string>,
|
||||
getPackageVersion: () => Promise<string>,
|
||||
getFullDebugInfo: () => Promise<DebugInfoData>,
|
||||
getFullDebugInfoParsed: () => Promise<DebugInfoParsedLine[]>,
|
||||
getFullDebugInfoParsedString: () => Promise<string>,
|
||||
hasDevSettings: () => Promise<boolean> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ thiserror = { workspace = true }
|
|||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest-websocket = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
whoami = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -884,14 +884,13 @@ async fn minecraft_entitlements(token: &str) -> Result<MinecraftEntitlements, Mi
|
|||
|
||||
#[derive(Deserialize)]
|
||||
struct PlusLoginResponse {
|
||||
#[allow(dead_code)]
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(credentials))]
|
||||
async fn record_plus_login(credentials: &MinecraftCredentials) -> anyhow::Result<()> {
|
||||
async fn record_plus_login(credentials: &MinecraftCredentials) -> anyhow::Result<Option<String>> {
|
||||
let Some(base_url) = Core::get().plus_backend_url.clone() else {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let server_id = generate_plus_server_id();
|
||||
|
|
@ -938,7 +937,75 @@ async fn record_plus_login(credentials: &MinecraftCredentials) -> anyhow::Result
|
|||
anyhow::bail!("Poly+ login rejected analytics login: status={status}, body={body}");
|
||||
}
|
||||
|
||||
let _response = plus_res.json::<PlusLoginResponse>().await?;
|
||||
let response = plus_res.json::<PlusLoginResponse>().await?;
|
||||
|
||||
Ok(Some(response.token))
|
||||
}
|
||||
|
||||
const PLUS_PLAYTIME_RECONNECT_DELAY: std::time::Duration = std::time::Duration::from_secs(15);
|
||||
|
||||
pub fn spawn_plus_playtime_tracking() {
|
||||
tokio::task::spawn(async move {
|
||||
loop {
|
||||
match default_plus_credentials().await {
|
||||
Ok(Some(credentials)) => {
|
||||
if let Err(err) = run_plus_playtime_session(&credentials).await {
|
||||
tracing::warn!("[plus] playtime websocket session ended: {err:?}");
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// No user logged in yet; retry after a delay.
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"[plus] failed to load credentials for playtime tracking: {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(PLUS_PLAYTIME_RECONNECT_DELAY).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn default_plus_credentials() -> LauncherResult<Option<MinecraftCredentials>> {
|
||||
let state = crate::store::State::get().await?;
|
||||
let mut store = state.credentials.write().await;
|
||||
store.get_default().await
|
||||
}
|
||||
|
||||
async fn run_plus_playtime_session(credentials: &MinecraftCredentials) -> anyhow::Result<()> {
|
||||
use futures::TryStreamExt;
|
||||
use reqwest_websocket::{Message, RequestBuilderExt};
|
||||
|
||||
let Some(base_url) = Core::get().plus_backend_url.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(token) = record_plus_login(credentials).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let ws_url = format!("{}/websocket", base_url.trim_end_matches('/'));
|
||||
|
||||
let mut websocket = crate::utils::http::REQWEST_CLIENT
|
||||
.get(&ws_url)
|
||||
.bearer_auth(&token)
|
||||
.upgrade()
|
||||
.send()
|
||||
.await?
|
||||
.into_websocket()
|
||||
.await?;
|
||||
|
||||
tracing::info!(
|
||||
"[plus] playtime websocket connected for user {}",
|
||||
credentials.id
|
||||
);
|
||||
|
||||
while let Some(message) = websocket.try_next().await? {
|
||||
if let Message::Close { .. } = message {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue