UNPKG

webpresence

Version:

Discord Rich Presence for websites - Show your browsing activity in Discord

1,458 lines (1,450 loc) 67 kB
import path2 from 'path'; import { fileURLToPath } from 'url'; import chalk from 'chalk'; import { spawn, execSync } from 'child_process'; import fs from 'fs'; import os from 'os'; import express, { Router } from 'express'; import http from 'http'; import cors from 'cors'; import net from 'net'; import * as DiscordRPC from 'discord-rpc'; import { WebSocketServer, WebSocket } from 'ws'; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var init_esm_shims = __esm({ "node_modules/tsup/assets/esm_shims.js"() { } }); // src/config/index.ts var defaultConfig, runtimeConfig, config; var init_config = __esm({ "src/config/index.ts"() { init_esm_shims(); defaultConfig = { // Discord RPC configuration discord: { clientId: "1370122815273046117", // Application ID (not logged for privacy) reconnectAttempts: 0, maxReconnectAttempts: 10 }, // Server configuration server: { port: 8874, activityTimeout: 18e4, // 3 minutes timeout for client activity (4x the heartbeat interval) inactiveCheckInterval: 15e3 // Check for inactive clients every 15 seconds }, // Presence configuration presence: { enabled: true, defaultPrefix: "Viewing", // Default prefix for details text creditText: "by Utkarsh", // Credit text to show in state buttons: [ { label: "GitHub Repository", url: "https://github.com/utkarshthedev/WebPresence" }, { label: "Follow on Twitter", url: "https://twitter.com/utkarshthedev" } ], largeImageKey: "web", smallImageKey: "me", smallImageText: "Utkarsh Tiwari", // Browser-specific icons browserIcons: { chrome: { iconKey: "chrome", text: "Google Chrome" }, firefox: { iconKey: "firefox", text: "Mozilla Firefox" }, default: { iconKey: "me", text: "Utkarsh Tiwari" } } } }; runtimeConfig = { ...defaultConfig, // User preferences that can be customized userPreferences: { prefixText: defaultConfig.presence.defaultPrefix, disabledSites: [], // Sites where presence should be disabled alwaysEnabledSites: [], // Sites where presence should always be shown continuousTimer: true // Keep timer running when switching tabs (enabled by default) } }; config = { // Get the current configuration get: /* @__PURE__ */ __name(() => runtimeConfig, "get"), // Get Discord configuration getDiscord: /* @__PURE__ */ __name(() => runtimeConfig.discord, "getDiscord"), // Get server configuration getServer: /* @__PURE__ */ __name(() => runtimeConfig.server, "getServer"), // Get presence configuration getPresence: /* @__PURE__ */ __name(() => runtimeConfig.presence, "getPresence"), // Get user preferences getUserPreferences: /* @__PURE__ */ __name(() => runtimeConfig.userPreferences, "getUserPreferences"), // Update user preferences updateUserPreferences: /* @__PURE__ */ __name((preferences) => { runtimeConfig.userPreferences = { ...runtimeConfig.userPreferences, ...preferences }; return runtimeConfig.userPreferences; }, "updateUserPreferences"), // Reset user preferences to defaults resetUserPreferences: /* @__PURE__ */ __name(() => { runtimeConfig.userPreferences.prefixText = defaultConfig.presence.defaultPrefix; runtimeConfig.userPreferences.disabledSites = []; runtimeConfig.userPreferences.alwaysEnabledSites = []; runtimeConfig.userPreferences.continuousTimer = true; return runtimeConfig.userPreferences; }, "resetUserPreferences") }; } }); function formatTimestamp() { return (/* @__PURE__ */ new Date()).toISOString(); } function formatLogMessage(level, message, meta) { const timestamp = formatTimestamp(); let logMessage = `[${timestamp}] [${level}] ${message}`; if (meta) { try { if (typeof meta === "string") { logMessage += ` - ${meta}`; } else { logMessage += ` - ${JSON.stringify(meta)}`; } } catch (error) { logMessage += ` - [Error serializing metadata: ${error}]`; } } return logMessage; } var isRunningAsCLI, verbose, logger; var init_logger = __esm({ "src/utils/logger.ts"() { init_esm_shims(); __name(formatTimestamp, "formatTimestamp"); __name(formatLogMessage, "formatLogMessage"); isRunningAsCLI = process.argv.length > 1 && process.argv[1].includes("cli"); verbose = !isRunningAsCLI; logger = { error: /* @__PURE__ */ __name((message, meta) => { if (isRunningAsCLI) { console.error(chalk.red(`ERROR: ${message}`)); if (verbose && meta) { console.error(chalk.gray(JSON.stringify(meta, null, 2))); } } else { console.error(formatLogMessage("ERROR" /* ERROR */, message, meta)); } }, "error"), warn: /* @__PURE__ */ __name((message, meta) => { if (isRunningAsCLI) { if (verbose || message.includes("Discord") || message.includes("connection")) { console.warn(chalk.yellow(`WARNING: ${message}`)); if (verbose && meta) { console.warn(chalk.gray(JSON.stringify(meta, null, 2))); } } } else { console.warn(formatLogMessage("WARN" /* WARN */, message, meta)); } }, "warn"), info: /* @__PURE__ */ __name((message, meta) => { if (isRunningAsCLI) { const isImportant = message.includes("Server") || message.includes("Discord") || message.includes("connected") || message.includes("presence"); if (verbose || isImportant) { console.log(chalk.blue(message)); if (verbose && meta) { console.log(chalk.gray(JSON.stringify(meta, null, 2))); } } } else { console.log(formatLogMessage("INFO" /* INFO */, message, meta)); } }, "info"), debug: /* @__PURE__ */ __name((message, meta) => { if (process.env.DEBUG) { if (isRunningAsCLI && verbose) { console.log(chalk.gray(`DEBUG: ${message}`)); if (meta) { console.log(chalk.gray(JSON.stringify(meta, null, 2))); } } else if (!isRunningAsCLI) { console.log(formatLogMessage("DEBUG" /* DEBUG */, message, meta)); } } }, "debug"), // CLI-specific methods for better UX success: /* @__PURE__ */ __name((message) => { console.log(chalk.green(message)); }, "success"), important: /* @__PURE__ */ __name((message) => { console.log(chalk.cyan(message)); }, "important"), // Control verbosity setVerbose: /* @__PURE__ */ __name((v) => { verbose = v; }, "setVerbose"), isVerbose: /* @__PURE__ */ __name(() => verbose, "isVerbose") }; } }); // src/utils/daemon.ts var daemon_exports = {}; __export(daemon_exports, { getDaemonLogPath: () => getDaemonLogPath, getDaemonPid: () => getDaemonPid, isDaemonRunning: () => isDaemonRunning, startDaemon: () => startDaemon, stopDaemon: () => stopDaemon }); function ensureDaemonDir() { if (!fs.existsSync(DAEMON_DIR)) { try { fs.mkdirSync(DAEMON_DIR, { recursive: true }); } catch (error) { logger.error(`Failed to create daemon directory: ${error.message}`); throw error; } } } async function startDaemon(options = {}) { ensureDaemonDir(); if (isDaemonRunning()) { return { success: false, message: "Daemon is already running" }; } try { const args = ["start"]; if (options.port) { args.push("--port", options.port.toString()); } fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Starting WebPresence daemon ` ); const __filename2 = fileURLToPath(import.meta.url); const __dirname2 = path2.dirname(__filename2); const cliPath = path2.resolve(__dirname2, "../../dist/cli.js"); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] CLI path: ${cliPath} ` ); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Starting daemon with standalone script ` ); const __filename22 = fileURLToPath(import.meta.url); const __dirname22 = path2.dirname(__filename22); const daemonPath = path2.resolve(__dirname22, "../../dist/daemon.js"); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Daemon script path: ${daemonPath} ` ); if (!fs.existsSync(daemonPath)) { fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: Daemon script not found at ${daemonPath} ` ); throw new Error(`Daemon script not found at ${daemonPath}`); } const child = spawn(process.execPath, [daemonPath], { detached: true, stdio: ["ignore", fs.openSync(LOG_FILE, "a"), fs.openSync(LOG_FILE, "a")], env: { ...process.env, WEBPRESENCE_DAEMON: "true" } }); child.unref(); fs.writeFileSync(PID_FILE, child.pid.toString()); return { success: true, message: `Daemon started with PID ${child.pid}`, pid: child.pid }; } catch (error) { logger.error(`Failed to start daemon: ${error.message}`, { error: error.stack }); return { success: false, message: `Failed to start daemon: ${error.message}` }; } } async function stopDaemon() { if (!isDaemonRunning()) { return { success: false, message: "Daemon is not running" }; } try { const pid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10); process.kill(pid, "SIGTERM"); await new Promise((resolve) => { const checkInterval = setInterval(() => { try { process.kill(pid, 0); } catch (error) { clearInterval(checkInterval); resolve(); } }, 100); setTimeout(() => { clearInterval(checkInterval); resolve(); }, 5e3); }); fs.unlinkSync(PID_FILE); return { success: true, message: `Daemon with PID ${pid} stopped` }; } catch (error) { logger.error(`Failed to stop daemon: ${error.message}`, { error: error.stack }); return { success: false, message: `Failed to stop daemon: ${error.message}` }; } } function isDaemonRunning() { if (!fs.existsSync(PID_FILE)) { return false; } try { const pidContent = fs.readFileSync(PID_FILE, "utf8").trim(); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Checking daemon status, PID file content: ${pidContent} ` ); if (!pidContent || isNaN(parseInt(pidContent, 10))) { fs.unlinkSync(PID_FILE); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Invalid PID content, removing PID file ` ); return false; } const pid = parseInt(pidContent, 10); try { process.kill(pid, 0); } catch (e) { fs.unlinkSync(PID_FILE); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Process with PID ${pid} does not exist, removing PID file ` ); return false; } try { let psCommand; if (process.platform === "win32") { psCommand = `tasklist /FI "PID eq ${pid}" /FO CSV /NH`; } else { psCommand = `ps -p ${pid} -o comm=,args=`; } const output = execSync(psCommand, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] // Suppress stderr }); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Process info for PID ${pid}: ${output.trim()} ` ); const isNodeProcess = output.toLowerCase().includes("node"); const isWebPresenceProcess = output.toLowerCase().includes("webpresence") || output.toLowerCase().includes("daemon.js") || output.toLowerCase().includes("cli.js"); if (!isNodeProcess) { fs.unlinkSync(PID_FILE); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Process with PID ${pid} is not a Node.js process, removing PID file ` ); return false; } try { const serverPort = config.getServer().port || 8874; try { const net2 = __require("net"); const tester = net2.createServer().once("error", (err) => { if (err.code === "EADDRINUSE") { fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Port ${serverPort} is in use (Node.js check) ` ); tester.close(); return true; } }).once("listening", () => { tester.close(); }); tester.listen(serverPort); } catch (nodeCheckError) { } try { const netstatCommand = process.platform === "win32" ? `netstat -ano | findstr :${serverPort}` : `netstat -tuln | grep :${serverPort}`; const netstatOutput = execSync(netstatCommand, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] // Suppress stderr to avoid showing command not found errors }); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Port check for ${serverPort}: ${netstatOutput.trim()} ` ); return true; } catch (netstatError) { if (process.platform !== "win32") { try { const lsofOutput = execSync(`lsof -i :${serverPort} -t`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] // Suppress stderr }); if (lsofOutput.trim()) { fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Port ${serverPort} is in use (lsof check) ` ); return true; } } catch (lsofError) { } } fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Port check failed, but process appears to be running ` ); return isWebPresenceProcess; } } catch (portCheckError) { fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] All port checks failed, but process appears to be running ` ); return isWebPresenceProcess; } } catch (e) { fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Error checking process details: ${e} ` ); try { process.kill(pid, 0); return true; } catch (killError) { fs.unlinkSync(PID_FILE); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Process with PID ${pid} does not exist, removing PID file ` ); return false; } } } catch (error) { try { fs.unlinkSync(PID_FILE); fs.appendFileSync( LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] Error checking daemon status, removing PID file: ${error} ` ); } catch (e) { } return false; } } function getDaemonPid() { if (!isDaemonRunning()) { return null; } try { return parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10); } catch (error) { return null; } } function getDaemonLogPath() { return LOG_FILE; } var DAEMON_DIR, PID_FILE, LOG_FILE; var init_daemon = __esm({ "src/utils/daemon.ts"() { init_esm_shims(); init_logger(); init_config(); DAEMON_DIR = path2.join(os.homedir(), ".webpresence"); PID_FILE = path2.join(DAEMON_DIR, "webpresence.pid"); LOG_FILE = path2.join(DAEMON_DIR, "webpresence.log"); __name(ensureDaemonDir, "ensureDaemonDir"); __name(startDaemon, "startDaemon"); __name(stopDaemon, "stopDaemon"); __name(isDaemonRunning, "isDaemonRunning"); __name(getDaemonPid, "getDaemonPid"); __name(getDaemonLogPath, "getDaemonLogPath"); } }); // src/index.ts init_esm_shims(); // src/services/discord.ts init_esm_shims(); init_config(); // src/data/siteIcons.ts init_esm_shims(); var siteIcons = [ // Video & Entertainment platforms { domain: "youtube.com", iconKey: "youtube", displayName: "YouTube" }, { domain: "youtu.be", iconKey: "youtube", displayName: "YouTube" }, { domain: "netflix.com", iconKey: "netflix", displayName: "Netflix" }, { domain: "twitch.tv", iconKey: "twitch", displayName: "Twitch" }, { domain: "spotify.com", iconKey: "spotify", displayName: "Spotify" }, { domain: "soundcloud.com", iconKey: "soundcloud", displayName: "SoundCloud" }, { domain: "music.youtube.com", iconKey: "ytmusic", displayName: "YouTube Music" }, { domain: "disneyplus.com", iconKey: "disneyplus", displayName: "Disney+" }, { domain: "primevideo.com", iconKey: "primevideo", displayName: "Prime Video" }, { domain: "hotstar.com", iconKey: "hotstar", displayName: "Disney+ Hotstar" }, { domain: "hbomax.com", iconKey: "hbomax", displayName: "HBO Max" }, { domain: "sonyliv.com", iconKey: "sonyliv", displayName: "Sony LIV" }, { domain: "zee5.com", iconKey: "zee5", displayName: "ZEE5" }, { domain: "jiocinema.com", iconKey: "jiocinema", displayName: "JioCinema" }, { domain: "crunchyroll.com", iconKey: "crunchyroll", displayName: "Crunchyroll" }, { domain: "imdb.com", iconKey: "imdb", displayName: "IMDb" }, // Social media { domain: "instagram.com", iconKey: "instagram", displayName: "Instagram" }, { domain: "facebook.com", iconKey: "facebook", displayName: "Facebook" }, { domain: "x.com", iconKey: "twitter", displayName: "Twitter (X)" }, { domain: "threads.net", iconKey: "threads", displayName: "Threads" }, { domain: "linkedin.com", iconKey: "linkedin", displayName: "LinkedIn" }, { domain: "reddit.com", iconKey: "reddit", displayName: "Reddit" }, { domain: "tiktok.com", iconKey: "tiktok", displayName: "TikTok" }, { domain: "discord.com", iconKey: "discord", displayName: "Discord" }, { domain: "pinterest.com", iconKey: "pinterest", displayName: "Pinterest" }, { domain: "whatsapp.com", iconKey: "whatsapp", displayName: "WhatsApp" }, { domain: "web.whatsapp.com", iconKey: "whatsapp", displayName: "WhatsApp Web" }, { domain: "telegram.org", iconKey: "telegram", displayName: "Telegram" }, { domain: "web.telegram.org", iconKey: "telegram", displayName: "Telegram Web" }, { domain: "snapchat.com", iconKey: "snapchat", displayName: "Snapchat" }, // Development platforms { domain: "github.com", iconKey: "github", displayName: "GitHub" }, { domain: "gitlab.com", iconKey: "gitlab", displayName: "GitLab" }, { domain: "stackoverflow.com", iconKey: "stackoverflow", displayName: "Stack Overflow" }, { domain: "codepen.io", iconKey: "codepen", displayName: "CodePen" }, { domain: "replit.com", iconKey: "replit", displayName: "Replit" }, { domain: "codesandbox.io", iconKey: "codesandbox", displayName: "CodeSandbox" }, { domain: "vercel.com", iconKey: "vercel", displayName: "Vercel" }, { domain: "netlify.com", iconKey: "netlify", displayName: "Netlify" }, { domain: "heroku.com", iconKey: "heroku", displayName: "Heroku" }, { domain: "npmjs.com", iconKey: "npm", displayName: "npm" }, { domain: "github.dev", iconKey: "githubdev", displayName: "GitHub.dev" }, { domain: "code.visualstudio.com", iconKey: "vscode", displayName: "VS Code" }, { domain: "vscode.dev", iconKey: "vscode", displayName: "VS Code Web" }, { domain: "jetbrains.com", iconKey: "jetbrains", displayName: "JetBrains" }, { domain: "intellij-idea", iconKey: "intellij", displayName: "IntelliJ IDEA" }, { domain: "render.com", iconKey: "render", displayName: "Render" }, { domain: "railway.app", iconKey: "railway", displayName: "Railway" }, { domain: "fly.io", iconKey: "flyio", displayName: "Fly.io" }, { domain: "digitalocean.com", iconKey: "digitalocean", displayName: "DigitalOcean" }, // Coding & Learning Platforms { domain: "leetcode.com", iconKey: "leetcode", displayName: "LeetCode" }, { domain: "hackerrank.com", iconKey: "hackerrank", displayName: "HackerRank" }, { domain: "codewars.com", iconKey: "codewars", displayName: "Codewars" }, { domain: "codeforces.com", iconKey: "codeforces", displayName: "Codeforces" }, { domain: "interviewbit.com", iconKey: "interviewbit", displayName: "InterviewBit" }, { domain: "codingninjas.com", iconKey: "codingninjas", displayName: "Coding Ninjas" }, { domain: "scaler.com", iconKey: "scaler", displayName: "Scaler Academy" }, { domain: "freecodecamp.org", iconKey: "freecodecamp", displayName: "freeCodeCamp" }, { domain: "w3schools.com", iconKey: "w3schools", displayName: "W3Schools" }, { domain: "geeksforgeeks.org", iconKey: "geeksforgeeks", displayName: "GeeksforGeeks" }, { domain: "tutorialspoint.com", iconKey: "tutorialspoint", displayName: "TutorialsPoint" }, { domain: "javatpoint.com", iconKey: "javatpoint", displayName: "JavaTpoint" }, { domain: "mdn.dev", iconKey: "mdn", displayName: "MDN Web Docs" }, { domain: "developer.mozilla.org", iconKey: "mdn", displayName: "MDN Web Docs" }, // AI & ML Platforms { domain: "openai.com", iconKey: "openai", displayName: "OpenAI" }, { domain: "chatgpt.com", iconKey: "chatgpt", displayName: "ChatGPT" }, { domain: "anthropic.com", iconKey: "anthropic", displayName: "Anthropic" }, { domain: "claude.ai", iconKey: "claude", displayName: "Claude" }, { domain: "gemini.google.com", iconKey: "gemini", displayName: "Google Gemini" }, { domain: "huggingface.co", iconKey: "huggingface", displayName: "Hugging Face" }, { domain: "kaggle.com", iconKey: "kaggle", displayName: "Kaggle" }, { domain: "colab.research.google.com", iconKey: "colab", displayName: "Google Colab" }, { domain: "perplexity.ai", iconKey: "perplexity", displayName: "Perplexity AI" }, { domain: "midjourney.com", iconKey: "midjourney", displayName: "Midjourney" }, // Gaming Platforms { domain: "steam.com", iconKey: "steam", displayName: "Steam" }, { domain: "steampowered.com", iconKey: "steam", displayName: "Steam" }, { domain: "epicgames.com", iconKey: "epic", displayName: "Epic Games" }, { domain: "ea.com", iconKey: "ea", displayName: "Electronic Arts" }, { domain: "ubisoft.com", iconKey: "ubisoft", displayName: "Ubisoft" }, { domain: "battle.net", iconKey: "battlenet", displayName: "Battle.net" }, { domain: "blizzard.com", iconKey: "blizzard", displayName: "Blizzard" }, { domain: "playstation.com", iconKey: "playstation", displayName: "PlayStation" }, { domain: "xbox.com", iconKey: "xbox", displayName: "Xbox" }, { domain: "nintendo.com", iconKey: "nintendo", displayName: "Nintendo" }, { domain: "riotgames.com", iconKey: "riot", displayName: "Riot Games" }, { domain: "leagueoflegends.com", iconKey: "lol", displayName: "League of Legends" }, { domain: "minecraft.net", iconKey: "minecraft", displayName: "Minecraft" }, { domain: "roblox.com", iconKey: "roblox", displayName: "Roblox" }, { domain: "fortnite.com", iconKey: "fortnite", displayName: "Fortnite" }, // Productivity & Work { domain: "notion.so", iconKey: "notion", displayName: "Notion" }, { domain: "trello.com", iconKey: "trello", displayName: "Trello" }, { domain: "slack.com", iconKey: "slack", displayName: "Slack" }, { domain: "zoom.us", iconKey: "zoom", displayName: "Zoom" }, { domain: "meet.google.com", iconKey: "gmeet", displayName: "Google Meet" }, { domain: "teams.microsoft.com", iconKey: "teams", displayName: "Microsoft Teams" }, { domain: "figma.com", iconKey: "figma", displayName: "Figma" }, { domain: "canva.com", iconKey: "canva", displayName: "Canva" }, // Google services { domain: "google.com", iconKey: "google", displayName: "Google" }, { domain: "gmail.com", iconKey: "gmail", displayName: "Gmail" }, { domain: "drive.google.com", iconKey: "gdrive", displayName: "Google Drive" }, { domain: "docs.google.com", iconKey: "gdocs", displayName: "Google Docs" }, { domain: "maps.google.com", iconKey: "gmaps", displayName: "Google Maps" }, // Education { domain: "coursera.org", iconKey: "coursera", displayName: "Coursera" }, { domain: "udemy.com", iconKey: "udemy", displayName: "Udemy" }, { domain: "khanacademy.org", iconKey: "khan", displayName: "Khan Academy" }, { domain: "pw.live", iconKey: "pw", displayName: "Physics Wallah" }, { domain: "physicswallah.live", iconKey: "pw", displayName: "Physics Wallah" }, { domain: "unacademy.com", iconKey: "unacademy", displayName: "Unacademy" }, { domain: "byjus.com", iconKey: "byjus", displayName: "BYJU'S" }, { domain: "vedantu.com", iconKey: "vedantu", displayName: "Vedantu" }, { domain: "doubtnut.com", iconKey: "doubtnut", displayName: "Doubtnut" }, { domain: "edx.org", iconKey: "edx", displayName: "edX" }, { domain: "codecademy.com", iconKey: "codecademy", displayName: "Codecademy" }, { domain: "datacamp.com", iconKey: "datacamp", displayName: "DataCamp" }, { domain: "sololearn.com", iconKey: "sololearn", displayName: "SoloLearn" }, // News & Media { domain: "medium.com", iconKey: "medium", displayName: "Medium" }, { domain: "dev.to", iconKey: "devto", displayName: "DEV Community" }, { domain: "hashnode.com", iconKey: "hashnode", displayName: "Hashnode" }, { domain: "indiatimes.com", iconKey: "toi", displayName: "Times of India" }, { domain: "ndtv.com", iconKey: "ndtv", displayName: "NDTV" }, { domain: "hindustantimes.com", iconKey: "ht", displayName: "Hindustan Times" }, // Cloud services & Databases { domain: "aws.amazon.com", iconKey: "aws", displayName: "AWS" }, { domain: "cloud.google.com", iconKey: "gcloud", displayName: "Google Cloud" }, { domain: "azure.microsoft.com", iconKey: "azure", displayName: "Microsoft Azure" }, { domain: "firebase.google.com", iconKey: "firebase", displayName: "Firebase" }, { domain: "supabase.com", iconKey: "supabase", displayName: "Supabase" }, { domain: "redis.io", iconKey: "redis", displayName: "Redis" }, { domain: "drizzle.team", iconKey: "drizzle", displayName: "Drizzle ORM" }, { domain: "prisma.io", iconKey: "prisma", displayName: "Prisma" }, { domain: "mongodb.com", iconKey: "mongodb", displayName: "MongoDB" }, { domain: "postgresql.org", iconKey: "postgresql", displayName: "PostgreSQL" }, // E-commerce & Shopping { domain: "amazon.com", iconKey: "amazon", displayName: "Amazon" }, { domain: "amazon.in", iconKey: "amazon", displayName: "Amazon India" }, { domain: "flipkart.com", iconKey: "flipkart", displayName: "Flipkart" }, { domain: "myntra.com", iconKey: "myntra", displayName: "Myntra" }, { domain: "ajio.com", iconKey: "ajio", displayName: "AJIO" }, { domain: "meesho.com", iconKey: "meesho", displayName: "Meesho" }, { domain: "nykaa.com", iconKey: "nykaa", displayName: "Nykaa" }, { domain: "swiggy.com", iconKey: "swiggy", displayName: "Swiggy" }, { domain: "zomato.com", iconKey: "zomato", displayName: "Zomato" }, // Other popular sites { domain: "wikipedia.org", iconKey: "wikipedia", displayName: "Wikipedia" }, { domain: "bing.com", iconKey: "bing", displayName: "Bing" }, { domain: "duckduckgo.com", iconKey: "duckduckgo", displayName: "DuckDuckGo" } ]; function findIconForDomain(domain) { const lowerDomain = domain.toLowerCase(); return siteIcons.find((site) => lowerDomain.includes(site.domain)); } __name(findIconForDomain, "findIconForDomain"); // src/services/discord.ts init_logger(); var rpc = null; var rpcConnected = false; var rpcReady = false; var reconnectTimeout = null; var connectionInProgress = false; var initialTimestamp = null; async function connectToDiscord() { if (connectionInProgress) { logger.info( "Discord connection already in progress, skipping duplicate attempt" ); return false; } if (reconnectTimeout) { clearTimeout(reconnectTimeout); reconnectTimeout = null; } connectionInProgress = true; const discordConfig = config.getDiscord(); const maxReconnectAttempts = discordConfig.maxReconnectAttempts || 10; try { if (rpc) { try { logger.info("Cleaning up previous Discord RPC client..."); rpc.destroy(); } catch (e) { logger.warn("Error during Discord RPC client cleanup (non-critical)", { error: e }); } } logger.info("Creating Discord RPC client..."); rpc = new DiscordRPC.Client({ transport: "ipc" }); logger.info("Attempting to connect to Discord..."); rpc.on("ready", () => { rpcReady = true; discordConfig.reconnectAttempts = 0; logger.success( "\u2705 Discord RPC ready - Rich Presence can now be displayed" ); if (rpc && rpc.user) { logger.success(`\u2705 Connected to Discord successfully`); } }); rpc.on("connected", () => { rpcConnected = true; logger.success("\u2705 Connected to Discord RPC service"); }); rpc.on("disconnected", () => { logger.warn("\u274C Disconnected from Discord RPC"); rpcConnected = false; rpcReady = false; discordConfig.reconnectAttempts++; if (discordConfig.reconnectAttempts > maxReconnectAttempts) { logger.warn( `Maximum reconnect attempts (${maxReconnectAttempts}) reached. Will try again in 60 seconds.` ); discordConfig.reconnectAttempts = 1; reconnectTimeout = setTimeout(connectToDiscord, 6e4); return; } const delay = Math.min( 3e4, Math.pow(2, Math.min(discordConfig.reconnectAttempts, 5)) * 1e3 ); logger.info( `Will attempt to reconnect in ${delay / 1e3} seconds (attempt ${discordConfig.reconnectAttempts} of ${maxReconnectAttempts})` ); reconnectTimeout = setTimeout(connectToDiscord, delay); }); rpc.on("error", (error) => { logger.error("Discord RPC client error", { error }); }); logger.info("Logging in with Discord RPC client..."); try { await rpc.login({ clientId: discordConfig.clientId }); } catch (error) { logger.error("Discord login failed:", { error }); connectionInProgress = false; rpcConnected = false; rpcReady = false; const delay = Math.min( 3e4, Math.pow(2, Math.min(discordConfig.reconnectAttempts, 5)) * 1e3 ); logger.info( `Will attempt to reconnect in ${delay / 1e3} seconds (attempt ${discordConfig.reconnectAttempts} of ${maxReconnectAttempts})` ); reconnectTimeout = setTimeout(connectToDiscord, delay); return false; } } catch (error) { logger.error("\u274C Failed to connect to Discord:", { error: error.message, stack: error.stack }); if (error.message === "RPC_CONNECTION_TIMEOUT") { logger.info("\n\u274C Possible issues:"); logger.info("1. Discord is running but RPC is disabled"); logger.info("2. Discord IPC connection is blocked by a firewall"); logger.info("3. The connection approach might need adjustment"); logger.info("\n\u{1F4CB} Troubleshooting steps:"); logger.info("1. Check if 'Game Activity' is enabled in Discord settings"); logger.info( " - Open Discord Settings > Activity Settings > Activity Status" ); logger.info( " - Make sure 'Display current activity as a status message' is ON" ); logger.info( "2. Try restarting Discord completely (close from system tray)" ); logger.info("3. Make sure no firewall is blocking the connection"); } rpcConnected = false; rpcReady = false; connectionInProgress = false; discordConfig.reconnectAttempts++; if (discordConfig.reconnectAttempts > maxReconnectAttempts) { logger.warn( `Maximum reconnect attempts (${maxReconnectAttempts}) reached. Will try again in 60 seconds.` ); discordConfig.reconnectAttempts = 1; reconnectTimeout = setTimeout(connectToDiscord, 6e4); return false; } const delay = Math.min( 3e4, Math.pow(2, Math.min(discordConfig.reconnectAttempts, 5)) * 1e3 ); logger.info( `\u23F1\uFE0F Will attempt to reconnect in ${delay / 1e3} seconds (attempt ${discordConfig.reconnectAttempts} of ${maxReconnectAttempts})` ); reconnectTimeout = setTimeout(connectToDiscord, delay); return false; } connectionInProgress = false; return true; } __name(connectToDiscord, "connectToDiscord"); function setActivity(title, url, browser) { if (!rpcConnected || !rpcReady || !rpc) { logger.warn("Discord RPC not ready, cannot set activity"); return false; } try { let domain = ""; try { domain = new URL(url).hostname; } catch (e) { domain = url; logger.warn(`Failed to parse URL: ${url}`, { error: e }); } const presenceConfig = config.getPresence(); const userPrefs = config.getUserPreferences(); const now = Date.now(); if (initialTimestamp === null || !userPrefs.continuousTimer) { initialTimestamp = now; } const siteIcon = findIconForDomain(domain); let displayDomain = (siteIcon == null ? void 0 : siteIcon.displayName) || domain; if (displayDomain.length < 2) { displayDomain = (siteIcon == null ? void 0 : siteIcon.displayName) || `${domain} website`; logger.info( `Domain name too short, using extended name: ${displayDomain}` ); } const browserIcon = browser && browser in presenceConfig.browserIcons ? presenceConfig.browserIcons[browser] : presenceConfig.browserIcons.default; if (siteIcon) { logger.info(`Using custom icon for ${domain}: ${siteIcon.iconKey}`); } else { logger.info(`Using default web icon for ${domain}`); } if (browser) { logger.info(`Using browser icon for ${browser}: ${browserIcon.iconKey}`); } const activityData = { // Main title with customized prefix: "[prefix] - [page title]" details: `${userPrefs.prefixText} - ${title.length > 40 ? `${title.substring(0, 37)}...` : title}`, // Credit text: "by utkarsh tiwari" state: `- ${presenceConfig.creditText}`, // Use continuous timer if enabled, otherwise use current time startTimestamp: initialTimestamp, // Use site-specific icon if available, otherwise use default web icon largeImageKey: (siteIcon == null ? void 0 : siteIcon.iconKey) || presenceConfig.largeImageKey, largeImageText: displayDomain, // Add browser-specific small image smallImageKey: browserIcon.iconKey, smallImageText: browserIcon.text, // Add buttons buttons: presenceConfig.buttons, instance: false }; const setActivityPromise = new Promise((resolve, reject) => { try { if (!rpc) { return reject(new Error("RPC client is null")); } rpc.setActivity(activityData); resolve(true); } catch (error) { reject(error); } }); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error("setActivity timed out after 5 seconds")); }, 5e3); }); return Promise.race([setActivityPromise, timeoutPromise]).then(() => true).catch((error) => { logger.error("Error setting activity:", { error: error.message, stack: error.stack }); if (rpcConnected) { logger.warn( "Discord connection appears broken, attempting to reconnect" ); rpcConnected = false; rpcReady = false; connectToDiscord(); } return false; }); } catch (error) { logger.error("Error in setActivity:", { error: error.message, stack: error.stack }); if (rpcConnected) { logger.warn("Discord connection appears broken, attempting to reconnect"); rpcConnected = false; rpcReady = false; connectToDiscord(); } return false; } } __name(setActivity, "setActivity"); function clearActivity() { if (!rpcConnected || !rpcReady || !rpc) { logger.warn("Discord RPC not ready, cannot clear activity"); return false; } try { const clearActivityPromise = new Promise((resolve, reject) => { try { if (!rpc) { return reject(new Error("RPC client is null")); } rpc.clearActivity(); initialTimestamp = null; resolve(true); } catch (error) { reject(error); } }); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error("clearActivity timed out after 5 seconds")); }, 5e3); }); return Promise.race([clearActivityPromise, timeoutPromise]).then(() => { logger.info("Successfully cleared Discord presence"); return true; }).catch((error) => { logger.error("Error clearing activity:", { error: error.message, stack: error.stack }); logger.warn( "Discord connection appears broken, attempting to reconnect" ); rpcConnected = false; rpcReady = false; connectToDiscord(); return false; }); } catch (error) { logger.error("Error in clearActivity:", { error: error.message, stack: error.stack }); logger.warn("Discord connection appears broken, attempting to reconnect"); rpcConnected = false; rpcReady = false; connectToDiscord(); return false; } } __name(clearActivity, "clearActivity"); function isConnected() { return rpcConnected && rpcReady; } __name(isConnected, "isConnected"); function resetTimestamp() { initialTimestamp = null; return true; } __name(resetTimestamp, "resetTimestamp"); var discord = { connect: connectToDiscord, setActivity, clearActivity, isConnected, resetTimestamp }; // src/services/websocket.ts init_esm_shims(); init_config(); init_logger(); var clientActivity = /* @__PURE__ */ new Map(); var presenceEnabled = true; function initWebSocketServer(server2) { const wss2 = new WebSocketServer({ server: server2, // Add error handling for the server itself clientTracking: true, perMessageDeflate: true }); wss2.on("error", (error) => { logger.error("WebSocket server error:", { error: error.message, stack: error.stack }); }); const serverConfig = config.getServer(); const checkInterval = setInterval(() => { try { checkInactiveClients(wss2); } catch (error) { logger.error("Error checking inactive clients:", { error }); } }, serverConfig.inactiveCheckInterval); wss2.on("close", () => { logger.info("WebSocket server closing, clearing check interval"); clearInterval(checkInterval); }); wss2.on("connection", (ws, req) => { const clientIp = req.socket.remoteAddress || "unknown"; logger.info(`Browser extension connected from ${clientIp}`); clientActivity.set(ws, Date.now()); ws.on("error", (error) => { logger.error("WebSocket client error:", { error: error.message, clientIp }); }); ws.on("message", async (message) => { try { let data; try { data = JSON.parse(message.toString()); } catch (parseError) { logger.error("Failed to parse WebSocket message:", { error: parseError.message, message: message.toString().substring(0, 100) + (message.toString().length > 100 ? "..." : "") }); return; } clientActivity.set(ws, Date.now()); switch (data.type) { case "presence": { const { title, url, browser } = data; if (!title || !url) { logger.warn("Received presence update with missing data", { data }); return; } let domain = ""; try { domain = new URL(url).hostname; } catch (e) { domain = url; logger.warn(`Failed to parse URL: ${url}`, { error: e }); } const userPrefs = config.getUserPreferences(); const isDisabled = userPrefs.disabledSites.some( (site) => domain.includes(site) ); const isAlwaysEnabled = userPrefs.alwaysEnabledSites.some( (site) => domain.includes(site) ); if (isDisabled && !isAlwaysEnabled) { logger.info(`Skipping presence for disabled site: ${domain}`); return; } if (!presenceEnabled && !isAlwaysEnabled) { return; } if (logger.isVerbose()) { logger.info(`Tab updated: ${title} - ${domain}`); } if (discord.isConnected()) { const resetTimerForAlwaysEnabled = !presenceEnabled && isAlwaysEnabled; if (resetTimerForAlwaysEnabled) { logger.info( "Resetting timer for always-enabled site with presence disabled" ); discord.resetTimestamp(); } try { const result = await discord.setActivity(title, url, browser); if (!result) { logger.warn(`Failed to set activity for ${domain}`); } } catch (error) { logger.error("Error setting Discord activity:", { error, title, domain }); } } else { logger.info("Discord not connected, skipping presence update"); } break; } case "toggle": { presenceEnabled = data.enabled; logger.info(`Presence ${presenceEnabled ? "enabled" : "disabled"}`); if (!presenceEnabled && discord.isConnected()) { try { const result = await discord.clearActivity(); if (result) { logger.info( "Cleared Discord presence due to presence being disabled" ); } else { logger.warn("Failed to clear Discord presence"); } } catch (error) { logger.error("Error clearing Discord activity:", { error }); } } break; } case "updatePreferences": { if (data.preferences) { try { const oldPrefs = config.getUserPreferences(); const continuousTimerChanged = "continuousTimer" in data.preferences && oldPrefs.continuousTimer !== data.preferences.continuousTimer; const updatedPrefs = config.updateUserPreferences( data.preferences ); logger.info("Updated user preferences:", updatedPrefs); if (continuousTimerChanged && !updatedPrefs.continuousTimer) { logger.info("Continuous timer disabled, resetting timestamp"); discord.resetTimestamp(); } broadcastState(wss2); } catch (error) { logger.error("Error updating preferences:", { error, preferences: data.preferences }); } } break; } case "ping": { try { ws.send(JSON.stringify({ type: "pong" })); logger.debug("Received heartbeat from client"); } catch (error) { logger.warn("Error sending pong response:", { error }); } break; } case "clearPresence": { if (discord.isConnected()) { try { const result = await discord.clearActivity(); if (result) { logger.info( "Cleared Discord presence as requested by client" ); } else { logger.warn("Failed to clear Discord presence as requested"); } } catch (error) { logger.error("Error clearing Discord activity:", { error }); } } break; } default: { logger.warn(`Received unknown message type: ${data.type}`); break; } } } catch (error) { logger.error("Error processing WebSocket message:", { error: error.message, stack: error.stack, message: typeof message === "string" ? message.substring(0, 100) + (message.length > 100 ? "..." : "") : "binary data" }); } }); ws.on("close", (code, reason) => { logger.info( `Browser extension disconnected (code: ${code}, reason: ${reason || "none"})` ); clientActivity.delete(ws); if (clientActivity.size === 0 && discord.isConnected()) { try { discord.clearActivity().then((result) => { if (result) { logger.info( "Cleared Discord presence as all clients disconnected" ); } else { logger.warn( "Failed to clear Discord presence after client disconnection" ); } }).catch((error) => { logger.error("Error clearing Discord activity:", { error }); }); } catch (error) { logger.error("Error initiating Discord activity clear:", { error }); } } }); try { sendState(ws); } catch (error) { logger.error("Error sending initial state to client:", { error }); } }); return wss2; } __name(initWebSocketServer, "initWebSocketServer"); function checkInactiveClients(wss2) { const now = Date.now(); const serverConfig = config.getServer(); clientActivity.forEach((lastActivity, ws) => { if (now - lastActivity > serverConfig.activityTimeout) { logger.info("Client inactive for too long, clearing presence"); if (discord.isConnected()) { try { discord.clearActivity().then((result) => { if (result) { logger.info( "Cleared Discord presence due to client inactivity" ); } else { logger.warn( "Failed to clear Discord presence for inactive client" ); } }).catch((error) => { logger.error( "Error clearing Discord activity for inactive client:", { error } ); }); } catch (error) { logger.error( "Error initiating Discord activity clear for inactive client:", { error } ); } } clientActivity.delete(ws); if (ws.readyState === WebSocket.OPEN) { try { ws.close(1e3, "Inactive timeout"); } catch (error) { logger.warn("Error closing inactive WebSocket connection:", { error }); } } } }); } __name(checkInactiveClients, "checkInactiveClients"); function sendState(ws) { if (ws.readyState !== WebSocket.OPEN) { logger.warn("Attempted to send state to non-open WebSocket"); return; } try { const state = { type: "state",