From d6623891bbb0d2901e65a054b61d974149231643 Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Tue, 8 Jul 2025 14:56:44 +0200 Subject: [PATCH] feat: add osu status to rpc --- bun.lock | 9 +++ package.json | 1 + src-tauri/src/presence.rs | 2 +- src/lib/api/ezpp.ts | 23 +++++++- src/lib/presence.ts | 10 ++-- src/lib/types.ts | 62 +++++++++++++++++++++ src/lib/utils.ts | 11 ++++ src/pages/Launch.svelte | 112 ++++++++++++++++++++++++++++++++++++++ src/routes/+layout.svelte | 17 +++--- tests/imageCheck.test.ts | 24 ++++++++ 10 files changed, 256 insertions(+), 15 deletions(-) create mode 100644 tests/imageCheck.test.ts diff --git a/bun.lock b/bun.lock index 7bda47e..f0032a0 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,7 @@ "@sveltejs/kit": "2.22.2", "@sveltejs/vite-plugin-svelte": "5.1.0", "@tauri-apps/cli": "2.6.1", + "@types/bun": "^1.2.18", "@types/crypto-js": "^4.2.2", "@types/semver": "^7.7.0", "autoprefixer": "10.4.21", @@ -303,6 +304,8 @@ "@tauri-apps/plugin-sql": ["@tauri-apps/plugin-sql@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-JYwIocfsLaDWa41LMiZWuzts7yCJR+EpZPRmgpO7Gd7XiAS9S67dKz306j/k/d9XntB0YopMRBol2OIWMschuA=="], + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/crypto-js": ["@types/crypto-js@4.2.2", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="], @@ -313,6 +316,8 @@ "@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="], + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + "@types/semver": ["@types/semver@7.7.0", "", {}, "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.35.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.35.1", "@typescript-eslint/type-utils": "8.35.1", "@typescript-eslint/utils": "8.35.1", "@typescript-eslint/visitor-keys": "8.35.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.35.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg=="], @@ -369,6 +374,8 @@ "buffer-builder": ["buffer-builder@0.2.0", "", {}, "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg=="], + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="], @@ -397,6 +404,8 @@ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], diff --git a/package.json b/package.json index 3d0b5d1..678ea2f 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@sveltejs/kit": "2.22.2", "@sveltejs/vite-plugin-svelte": "5.1.0", "@tauri-apps/cli": "2.6.1", + "@types/bun": "^1.2.18", "@types/crypto-js": "^4.2.2", "@types/semver": "^7.7.0", "autoprefixer": "10.4.21", diff --git a/src-tauri/src/presence.rs b/src-tauri/src/presence.rs index 440d464..977c7dc 100644 --- a/src-tauri/src/presence.rs +++ b/src-tauri/src/presence.rs @@ -44,7 +44,7 @@ impl PresenceActor { state: "Idle in Launcher...".to_string(), details: " ".to_string(), large_image_key: "ezppfarm".to_string(), - large_image_text: "EZPPFarm v1.0.0".to_string(), + large_image_text: "EZPPFarm".to_string(), small_image_key: None, small_image_text: None, }; diff --git a/src/lib/api/ezpp.ts b/src/lib/api/ezpp.ts index 9d4f8d6..707712f 100644 --- a/src/lib/api/ezpp.ts +++ b/src/lib/api/ezpp.ts @@ -1,4 +1,9 @@ -import type { EZPPUser, EZPPUserInfoResponse, EZPPUserResponse } from '@/types'; +import type { + EZPPUser, + EZPPUserInfoResponse, + EZPPUserResponse, + EZPPUSerStatusResponse, +} from '@/types'; import { betterFetch } from '@better-fetch/fetch'; const BANCHO_ENDPOINT = 'https://c.ez-pp.farm/'; @@ -68,4 +73,20 @@ export const ezppfarm = { }); return request.error ? undefined : request.data; }, + getUserStatus: async (userId: number) => { + const request = await betterFetch( + `${API_ENDPOINT}v1/get_player_status`, + { + timeout, + query: { + id: userId, + }, + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'EZPPLauncher', + }, + } + ); + return request.error ? undefined : request.data; + }, }; diff --git a/src/lib/presence.ts b/src/lib/presence.ts index fb5c969..dbdd538 100644 --- a/src/lib/presence.ts +++ b/src/lib/presence.ts @@ -3,15 +3,15 @@ import { invoke } from '@tauri-apps/api/core'; export const connect = async () => await invoke('presence_connect'); export const disconnect = async () => await invoke('presence_disconnect'); export const updateStatus = async (status: { - state?: string; - details?: string; - large_image_key?: string; + state?: string | null; + details?: string | null; + largeImageKey?: string; }) => await invoke('presence_update_status', { state: status.state, details: status.details, - largeImageKey: status.large_image_key, + largeImageKey: status.largeImageKey, }); -export const updateUser = async (user: { username: string; id: string }) => +export const updateUser = async (user: { username: string; id?: string | null }) => await invoke('presence_update_user', { username: user.username, id: user.id }); export const isConnected = async () => await invoke('presence_is_connected'); diff --git a/src/lib/types.ts b/src/lib/types.ts index a0d97d0..42ceea0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -185,3 +185,65 @@ export type Release = { browser_download_url: string; }[]; }; + +export type EZPPUSerStatusResponse = EZPPUserOfflineStatus | EZPPUserOnlineStatus; + +type EZPPUserOfflineStatus = { + status: string; + player_status: { + online: false; + last_seen: number; + }; +}; + +type EZPPUserOnlineStatus = { + status: string; + player_status: { + online: true; + login_time: number; + status: { + action: EZPPActionStatus; + info_text: string; + mode: number; + mods: number; + beatmap: EZPPUserBeatmapStatus | null; + }; + }; +}; + +type EZPPUserBeatmapStatus = { + md5: string; + id: number; + set_id: number; + artist: string; + title: string; + version: string; + creator: string; + last_update: string; + total_length: number; + max_combo: number; + status: number; + plays: number; + passes: number; + mode: number; + bpm: number; + cs: number; + od: number; + ar: number; + hp: number; + diff: number; +}; + +export enum EZPPActionStatus { + AFK = 1, + PLAYING = 2, + EDITING = 3, + MODDING = 4, + MULTIPLAYER_SELECT = 5, + WATCHING = 6, + TESTING = 8, + SUBMITTING = 9, + MULTIPLAYER_IDLE = 11, + MULTIPLAYER_PLAYING = 12, + DIRECT = 13, +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e24ea61..e2e5006 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -89,3 +89,14 @@ export const formatBytes = (bytes: number, decimals = 2) => { export const openURL = async (url: string) => { await invoke('open_url_in_browser', { url }); }; + +export const urlIsValidImage = async (url: string) => { + try { + const request = await fetch(url); + if (!request.ok) return false; + const contentType = request.headers.get('content-type'); + return contentType?.startsWith('image/'); + } catch { + return false; + } +}; diff --git a/src/pages/Launch.svelte b/src/pages/Launch.svelte index 06db761..d983de8 100644 --- a/src/pages/Launch.svelte +++ b/src/pages/Launch.svelte @@ -48,6 +48,7 @@ numberHumanReadable, openURL, releaseStreamToReadable, + urlIsValidImage, } from '@/utils'; import { fade, scale } from 'svelte/transition'; import { Checkbox } from '@/components/ui/checkbox'; @@ -98,6 +99,8 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; import { ezppfarm } from '@/api/ezpp'; import Hearts from '@/components/ui/effects/Hearts.svelte'; + import { EZPPActionStatus } from '@/types'; + import * as presence from '@/presence'; let selectedTab = $state('home'); let progress = $state(-1); @@ -315,7 +318,116 @@ await replaceUIFiles(osuPath, false); await new Promise((res) => setTimeout(res, 1000)); await getCurrentWindow().hide(); + + let presenceUpdater: number | undefined = undefined; + + const isPresenceConnected = await presence.isConnected(); + + if ($discordPresence && isPresenceConnected) { + let osuDetected = false; + presenceUpdater = window.setInterval(async () => { + if (!osuDetected) { + const osuRunning = await isOsuRunning(); + if (osuRunning) osuDetected = true; + return; + } + if ($currentUser) { + const userStatus = await ezppfarm.getUserStatus($currentUser.id); + if (userStatus?.player_status.online) { + let largeImageKey = 'ezppfarm'; + let details = 'Idle...'; + let state = + userStatus.player_status.status.info_text.length > 0 + ? userStatus.player_status.status.info_text + : ' '; + let beatmapCover = false; + const gamemode = getModeAndTypeFromGamemode(userStatus.player_status.status.mode); + const gamemodeName = getGamemodeName( + modeIntToStr(gamemode.mode), + typeIntToStr(gamemode.type) + ); + + switch (userStatus.player_status.status.action) { + case EZPPActionStatus.AFK: + details = 'AFK...'; + state = ' '; + break; + case EZPPActionStatus.PLAYING: + details = 'Playing...'; + break; + case EZPPActionStatus.EDITING: + details = 'Editing...'; + break; + case EZPPActionStatus.MODDING: + details = 'Modding...'; + break; + case EZPPActionStatus.MULTIPLAYER_SELECT: + details = 'Multiplayer: Selecting a Beatmap...'; + state = ' '; + break; + case EZPPActionStatus.WATCHING: + details = 'Watching...'; + break; + case EZPPActionStatus.TESTING: + details = 'Testing...'; + break; + case EZPPActionStatus.SUBMITTING: + details = 'Submitting...'; + break; + case EZPPActionStatus.MULTIPLAYER_IDLE: + details = 'Multiplayer: Idle...'; + state = ' '; + break; + case EZPPActionStatus.MULTIPLAYER_PLAYING: + details = 'Multiplayer: Playing...'; + break; + case EZPPActionStatus.DIRECT: + details = 'Browsing osu!direct...'; + state = ' '; + break; + } + + if (userStatus.player_status.status.beatmap !== null && beatmapCover) { + const beatmapCoverImage = `https://assets.ppy.sh/beatmaps/${userStatus.player_status.status.beatmap.set_id}/covers/list@2x.jpg`; + const isValidImage = await urlIsValidImage(beatmapCoverImage); + if (isValidImage) largeImageKey = beatmapCoverImage; + } + + details = `[${gamemodeName}] ${details}`; + + await Promise.all([ + presence.updateUser({ + username: $currentUser.name, + id: $currentUser.id.toFixed(), + }), + presence.updateStatus({ + details, + state, + largeImageKey, + }), + ]); + } + } + }, 1000 * 5); + } + await runOsu(osuPath, true); + + if (presenceUpdater) { + window.clearInterval(presenceUpdater); + await Promise.all([ + presence.updateUser({ + username: ' ', + id: null, + }), + presence.updateStatus({ + details: null, + state: 'Idle in Launcher...', + largeImageKey: 'ezppfarm', + }), + ]); + } + launchInfo = 'Cleaning up...'; await getCurrentWindow().show(); await new Promise((res) => setTimeout(res, 1000)); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3b559a1..acffac9 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -109,14 +109,6 @@ osuInstallationPath.set(config_osu_installation_path.get('')); discordPresence.set(config_discord_presence.get(true)); - try { - if ($discordPresence) { - presenceLoading.set(true); - await presence.connect(); - presenceLoading.set(false); - } - } catch {} - patch.subscribe((val) => config_patching.set(val)); customCursor.subscribe((val) => config_custom_cursor.set(val)); cursorSmoothening.subscribe((val) => config_cursor_smoothening.set(val)); @@ -134,6 +126,15 @@ } catch {} }); + try { + if ($discordPresence) { + currentLoadingInfo.set('Connecting to Discord RPC...'); + presenceLoading.set(true); + await presence.connect(); + presenceLoading.set(false); + } + } catch {} + firstStartup.set(isFirstStartup); }); diff --git a/tests/imageCheck.test.ts b/tests/imageCheck.test.ts new file mode 100644 index 0000000..7a2d34b --- /dev/null +++ b/tests/imageCheck.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from 'bun:test'; + +const urlIsValidImage = async (url: string) => { + try { + const request = await fetch(url); + if (!request.ok) return false; + const contentType = request.headers.get('content-type'); + return contentType?.startsWith('image/'); + } catch { + return false; + } +}; + +test('image check', async () => { + const imageUrl = 'https://assets.ppy.sh/beatmaps/1/covers/list@2x.jpg'; + const imageCheckResult = await urlIsValidImage(imageUrl); + expect(imageCheckResult).toBe(true); +}); + +test('image check fail', async () => { + const imageUrl = 'https://assets.ppy.sh/beatmaps/0/covers/list@2x.jpg'; + const imageCheckResult = await urlIsValidImage(imageUrl); + expect(imageCheckResult).toBe(false); +});