webpresence
Version:
Discord Rich Presence for websites - Show your browsing activity in Discord
494 lines (490 loc) • 15.8 kB
JavaScript
import { spawn, execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import os from 'os';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
var __defProp = Object.defineProperty;
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');
});
function formatTimestamp() {
return (/* @__PURE__ */ new Date()).toISOString();
}
__name(formatTimestamp, "formatTimestamp");
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;
}
__name(formatLogMessage, "formatLogMessage");
var isRunningAsCLI = process.argv.length > 1 && process.argv[1].includes("cli");
var verbose = !isRunningAsCLI;
var 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/config/index.ts
var 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"
}
}
}
};
var 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)
}
};
var 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")
};
// src/utils/daemon.ts
var DAEMON_DIR = path.join(os.homedir(), ".webpresence");
var PID_FILE = path.join(DAEMON_DIR, "webpresence.pid");
var LOG_FILE = path.join(DAEMON_DIR, "webpresence.log");
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;
}
}
}
__name(ensureDaemonDir, "ensureDaemonDir");
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 = path.dirname(__filename2);
const cliPath = path.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 = path.dirname(__filename22);
const daemonPath = path.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}`
};
}
}
__name(startDaemon, "startDaemon");
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}`
};
}
}
__name(stopDaemon, "stopDaemon");
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 net = __require("net");
const tester = net.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;
}
}
__name(isDaemonRunning, "isDaemonRunning");
function getDaemonPid() {
if (!isDaemonRunning()) {
return null;
}
try {
return parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
} catch (error) {
return null;
}
}
__name(getDaemonPid, "getDaemonPid");
function getDaemonLogPath() {
return LOG_FILE;
}
__name(getDaemonLogPath, "getDaemonLogPath");
export { getDaemonLogPath, getDaemonPid, isDaemonRunning, startDaemon, stopDaemon };
//# sourceMappingURL=daemon.js.map
//# sourceMappingURL=daemon.js.map