[Linux-only] basically bloxstap for sober

notification and dbus stuff

+114 -24
+1
src/api/constants.ts
··· 10 10 11 11 export const LOCAL_CONFIG_ROOT = `${homedir()}/.config/tuxstrap`; 12 12 export const SOBER_CONFIG_PATH = `${SOBER_PATH}/config/sober/config.json`; 13 + export const ROBLOX_COOKIES_FILE = `${SOBER_PATH}/data/sober/cookies`; 13 14 14 15 export const DISCORD_APPID = "1005469189907173486"; 15 16 export const SMALL_IMAGE_KEY = "roblox";
+23 -4
src/api/roblox/GameInfo.ts
··· 1 1 import { TimedDataCache } from "@ocbwoy3/libocbwoy3"; 2 + import { ROBLOX_COOKIES_FILE } from "../constants"; 3 + import { readFileSync } from "fs"; 2 4 3 5 const GameNameCache = new TimedDataCache<string, string>(900); // 30 minutes 4 6 7 + let cookies = ""; 8 + 9 + try { 10 + cookies = readFileSync(ROBLOX_COOKIES_FILE,"utf-8")?.toString() || ""; 11 + } catch {} 12 + 5 13 export async function getGameDetails(placeId: string): Promise<string | null> { 6 14 // Check cache first 7 - const cached = GameNameCache.get(placeId); 8 - if (cached) return cached; 15 + if (GameNameCache.has(placeId)) { 16 + return GameNameCache.get(placeId) || "???"; 17 + } 9 18 10 19 try { 20 + // roblox is stupid for making this endpoint locked behind an account 11 21 const res = await fetch( 12 22 `https://games.roblox.com/v1/games/multiget-place-details?placeIds=${placeId}`, 13 - { headers: { accept: "application/json" } } 23 + { 24 + headers: { 25 + accept: "application/json", 26 + cookie: cookies 27 + } 28 + } 14 29 ); 15 30 31 + // console.log(res,await res.body?.text()); 32 + 16 33 if (!res.ok) return null; 17 34 18 35 const data: { ··· 22 39 23 40 const gameName = data[0]?.name ?? "Unknown Game"; 24 41 25 - if (gameName) GameNameCache.set(placeId, gameName, 900_000); 42 + if (gameName) { 43 + GameNameCache.set(placeId, gameName, 900_000); 44 + } 26 45 27 46 return gameName; 28 47 } catch (e) {
+1 -1
src/index.ts
··· 76 76 const child = exec(`flatpak run ${SOBER_APPID} "${robloxLaunchURL}"`); 77 77 78 78 const watcher = new ActivityWatcher(child, { 79 - verbose: false, 79 + verbose: true, 80 80 tuxstrapLaunchTime: Date.now() 81 81 }); 82 82
+17 -17
src/plugins/debugPlugin.ts
··· 1 - import { getGameDetails } from "api/roblox/GameInfo"; 1 + import { getGameDetails } from "../api/roblox/GameInfo"; 2 2 import { registerPlugin } from "../api/Plugin"; 3 - import { SendNotification } from "api/linux"; 4 - import { ServerType } from "api/types"; 3 + import { SendNotification } from "../api/linux"; 5 4 6 5 registerPlugin( 7 6 { 8 - name: "TuxStrap Debug", 9 - id: "tuxstrap-debug", 7 + name: "TuxStrap Notifs", 8 + id: "tuxstrap-notif", 10 9 forceEnable: true, 11 10 configPrio: -9e9 12 11 }, 13 12 (plugin) => { 14 - plugin.on("BLOXSTRAP_RPC", (a) => console.log("BLOXSTRAP_RPC", a)); 15 13 plugin.on("GAME_JOIN", async (a) => { 16 - console.log("GAME_JOIN", a); 14 + // console.log("GAME_JOIN", a); 17 15 const gameName = await getGameDetails(a.placeId); 18 16 if (!gameName) return; 19 17 await SendNotification( 20 18 "Roblox", 21 - `${gameName}${a.ipAddrUdmux ? "\n(UDMUX Protected)" : ""}` 19 + `${gameName}${a.ipAddrUdmux ? "\n(UDMUX Protected)" : ""}`, 20 + 3000 22 21 ); 23 22 }); 24 23 plugin.on("TELEPORT", async (a) => { 25 - console.log("TELEPORT", a); 24 + // console.log("TELEPORT", a); 26 25 const pi = plugin.currentState.getPlaceId(); 27 26 if (!pi) return; 28 27 const gameName = await getGameDetails(pi); 29 28 if (!gameName) return; 30 29 await SendNotification( 31 30 "Roblox", 32 - `${gameName} is teleporting you to another place (${a.serverType})` 31 + `${gameName} is teleporting you to another place (${a.serverType})`, 32 + 3000 33 33 ); 34 34 }); 35 - plugin.on("GAME_LEAVE", (a) => console.log("GAME_LEAVE", a)); 36 - plugin.on("PLAYER_JOIN", (a) => console.log("PLAYER_JOIN", a)); 37 - plugin.on("PLAYER_LEAVE", (a) => console.log("PLAYER_LEAVE", a)); 38 - plugin.on("STATE_CHANGE", (a) => console.log("STATE_CHANGE", a)); 39 - plugin.currentState.onStateChange((a) => { 40 - console.log("onStateChange", a); 41 - }); 35 + // plugin.on("GAME_LEAVE", (a) => console.log("GAME_LEAVE", a)); 36 + // plugin.on("PLAYER_JOIN", (a) => console.log("PLAYER_JOIN", a)); 37 + // plugin.on("PLAYER_LEAVE", (a) => console.log("PLAYER_LEAVE", a)); 38 + // plugin.on("STATE_CHANGE", (a) => console.log("STATE_CHANGE", a)); 39 + // plugin.currentState.onStateChange((a) => { 40 + // console.log("onStateChange", a); 41 + // }); 42 42 } 43 43 );
+1
src/plugins/index.ts
··· 1 1 import "./default"; 2 2 import "./debugPlugin"; 3 + import "./systemIO"
+71
src/plugins/systemIO.ts
··· 1 + import { $ } from "bun"; 2 + import { GetProxyInterface, SendNotification } from "../api/linux"; 3 + import { registerPlugin } from "../api/Plugin"; 4 + import { getGameDetails } from "../api/roblox/GameInfo"; 5 + 6 + registerPlugin( 7 + { 8 + name: "System Clipboard & D-Bus", 9 + id: "tuxstrap-io", 10 + forceEnable: true, 11 + configPrio: -9e9 12 + }, 13 + async (plugin) => { 14 + 15 + plugin.setFFlag("FFlagClientAllowClipboardControl", true); 16 + plugin.setFFlag("FFlagClientAllowMPRISControl", true); 17 + plugin.setFFlag("FFlagIsLinux", true); 18 + 19 + plugin.on("BLOXSTRAP_RPC",async(a)=>{ 20 + switch (a.type) { 21 + case "WaylandCopy": { 22 + if (typeof a.data === "string" && a.data.length <= 512) {} else {return}; 23 + const gameName = await getGameDetails(plugin.currentState.getPlaceId()!); 24 + if (!gameName) return; 25 + await SendNotification( 26 + "Roblox", 27 + `${gameName} wrote to the Wayland clipboard!`, 28 + 3000 29 + ); 30 + $`echo ${a.data} | wl-copy -n`.quiet().nothrow(); 31 + break; 32 + } 33 + case "CallDBus": { 34 + if (typeof a.data !== "object") return; 35 + if (typeof a.data.busName !== "string") return; 36 + if (typeof a.data.action !== "string") return; 37 + 38 + const { busName, action }: {busName: string, action: string} = a.data; 39 + 40 + if (busName.length >= 64) return; 41 + if (!busName.startsWith("org.mpris.MediaPlayer2.")) return; 42 + 43 + if (!["Play", "Pause", "PlayPause"].includes(action)) return; 44 + 45 + try { 46 + const i = await GetProxyInterface(busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player"); 47 + switch (action) { 48 + case "Play": { 49 + await (i as any).Play(); 50 + break; 51 + } 52 + case "Pause": { 53 + await (i as any).Pause(); 54 + break; 55 + } 56 + case "PlayPause": { 57 + await (i as any).PlayPause(); 58 + break; 59 + } 60 + 61 + } 62 + } catch {} 63 + 64 + break; 65 + } 66 + 67 + } 68 + }) 69 + 70 + } 71 + );
-2
tsconfig.json
··· 25 25 "noUnusedLocals": false, 26 26 "noUnusedParameters": false, 27 27 "noPropertyAccessFromIndexSignature": false, 28 - 29 - "baseUrl": "./src/", 30 28 }, 31 29 }