webpresence
Version:
Discord Rich Presence for websites - Show your browsing activity in Discord
1,500 lines (1,488 loc) • 52.7 kB
JavaScript
#!/usr/bin/env node
import 'path';
import 'url';
import chalk from 'chalk';
import express, { Router } from 'express';
import http from 'http';
import cors from 'cors';
import { execSync } from 'child_process';
import net from 'net';
import * as DiscordRPC from 'discord-rpc';
import { WebSocketServer, WebSocket } from 'ws';
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
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 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/daemon.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 {
const state = {
type: "state",
enabled: presenceEnabled,
connected: discord.isConnected(),
preferences: config.getUserPreferences()
};
ws.send(JSON.stringify(state));
} catch (error) {
logger.error("Error sending state to client:", { error });
}
}
__name(sendState, "sendState");
function broadcastState(wss2) {
try {
let sentCount = 0;
let errorCount = 0;
wss2.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
try {
sendState(client);
sentCount++;
} catch (error) {
logger.error("Error broadcasting state to client:", { error });
errorCount++;
}
}
});
logger.info(
`Broadcast state to ${sentCount} clients (${errorCount} errors)`
);
} catch (error) {
logger.error("Error during state broadcast:", { error });
}
}
__name(broadcastState, "broadcastState");
function togglePresence(enabled) {
presenceEnabled = enabled !== void 0 ? enabled : !presenceEnabled;
logger.info(
`Presence toggled to ${presenceEnabled ? "enabled" : "disabled"}`
);
return presenceEnabled;
}
__name(togglePresence, "togglePresence");
function getPresenceState() {
try {
return {
enabled: presenceEnabled,
connected: discord.isConnected(),
preferences: config.getUserPreferences()
};
} catch (error) {
logger.error("Error getting presence state:", { error });
return {
enabled: presenceEnabled,
connected: false,
preferences: {
prefixText: "Viewing",
disabledSites: [],
alwaysEnabledSites: [],
continuousTimer: true
}
};
}
}
__name(getPresenceState, "getPresenceState");
// src/index.ts
init_config();
// src/routes/index.ts
init_esm_shims();
init_config();
function initRoutes(wss2) {
const router = Router();
router.get("/status", (_, res) => {
const state = getPresenceState();
res.json({
running: true,
discordConnected: state.connected,
presenceEnabled: state.enabled,
preferences: state.preferences
});
});
router.post("/toggle", (req, res) => {
const { enabled } = req.body;
const newState = togglePresence(enabled);
wss2.clients.forEach((client) => {
client.send(
JSON.stringify({
type: "state",
...getPresenceState()
})
);
});
res.json({ enabled: newState });
});
router.post("/preferences", (req, res) => {
const { preferences } = req.body;
if (preferences) {
const updatedPrefs = config.updateUserPreferences(preferences);
wss2.clients.forEach((client) => {
client.send(
JSON.stringify({
type: "state",
...getPresenceState()
})
);
});
res.json({ preferences: updatedPrefs });
} else {
res.status(400).json({ error: "Invalid preferences" });
}
});
router.post("/stop", async (req, res) => {
wss2.clients.forEach((client) => {
client.send(
JSON.stringify({
type: "shutdown",
message: "Server is shutting down"
})
);
});
res.json({ success: true, message: "Server is shutting down" });
setTimeout(async () => {
try {
await stopServer();
} catch (error) {
console.error("Error stopping server:", error);
}
}, 500);
});
return router;
}
__name(initRoutes, "initRoutes");
// src/index.ts
init_logger();
var server = null;
var wss = null;
var isRunning = false;
async function checkPortInUse(port) {
return new Promise((resolve) => {
const socket = new net.Socket();
let timeoutId;
timeoutId = setTimeout(() => {
socket.destroy();
resolve(false);
}, 500);
socket.once("connect", () => {
clearTimeout(timeoutId);
socket.destroy();
resolve(true);
});
socket.once("error", (err) => {
clearTimeout(timeoutId);
if (err.code === "ECONNREFUSED") {
resolve(false);
} else {
resolve(true);
}
});
socket.connect(port, "127.0.0.1");
});
}
__name(checkPortInUse, "checkPortInUse");
function setupErrorHandlers() {
process.on("uncaughtException", (error) => {
logger.error(`Uncaught Exception: ${error.message}`, {
error: error.stack
});
});
process.on("unhandledRejection", (reason, promise) => {
logger.error(
`Unhandled Promise Rejection at: ${promise}
Reason: ${reason}`
);
});
process.on("SIGINT", async () => {
logger.info("Received SIGINT signal, shutting down gracefully...");
await stopServer();
process.exit(0);
});
process.on("SIGTERM", async () => {
logger.info("Received SIGTERM signal, shutting down gracefully...");
await stopServer();
process.exit(0);
});
}
__name(setupErrorHandlers, "setupErrorHandlers");
async function startServer(options) {
if (isRunning || server !== null) {
logger.info("Server is already running");
return { success: true, port: config.getServer().port };
}
if (options == null ? void 0 : options.port) {
config.getServer().port = options.port;
}
const PORT = config.getServer().port;
const portInUse = await checkPortInUse(PORT);
if (portInUse) {
logger.error(`Port ${PORT} is already in use. Cannot start server.`);
return { success: false, port: PORT, error: "PORT_IN_USE" };
}
setupErrorHandlers();
try {
const app = express();
app.use(cors());
app.use(express.json());
server = http.createServer(app);
wss = initWebSocketServer(server);
app.use("/api", initRoutes(wss));
if (!(options == null ? void 0 : options.skipDiscord)) {
logger.info("\u{1F680} Initializing Discord RPC connection...");
await discord.connect();
}
const PORT2 = config.getServer().port;
return new Promise((resolve) => {
if (!server) {
resolve({ success: false, port: PORT2 });
return;
}
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
logger.error(
`Port ${PORT2} is already in use. Server may already be running.`
);
isRunning = true;
resolve({ success: false, port: PORT2 });
} else {
logger.error(`Server error: ${err.message}`, { error: err.stack });
resolve({ success: false, port: PORT2 });
}
});
try {
server.listen(PORT2, () => {
logger.success(`Server running on http://localhost:${PORT2}`);
logger.success(`WebSocket server running on ws://localhost:${PORT2}`);
isRunning = true;
resolve({ success: true, port: PORT2 });
});
} catch (listenError) {
logger.error(`Failed to start server: ${listenError.message}`, {
error: listenError.stack
});
resolve({ success: false, port: PORT2 });
}
});
} catch (error) {
logger.error("Failed to start server:", {
error: error.message,
stack: error.stack
});
return { success: false, port: config.getServer().port };
}
}
__name(startServer, "startServer");
async function stopServer() {
if (!isRunning && !server) {
logger.info("Server is not running");
return { success: true };
}
try {
return new Promise((resolve) => {
if (!server) {
isRunning = false;
wss = null;
resolve({ success: true });
return;
}
const timeout = setTimeout(() => {
logger.warn("Server close operation timed out, forcing cleanup");
isRunning = false;
server = null;
wss = null;
resolve({ success: true });
}, 5e3);
server.close(async () => {
clearTimeout(timeout);
logger.info("Server stopped");
try {
if (discord.isConnected()) {
await discord.clearActivity();
}
} catch (discordError) {
logger.warn(
`Error clearing Discord activity: ${discordError.message}`
);
}
isRunning = false;
server = null;
wss = null;
resolve({ success: true });
});
});
} catch (error) {
logger.error("Failed to stop server:", {
error: error.message,
stack: error.stack
});
isRunning = false;
server = null;
wss = null;
return { success: false };
}
}
__name(stopServer, "stopServer");
function isServerRunning(options) {
const opts = {
checkPort: true,
checkApi: false,
...options
};
if (isRunning) {
return true;
}
if (!opts.checkPort && !opts.checkApi) {
return false;
}
if (opts.checkPort) {
const serverPort = config.getServer().port;
try {
const net2 = __require("net");
const socket = new net2.Socket();
let isConnected2 = false;
socket.connect(serverPort, "127.0.0.1");
socket.setTimeout(500);
socket.on("connect", () => {
isConnected2 = true;
socket.destroy();
});
try {
execSync("sleep 0.2", { stdio: "ignore" });
} catch (e) {
if (process.platform === "win32") {
try {
execSync(`ping -n 1 -w 200 127.0.0.1 >nul`, { stdio: "ignore" });
} catch (pingError) {
}
}
}
if (isConnected2) {
return true;
}
} catch (socketError) {
}
try {
const netstatCommand = process.platform === "win32" ? `netstat -ano | findstr :${serverPort}` : `netstat -tuln | grep :${serverPort}`;
const output = execSync(netstatCommand, {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"]
// Suppress stderr to avoid showing command not found errors
});
if (output && output.trim().length > 0) {
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 && lsofOutput.trim()) {
return true;
}
} catch (lsofError) {