webpresence
Version:
Discord Rich Presence for websites - Show your browsing activity in Discord
1,460 lines (1,451 loc) • 67.2 kB
JavaScript
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/api.ts
init_esm_shims();
// 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 {
con