const express = require("express"); const http = require("http"); const https = require("https"); const helmet = require("helmet"); const cors = require("cors"); const fs = require("fs"); const path = require("path"); const config = require("./lib/config"); const { globallimiter } = require("./middleware/ratelimit"); const wsserver = require("./lib/wsserver"); const dnsserver = require("./lib/dnsserver"); const { Client, GatewayIntentBits, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { db } = require("./lib/database"); const authroutes = require("./routes/auth"); const photonroutes = require("./routes/photon"); const titledataroutes = require("./routes/titledata"); const iaproutes = require("./routes/iap"); const mmrroutes = require("./routes/mmr"); const friendsroutes = require("./routes/friends"); const votingroutes = require("./routes/voting"); const questsroutes = require("./routes/quests"); const progressionroutes = require("./routes/progression"); const siquestsroutes = require("./routes/siquests"); const promoroutes = require("./routes/promo"); const kidroutes = require("./routes/kid"); const modioroutes = require("./routes/modio"); const sharedblocksroutes = require("./routes/sharedblocks"); const moderationroutes = require("./routes/moderation"); const mothershiproutes = require("./routes/mothership"); const playfabcloudroutes = require("./routes/playfabcloud"); const adminroutes = require("./routes/admin"); const playfab = require("./lib/playfab"); // ─── Cosmetics autocomplete data ───────────────────────────── let cosmeticsData = null; function loadCosmetics() { const filePath = path.join(__dirname, "CosmeticsExport.txt"); if (!fs.existsSync(filePath)) { cosmeticsData = []; return; } const lines = fs.readFileSync(filePath, "utf8").split("\n").filter(Boolean); const items = []; for (let i = 1; i < lines.length; i++) { const cols = lines[i].split("\t"); if (cols.length >= 5) { const itemId = cols[2].trim(); const displayName = cols[3].trim(); const overrideName = cols[4].trim(); items.push({ item_id: itemId, display_name: displayName, override_display_name: overrideName || displayName, label: (overrideName || displayName) + " (" + itemId + ")", }); } } cosmeticsData = items; } loadCosmetics(); const app = express(); app.set("trust proxy", 1); app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "https://unpkg.com", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'", "data:", "https:"], }, }, })); app.use(cors()); app.use((req, res, next) => { if (req.url.startsWith("//")) { req.url = req.url.replace(/\/{2,}/g, "/"); } next(); }); app.use(express.json({ limit: "5mb" })); app.use(express.text({ limit: "5mb" })); app.use(globallimiter); app.use((req, res, next) => { const rpath = req.url.replace(/\/{2,}/g, "/"); if (rpath.startsWith("/api") || rpath.startsWith("/v1") || rpath.startsWith("/v2") || rpath.startsWith("/CloudScript")) { console.log(`[${req.method}] ${req.hostname}${req.originalUrl}`); try { if (!fs.existsSync(config.event_log_dir)) fs.mkdirSync(config.event_log_dir, { recursive: true }); const date = new Date().toISOString().slice(0, 10); const lf = path.join(config.event_log_dir, `requests-${date}.log`); const logentry = { time: new Date().toISOString(), method: req.method, host: req.hostname, url: req.originalUrl, headers: req.headers, body: req.body, }; fs.appendFileSync(lf, JSON.stringify(logentry) + "\n"); } catch (_) {} } next(); }); app.use((req, res, next) => { const rpath = req.url.replace(/\/{2,}/g, "/"); if (rpath.startsWith("/api") || rpath.startsWith("/v1") || rpath.startsWith("/v2") || rpath.startsWith("/CloudScript")) { const orig = res.json.bind(res); res.json = function (body) { const ts = new Date().toISOString(); try { if (!fs.existsSync(config.event_log_dir)) fs.mkdirSync(config.event_log_dir, { recursive: true }); const date = ts.slice(0, 10); const lf = path.join(config.event_log_dir, `responses-${date}.log`); const logentry = { time: ts, method: req.method, url: req.originalUrl, status: res.statusCode, response: body }; fs.appendFileSync(lf, JSON.stringify(logentry) + "\n"); } catch (_) {} return orig(body); }; const origsend = res.send.bind(res); res.send = function (body) { const ts = new Date().toISOString(); try { if (!fs.existsSync(config.event_log_dir)) fs.mkdirSync(config.event_log_dir, { recursive: true }); const date = ts.slice(0, 10); const lf = path.join(config.event_log_dir, `responses-${date}.log`); const logentry = { time: ts, method: req.method, url: req.originalUrl, status: res.statusCode, response: typeof body === 'string' ? body : String(body) }; fs.appendFileSync(lf, JSON.stringify(logentry) + "\n"); } catch (_) {} return origsend(body); }; } next(); }); app.use((req, res, next) => { if (req.headers["content-type"] && req.headers["content-type"].includes("application/json") && !req.body) { req.body = {}; } next(); }); app.use("/api", authroutes); app.use("/api", photonroutes); app.use("/api", titledataroutes); app.use("/api", iaproutes); app.use("/api", mmrroutes); app.use("/api", friendsroutes); app.use("/api", votingroutes); app.use("/api", questsroutes); app.use("/api", progressionroutes); app.use("/api", siquestsroutes); app.use("/api", promoroutes); app.use("/api", kidroutes); app.use("/api", modioroutes); app.use("/api", sharedblocksroutes); app.use("/api", moderationroutes); app.use("/api", playfabcloudroutes); app.use("/", playfabcloudroutes); app.use("/", mothershiproutes); // simple cookie parser for admin panel app.use((req, res, next) => { req.cookies = {}; const h = req.headers.cookie; if (h) h.split(";").forEach(c => { const p = c.trim().split("="); req.cookies[p[0]] = p[1]; }); next(); }); app.use("/", adminroutes); app.get("/", (req, res) => { res.status(200).send("ok"); }); app.get("/health", (req, res) => { res.status(200).json({ status: "ok" }); }); app.head("/", (req, res) => { res.status(200).end(); }); // ─── Monke Graph Data API ────────────────────────────────── app.get("/api/monke/graph", (req, res) => { try { const hours = parseFloat(req.query.hours) || 24; const since = `-${hours * 60} minutes`; let snapshots = db.prepare( "SELECT online, createdat FROM player_count_snapshots WHERE createdat > datetime('now', ?) ORDER BY createdat ASC" ).all(since); // Filter out noise (±10 from previous value, like the Python bot) const filtered = []; for (let i = 0; i < snapshots.length; i++) { if (i === 0) { filtered.push(snapshots[i]); continue; } const prev = filtered[filtered.length - 1]; if (Math.abs(snapshots[i].online - prev.online) <= 10) { filtered.push(snapshots[i]); } } // Subsample to max 256 points const step = Math.max(1, Math.floor(filtered.length / 256)); const sampled = filtered.filter((_, i) => i % step === 0); res.json(sampled.map(s => ({ timestamp: s.createdat, player_count: s.online, }))); } catch (e) { res.status(500).json({ error: e.message }); } }); // ─── Latest monke count ──────────────────────────────────── app.get("/api/monke/latest", (req, res) => { try { const online = db.prepare("SELECT COUNT(*) as c FROM friendpresence WHERE roomid != ''").get(); const total = db.prepare("SELECT COUNT(*) as c FROM players").get(); res.json({ player_count: online?.c || 0, total_players: total?.c || 0 }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.use((req, res) => { if (req.originalUrl.startsWith("/api") || req.originalUrl.startsWith("/v1") || req.originalUrl.startsWith("/v2")) { console.warn(`[404] ${req.method} ${req.hostname}${req.originalUrl}`); } res.status(404).send("Not found"); }); app.use((err, req, res, next) => { console.error("[uncaught]", err.message); res.status(500).send("Internal error"); }); const server = http.createServer(app); wsserver.attachto(server); server.listen(config.port, config.host, () => { console.log(`[server] listening on ${config.host}:${config.port}`); console.log(`[server] endpoints mounted under /api, /v1, /v2`); dnsserver.start(config.dnsredirectip); }); // ─── Discord Bot Features ──────────────────────────────────── const discordbot = require("./lib/discordbot"); function fuzzyScore(a, b) { a = a.toLowerCase(); b = b.toLowerCase(); let matches = 0; for (let i = 0; i < Math.min(a.length, b.length); i++) { if (a[i] === b[i]) matches++; } return matches / Math.max(a.length, b.length); } function getHistory(hours) { try { const since = `-${hours * 60} minutes`; return db.prepare( "SELECT online, total, createdat FROM player_count_snapshots WHERE createdat > datetime('now', ?) ORDER BY createdat ASC" ).all(since); } catch { return []; } } async function updatePlayerCountChannel(client) { try { const count = wsserver.ccu ? wsserver.ccu() : 0; const total = db.prepare("SELECT COUNT(*) as c FROM players").get(); const totalC = total?.c || 0; // Save snapshot db.prepare("INSERT INTO player_count_snapshots (online, total) VALUES (?,?)").run(count, totalC); // Rename voice channel via REST if (config.discord_count_channel) { try { await discordbot.renameChannel(config.discord_count_channel, `Online: ${count}`); } catch (_) {} } // Update bot status if (client && client.user) { client.user.setActivity(`${count} players`, { type: 3 }).catch(() => {}); } } catch (_) {} } // ─── Room List Auto-Updating Message ───────────────────────── let roomListLastHash = ""; async function updateRoomListMessage(client) { try { const channel = await client.channels.fetch("1513753897372483725").catch(() => null); if (!channel) return; const rows = db.prepare("SELECT fp.playfabid, fp.roomid, fp.zone, fp.region, fp.nickname, p.displayname FROM friendpresence fp LEFT JOIN players p ON p.playfabid = fp.playfabid LEFT JOIN privacystates ps ON ps.playfabid = fp.playfabid WHERE fp.roomid != '' AND (ps.state IS NULL OR ps.state != 'HIDDEN') ORDER BY fp.region, fp.nickname").all(); const hash = JSON.stringify(rows); if (hash === roomListLastHash) return; roomListLastHash = hash; const groups = {}; for (const r of rows) { const key = r.region || "Unknown"; if (!groups[key]) groups[key] = []; groups[key].push("`" + (r.displayname || r.nickname || r.playfabid) + "` in **" + r.roomid + "**" + (r.zone ? " (" + r.zone + ")" : "")); } const desc = rows.length ? Object.entries(groups).map(([region, list]) => "**" + region + "** (" + list.length + ")\n" + list.join("\n")).join("\n\n") : "No one is currently in a room."; const embed = { color: 3447003, title: "👥 Players In Rooms (" + rows.length + ")", description: desc.slice(0, 4000), timestamp: new Date().toISOString() }; const savedId = db.prepare("SELECT datavalue FROM mothershiptitledata WHERE datakey = 'room_list_msg_id'").pluck().get(); if (savedId) { const msg = await channel.messages.fetch(savedId).catch(() => null); if (msg) { await msg.edit({ embeds: [embed] }).catch(() => {}); return; } } const msg = await channel.send({ embeds: [embed] }).catch(() => null); if (msg) db.prepare("INSERT OR REPLACE INTO mothershiptitledata (datakey, datavalue) VALUES ('room_list_msg_id', ?)").run(msg.id); } catch (_) {} } // ─── Audit Links Auto-Updating Message ──────────────────────── let auditLinksLastHash = ""; async function updateAuditLinksMessage(client) { try { const channel = await client.channels.fetch("1514039171843625130").catch(() => null); if (!channel) return; const adminMembers = []; let lastId = ""; while (true) { const r = await discordbot.discordApi(`/guilds/${config.discord_guild_id}/members?limit=1000${lastId ? "&after=" + lastId : ""}`); if (r.status !== 200 || !r.data?.length) break; for (const m of r.data) { if (m.roles && m.roles.includes("1412161751020998666")) adminMembers.push(m); } lastId = r.data[r.data.length - 1].user?.id; if (!lastId || r.data.length < 1000) break; } const hash = JSON.stringify(adminMembers.map(m => m.user.id)); if (hash === auditLinksLastHash) return; auditLinksLastHash = hash; let linked = 0, unlinked = 0; const lines = []; for (const m of adminMembers) { const dl = db.prepare("SELECT playfabid FROM discord_links WHERE discord_id = ?").get(m.user.id); if (dl) { linked++; lines.push("✅ <@" + m.user.id + "> → `" + dl.playfabid + "`"); } else { unlinked++; lines.push("❌ <@" + m.user.id + "> → **NOT LINKED**"); } } const desc = "## 🔗 Admin Link Audit\n**" + linked + " linked / " + unlinked + " unlinked**\n" + lines.join("\n"); const embed = { color: 0x5865F2, description: desc.slice(0, 4000), footer: { text: "🔄 Updates every 5 minutes" } }; const savedId = db.prepare("SELECT datavalue FROM mothershiptitledata WHERE datakey = 'audit_links_msg_id'").pluck().get(); if (savedId) { const msg = await channel.messages.fetch(savedId).catch(() => null); if (msg) { await msg.edit({ embeds: [embed] }).catch(() => {}); return; } } const msg = await channel.send({ embeds: [embed] }).catch(() => null); if (msg) db.prepare("INSERT OR REPLACE INTO mothershiptitledata (datakey, datavalue) VALUES ('audit_links_msg_id', ?)").run(msg.id); } catch (_) {} } async function initBotCommands() { if (!config.discord_bot_token || !config.discord_client_id) return; try { db.prepare("DELETE FROM redeemable_codes WHERE type = 'discord_link' AND end_time < datetime('now')").run(); } catch (_) {} try { const existing = await discordbot.getSlashCommands(); const desired = [ { name: "playercount", desc: "Show current online player count" }, { name: "stats", desc: "Show server statistics" }, { name: "link", desc: "Link your Discord to your in-game account" }, { name: "cancellink", desc: "Cancel your pending link code so you can generate a new one" }, { name: "unlink", desc: "Unlink your Discord from your in-game account" }, { name: "playerinfo", desc: "Look up a player by ID or name", options: [{ type: 3, name: "identifier", description: "PlayFab ID, Oculus ID, Mothership ID, Display Name, or @mention", required: true }] }, { name: "ban", desc: "Ban a player from the game", options: [{ type: 3, name: "identifier", description: "PlayFab ID, Oculus ID, Mothership ID, Display Name, or @mention", required: true }, { type: 3, name: "reason", description: "Ban reason", required: true }] }, { name: "unban", desc: "Unban a player", options: [{ type: 3, name: "identifier", description: "PlayFab ID, Oculus ID, Mothership ID, Display Name, or @mention", required: true }] }, { name: "grant", desc: "Grant item(s) to a player", options: [{ type: 3, name: "identifier", description: "PlayFab ID, Oculus ID, Mothership ID, Display Name, or @mention", required: true }, { type: 3, name: "item", description: "PlayFab item ID", required: true, autocomplete: true }, { type: 3, name: "item2", description: "Second item (optional)", required: false, autocomplete: true }, { type: 3, name: "item3", description: "Third item (optional)", required: false, autocomplete: true }] }, { name: "linkstatus", desc: "Check if a player is linked", options: [{ type: 3, name: "identifier", description: "PlayFab ID, Oculus ID, Mothership ID, Display Name, or @mention", required: true }] }, { name: "findpeople", desc: "List all players currently in rooms" }, { name: "auditlinks", desc: "Check which admin role members have linked their Discord" }, { name: "remove", desc: "Remove item(s) from a player", options: [{ type: 3, name: "identifier", description: "PlayFab ID, Oculus ID, Mothership ID, Display Name, or @mention", required: true }, { type: 3, name: "item", description: "PlayFab item ID", required: true, autocomplete: true }, { type: 3, name: "item2", description: "Second item (optional)", required: false, autocomplete: true }, { type: 3, name: "item3", description: "Third item (optional)", required: false, autocomplete: true }] }, { name: "removeall", desc: "Remove an item from ALL players", options: [{ type: 3, name: "item", description: "PlayFab item ID to remove", required: true, autocomplete: true }] }, { name: "friendadd", desc: "Send a friend request or add directly (owner bypass)", options: [{ type: 3, name: "identifier", description: "Discord @mention (or any ID if you're the bot owner)", required: true }] }, { name: "friendremove", desc: "Remove a friend from your linked in-game account", options: [{ type: 3, name: "identifier", description: "Discord @mention (or any ID if you're the bot owner)", required: true }] }, { name: "friendaccept", desc: "Accept a pending friend request", options: [{ type: 3, name: "identifier", description: "Discord @mention of the person who sent the request", required: true }] }, { name: "frienddeny", desc: "Deny a pending friend request", options: [{ type: 3, name: "identifier", description: "Discord @mention of the person who sent the request", required: true }] }, { name: "privacy", desc: "Set your in-game privacy state", options: [{ type: 3, name: "state", description: "Privacy state", required: true, choices: [{ name: "VISIBLE (0) — Show in rooms", value: "0" }, { name: "PUBLIC_ONLY (1) — Show in public rooms only", value: "1" }, { name: "HIDDEN (2) — Appear offline", value: "2" }] }] }, { name: "warn", desc: "Send a warning notification to a player", options: [{ type: 3, name: "identifier", description: "PlayFab ID, Oculus ID, Mothership ID, Display Name, or @mention", required: true }, { type: 3, name: "reason", description: "Warning reason", required: true, choices: [{ name: "Toxicity", value: "toxicity" }, { name: "Hate Speech", value: "hate speech" }, { name: "Harassment", value: "harassment" }, { name: "Cheating", value: "cheating" }, { name: "Trolling", value: "trolling" }, { name: "Inappropriate Name", value: "inappropriate name" }, { name: "Other", value: "other" }] }, { type: 3, name: "subreason", description: "Additional details (optional)", required: false }] }, { name: "mute", desc: "Mute a player's voice", options: [{ type: 3, name: "identifier", description: "PlayFab ID, Oculus ID, Mothership ID, Display Name, or @mention", required: true }, { type: 4, name: "minutes", description: "Duration in minutes (0 = forever)", required: true }] }, { name: "unmute", desc: "Unmute a player's voice", options: [{ type: 3, name: "identifier", description: "PlayFab ID, Oculus ID, Mothership ID, Display Name, or @mention", required: true }] }, ]; // Delete stale commands (exist on Discord but not in desired) for (const ex of existing) { if (!desired.find(c => c.name === ex.name)) { await discordbot.discordApi(`/applications/${config.discord_client_id}/commands/${ex.id}`, "DELETE"); console.log(`[bot] deleted stale /${ex.name}`); } } // Helper to strip Discord's extra fields for comparison function cmdFingerprint(cmd) { return JSON.stringify({ name: cmd.name, description: cmd.description || cmd.desc, options: (cmd.options || []).map(o => ({ type: o.type, name: o.name, description: o.description, required: !!o.required, autocomplete: !!o.autocomplete })) }); } const existingFingerprints = {}; for (const ex of existing) existingFingerprints[ex.name] = cmdFingerprint(ex); // Only register if fingerprint changed or command doesn't exist for (const cmd of desired) { if (existingFingerprints[cmd.name] === cmdFingerprint(cmd)) { continue; // unchanged, skip } // Delete old version first if it exists const match = existing.find(c => c.name === cmd.name); if (match) { await discordbot.discordApi(`/applications/${config.discord_client_id}/commands/${match.id}`, "DELETE"); } const r = await discordbot.registerSlashCommand({ name: cmd.name, description: cmd.desc, options: cmd.options || [] }); if (r && r.status >= 200 && r.status < 300) { console.log(`[bot] registered /${cmd.name} (${r.status})`); } else { console.warn(`[bot] failed to register /${cmd.name}:`, r ? `${r.status} ${JSON.stringify(r.data)}` : "no response"); } await new Promise(r => setTimeout(r, 500)); // small delay to avoid rate limits } } catch (e) { console.warn("[bot] cmd reg failed:", e.message); } } // ─── Player Resolver ──────────────────────────────────────────── function resolvePlayer(identifier) { const clean = identifier.trim().replace(/<@!?(\d+)>/, "$1"); // Try Discord mention → discord_links if (/^\d{17,20}$/.test(clean)) { const dl = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(clean); if (dl) { const p = db.prepare("SELECT playfabid, oculusid, displayname FROM players WHERE playfabid = ?").get(dl.playfabid); if (p) { p._source = "discord"; return p; } return dl; } } // Try PlayFab ID let row = db.prepare("SELECT playfabid, oculusid, displayname FROM players WHERE playfabid = ?").get(clean); if (row) { row._source = "playfabid"; return row; } // Try Oculus ID row = db.prepare("SELECT playfabid, oculusid, displayname FROM players WHERE oculusid = ?").get(clean); if (row) { row._source = "oculusid"; return row; } // Try Mothership ID → mothershipplayers → players const ms = db.prepare("SELECT userid FROM mothershipplayers WHERE mothershipid = ?").get(clean); if (ms) { row = db.prepare("SELECT playfabid, oculusid, displayname FROM players WHERE oculusid = ?").get(ms.userid); if (row) { row._source = "mothershipid"; return row; } } // Try Display Name (case-insensitive) row = db.prepare("SELECT playfabid, oculusid, displayname FROM players WHERE LOWER(displayname) = LOWER(?)").get(clean); if (row) { row._source = "displayname"; return row; } return null; } // Check if a Discord user has a specific role async function hasDiscordRole(discordId, roleId) { if (!roleId) return false; try { const roles = await discordbot.getMemberRoles(discordId); return roles.includes(roleId); } catch { return false; } } // Discord Interactions endpoint (for slash commands) app.post("/api/discord/interactions", async (req, res) => { try { const body = req.body; if (body.type === 1) return res.json({ type: 1 }); // Autocomplete for item options (grant, remove, removeall) if (body.type === 4) { const focused = body.data?.options?.find(o => o.focused); if (focused && focused.name.startsWith("item") && ["grant", "remove", "removeall"].includes(body.data?.name)) { const value = (focused.value || "").toLowerCase(); const matches = cosmeticsData.filter(c => !value || c.item_id.toLowerCase().includes(value) || c.override_display_name.toLowerCase().includes(value) ).slice(0, 25); return res.json({ type: 8, data: { choices: matches.map(c => ({ name: c.label.slice(0, 100), value: c.item_id })) } }); } return res.json({ type: 8, data: { choices: [] } }); } if (body.type === 2) { const cmd = body.data?.name; const discordId = body.member?.user?.id || body.user?.id; const count = wsserver.ccu ? wsserver.ccu() : 0; const total = db.prepare("SELECT COUNT(*) as c FROM players").get().total || 0; if (cmd === "playercount") { return res.json({ type: 4, data: { embeds: [{ color: 3447003, description: "## 🦍 Player Count\n**↓ Stats ↓**\n```[Online] : " + count + "\n[Total Registered] : " + total + "\n```" }] } }); } if (cmd === "stats") { const maps = db.prepare("SELECT COUNT(*) as c FROM sharedmaps").get(); const bans = db.prepare("SELECT COUNT(*) as c FROM bans").get(); const shifts = db.prepare("SELECT COUNT(*) as c FROM shifts WHERE completed = 1").get(); return res.json({ type: 4, data: { embeds: [{ color: 5763719, description: "## 📊 Server Stats\n**↓ Stats ↓**\n```[Online] : " + count + "\n[Total Players] : " + total + "\n[Shifts Done] : " + (shifts?.c || 0) + "\n[Shared Maps] : " + (maps?.c || 0) + "\n[Bans] : " + (bans?.c || 0) + "\n```" }] } }); } if (cmd === "link") { if (!discordId) return res.json({ type: 4, data: { content: "Could not identify you.", flags: 64 } }); if (body.channel_id !== "1513875402890678324") return res.json({ type: 4, data: { content: "Please use <#1513875402890678324> to link your account.", flags: 64 } }); const linked = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (linked) { const pfName = linked.playfabid ? db.prepare("SELECT displayname FROM players WHERE playfabid = ?").get(linked.playfabid) : null; return res.json({ type: 4, data: { content: "✅ <@" + discordId + "> is already linked to `" + linked.playfabid + "`" + (pfName?.displayname ? " (" + pfName.displayname + ")" : "") + ". Use `/unlink` to unlink." } }); } db.prepare("UPDATE redeemable_codes SET active = 0 WHERE type = 'discord_link' AND end_time < datetime('now')").run(); const existing = db.prepare("SELECT code FROM redeemable_codes WHERE type = 'discord_link' AND discord_id = ? AND active = 1 AND (end_time IS NULL OR end_time > datetime('now'))").get(discordId); if (existing) { return res.json({ type: 4, data: { content: "<@" + discordId + "> already has a pending link code! Type this in the **Redemption Computer** in-game:\n\n`" + existing.code + "`\n\nThis code expires in 48 hours." } }); } const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; let code = ""; for (let i = 0; i < 8; i++) code += chars[Math.floor(Math.random() * chars.length)]; const endTime = new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(); db.prepare( "INSERT INTO redeemable_codes (code, type, discord_id, max_uses, end_time, created_by) VALUES (?, 'discord_link', ?, 1, ?, ?)" ).run(code, discordId, endTime, discordId); return res.json({ type: 4, data: { content: "<@" + discordId + "> | " + member.username + " Type this code in the **Redemption Computer** in-game:\n\n`" + code + "`\n\nThis code expires in **48 hours**." } }); } if (cmd === "unlink") { if (!discordId) return res.json({ type: 4, data: { content: "Could not identify you." } }); const linked = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (!linked) return res.json({ type: 4, data: { content: "You don't have a linked account." } }); db.prepare("DELETE FROM discord_links WHERE discord_id = ?").run(discordId); try { discordbot.sendChannelMessage("1513408264149274754", null, { color: 0x5865F2, description: "**Unlink Account**\n<@" + discordId + "> (`" + discordId + "`)\nUnlinked from: `" + linked.playfabid + "`\n", timestamp: new Date().toISOString() }); } catch (_) {} return res.json({ type: 4, data: { content: "Your Discord has been unlinked from your in-game account." } }); } if (cmd === "cancellink") { if (body.channel_id !== "1513875402890678324") return res.json({ type: 4, data: { content: "Please use <#1513875402890678324> to cancel a link code.", flags: 64 } }); const updated = db.prepare("UPDATE redeemable_codes SET active = 0 WHERE type = 'discord_link' AND discord_id = ? AND active = 1").run(discordId); if (updated.changes > 0) return res.json({ type: 4, data: { content: "Cancelled your pending link code. You can now use `/link` to get a new one." } }); return res.json({ type: 4, data: { content: "You don't have any pending link code." } }); } // ─── Friend Commands (require link, only in link channel) ── const BOT_OWNER_ID = "898859607391354891"; const friendIdentifier = body.data?.options?.find(o => o.name === "identifier")?.value || ""; function parseMention(raw) { const m = raw.trim().match(/^<@!?(\d{17,20})>$/); return m ? m[1] : null; } const isOwner = discordId === BOT_OWNER_ID; if (cmd === "friendadd") { if (body.channel_id !== "1481428446935777333") return res.json({ type: 4, data: { content: "Please use <#1481428446935777333> for friend commands.", flags: 64 } }); const linked = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (!linked || !linked.playfabid) return res.json({ type: 4, data: { content: "You must link your Discord using `/link` first." } }); if (isOwner) { const target = resolvePlayer(friendIdentifier); if (!target) return res.json({ type: 4, data: { content: "Player not found." } }); if (target.playfabid === linked.playfabid) return res.json({ type: 4, data: { content: "You can't friend yourself." } }); const existing = db.prepare("SELECT 1 FROM friendlinks WHERE playerid = ? AND friendid = ?").get(linked.playfabid, target.playfabid); if (existing) return res.json({ type: 4, data: { content: "They're already your friend." } }); db.prepare("INSERT INTO friendlinks (playerid, friendid) VALUES (?, ?)").run(linked.playfabid, target.playfabid); return res.json({ type: 4, data: { content: "✅ <@" + discordId + "> added **" + (target.displayname || target.playfabid) + "** as a friend!" } }); } const targetDiscordId = parseMention(friendIdentifier); if (!targetDiscordId) return res.json({ type: 4, data: { content: "Please mention the person you want to add (@username)." } }); const targetLink = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(targetDiscordId); if (!targetLink || !targetLink.playfabid) return res.json({ type: 4, data: { content: "That user hasn't linked their Discord yet." } }); if (targetLink.playfabid === linked.playfabid) return res.json({ type: 4, data: { content: "You can't friend yourself." } }); const existing = db.prepare("SELECT 1 FROM friendlinks WHERE playerid = ? AND friendid = ?").get(linked.playfabid, targetLink.playfabid); if (existing) return res.json({ type: 4, data: { content: "They're already your friend." } }); const pending = db.prepare("SELECT 1 FROM friend_requests WHERE from_playfabid = ? AND to_discord_id = ? AND status = 'pending'").get(linked.playfabid, targetDiscordId); if (pending) return res.json({ type: 4, data: { content: "You already have a pending request to that user." } }); db.prepare("INSERT OR REPLACE INTO friend_requests (from_playfabid, to_discord_id, status) VALUES (?, ?, 'pending')").run(linked.playfabid, targetDiscordId); return res.json({ type: 4, data: { content: "📨 <@" + discordId + "> sent a friend request to <@" + targetDiscordId + ">! They can accept with `/friendaccept`." } }); } if (cmd === "friendremove") { if (body.channel_id !== "1481428446935777333") return res.json({ type: 4, data: { content: "Please use <#1481428446935777333> for friend commands.", flags: 64 } }); const linked = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (!linked || !linked.playfabid) return res.json({ type: 4, data: { content: "You must link your Discord using `/link` first." } }); const target = isOwner ? resolvePlayer(friendIdentifier) : null; const targetPfId = target ? target.playfabid : null; if (!targetPfId) { const tdId = parseMention(friendIdentifier); if (!tdId) return res.json({ type: 4, data: { content: "Please mention the person you want to remove (@username)." } }); const tl = db.prepare("SELECT playfabid FROM discord_links WHERE discord_id = ?").get(tdId); if (!tl) return res.json({ type: 4, data: { content: "That user hasn't linked their Discord." } }); const existing = db.prepare("SELECT 1 FROM friendlinks WHERE playerid = ? AND friendid = ?").get(linked.playfabid, tl.playfabid); if (!existing) return res.json({ type: 4, data: { content: "They're not in your friend list." } }); db.prepare("DELETE FROM friendlinks WHERE playerid = ? AND friendid = ?").run(linked.playfabid, tl.playfabid); return res.json({ type: 4, data: { content: "✅ <@" + discordId + "> removed <@" + tdId + "> from friends!" } }); } if (target.playfabid === linked.playfabid) return res.json({ type: 4, data: { content: "You can't unfriend yourself." } }); const existing = db.prepare("SELECT 1 FROM friendlinks WHERE playerid = ? AND friendid = ?").get(linked.playfabid, target.playfabid); if (!existing) return res.json({ type: 4, data: { content: "They're not in your friend list." } }); db.prepare("DELETE FROM friendlinks WHERE playerid = ? AND friendid = ?").run(linked.playfabid, target.playfabid); return res.json({ type: 4, data: { content: "✅ <@" + discordId + "> removed **" + (target.displayname || target.playfabid) + "** from friends!" } }); } if (cmd === "friendaccept" || cmd === "frienddeny") { if (body.channel_id !== "1481428446935777333") return res.json({ type: 4, data: { content: "Please use <#1481428446935777333> for friend commands.", flags: 64 } }); const myLink = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (!myLink || !myLink.playfabid) return res.json({ type: 4, data: { content: "You must link your Discord using `/link` first." } }); const fromDiscordId = parseMention(friendIdentifier); if (!fromDiscordId) return res.json({ type: 4, data: { content: "Please @mention the person who sent the request." } }); const fromLink = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(fromDiscordId); if (!fromLink || !fromLink.playfabid) return res.json({ type: 4, data: { content: "That user hasn't linked their Discord." } }); const request = db.prepare("SELECT * FROM friend_requests WHERE from_playfabid = ? AND to_discord_id = ? AND status = 'pending'").get(fromLink.playfabid, discordId); if (!request) return res.json({ type: 4, data: { content: "No pending request from that user." } }); if (cmd === "friendaccept") { db.prepare("UPDATE friend_requests SET status = 'accepted' WHERE id = ?").run(request.id); db.prepare("INSERT OR IGNORE INTO friendlinks (playerid, friendid) VALUES (?, ?)").run(fromLink.playfabid, myLink.playfabid); db.prepare("INSERT OR IGNORE INTO friendlinks (playerid, friendid) VALUES (?, ?)").run(myLink.playfabid, fromLink.playfabid); return res.json({ type: 4, data: { content: "✅ <@" + discordId + "> accepted <@" + fromDiscordId + ">'s friend request! You are now friends." } }); } else { db.prepare("UPDATE friend_requests SET status = 'denied' WHERE id = ?").run(request.id); return res.json({ type: 4, data: { content: "❌ <@" + discordId + "> denied <@" + fromDiscordId + ">'s friend request." } }); } } // ─── Admin Commands (role-gated, require linked account) ─── const ADMIN_ROLE = "1412161751020998666"; const GRANT_ROLE = "1513426812359671808"; const AUDIT_CHANNEL = "1513408264149274754"; const identifier = body.data?.options?.find(o => o.name === "identifier")?.value || ""; const token = body.token; // interaction token for follow-up // Build user info for audit log const member = body.member?.user || body.user || {}; const auditUser = { id: discordId, name: member.username || "Unknown", avatar: member.avatar || "" }; const avatarUrl = auditUser.avatar ? "https://cdn.discordapp.com/avatars/" + auditUser.id + "/" + auditUser.avatar + "." + (auditUser.avatar.startsWith("a_") ? "gif" : "png") : ""; function sendAuditLog(action, target, detail) { try { discordbot.sendChannelMessage(AUDIT_CHANNEL, null, { color: 0x5865F2, author: { name: auditUser.name, icon_url: avatarUrl }, description: "**" + action + "**\nBy: <@" + auditUser.id + "> (`" + auditUser.id + "`)\nTarget: `" + target + "`\n" + detail, timestamp: new Date().toISOString(), }).catch(() => {}); } catch (_) {} } function isLinked() { return !!db.prepare("SELECT 1 FROM discord_links WHERE discord_id = ?").get(discordId); } if (cmd === "findpeople") { const rows = db.prepare("SELECT fp.playfabid, fp.roomid, fp.zone, fp.region, fp.nickname, p.displayname FROM friendpresence fp LEFT JOIN players p ON p.playfabid = fp.playfabid LEFT JOIN privacystates ps ON ps.playfabid = fp.playfabid WHERE fp.roomid != '' AND (ps.state IS NULL OR ps.state != 'HIDDEN') ORDER BY fp.region, fp.nickname").all(); if (!rows.length) return res.json({ type: 4, data: { content: "No one is currently in a room." } }); const groups = {}; for (const r of rows) { const key = r.region || "Unknown"; if (!groups[key]) groups[key] = []; groups[key].push("`" + (r.displayname || r.nickname || r.playfabid) + "` in **" + r.roomid + "**" + (r.zone ? " (" + r.zone + ")" : "")); } const desc = Object.entries(groups).map(([region, list]) => "**" + region + "** (" + list.length + ")\n" + list.join("\n")).join("\n\n"); return res.json({ type: 4, data: { content: "## 👥 Players Online\n" + desc.slice(0, 1900) } }); } // Fast commands (no PlayFab API calls) if (cmd === "auditlinks") { if (!await hasDiscordRole(discordId, ADMIN_ROLE)) return res.json({ type: 4, data: { content: "You don't have permission." } }); // Paginate through all guild members const adminMembers = []; let lastId = ""; while (true) { const r = await discordbot.discordApi(`/guilds/${config.discord_guild_id}/members?limit=1000${lastId ? "&after=" + lastId : ""}`); if (r.status !== 200 || !r.data?.length) break; for (const m of r.data) { if (m.roles && m.roles.includes(ADMIN_ROLE)) adminMembers.push(m); } lastId = r.data[r.data.length - 1].user?.id; if (!lastId || r.data.length < 1000) break; } if (!adminMembers.length) return res.json({ type: 4, data: { content: "No members with admin role found." } }); let linked = 0, unlinked = 0; const lines = []; for (const m of adminMembers) { const dl = db.prepare("SELECT playfabid FROM discord_links WHERE discord_id = ?").get(m.user.id); if (dl) { linked++; lines.push("✅ <@" + m.user.id + "> → `" + dl.playfabid + "`"); } else { unlinked++; lines.push("❌ <@" + m.user.id + "> → **NOT LINKED**"); } } const desc = "## 🔗 Admin Link Audit\n**" + linked + " linked / " + unlinked + " unlinked**\n" + lines.join("\n"); return res.json({ type: 4, data: { content: desc.slice(0, 1900) } }); } if (cmd === "linkstatus") { if (!isLinked()) return res.json({ type: 4, data: { content: "You must link your Discord using `/link` first." } }); if (!await hasDiscordRole(discordId, ADMIN_ROLE)) return res.json({ type: 4, data: { content: "You don't have permission." } }); const player = resolvePlayer(identifier); if (!player) return res.json({ type: 4, data: { content: "Player not found." } }); const dl = db.prepare("SELECT * FROM discord_links WHERE playfabid = ? OR mothershipid = (SELECT mothershipid FROM mothershipplayers WHERE userid = ?)").get(player.playfabid, player.oculusid || ""); if (!dl) return res.json({ type: 4, data: { content: "This player is not linked to any Discord account." } }); return res.json({ type: 4, data: { content: "## 🔗 Link Status\n**↓ Details ↓**\n```[PlayFab ID] : " + player.playfabid + "\n[Discord] : " + "<@" + dl.discord_id + ">\n[Discord ID] : " + dl.discord_id + "\n[Linked At] : " + (dl.linked_at || "N/A") + "\n```" } }); } if (cmd === "playerinfo") { if (!isLinked()) return res.json({ type: 4, data: { content: "You must link your Discord using `/link` first." } }); if (!await hasDiscordRole(discordId, ADMIN_ROLE)) return res.json({ type: 4, data: { content: "You don't have permission." } }); const player = resolvePlayer(identifier); if (!player) return res.json({ type: 4, data: { content: "Player not found. Try PlayFab ID, Oculus ID, Mothership ID, Display Name, or Discord @mention." } }); const dl = db.prepare("SELECT * FROM discord_links WHERE playfabid = ? OR mothershipid = (SELECT mothershipid FROM mothershipplayers WHERE userid = ?)").get(player.playfabid, player.oculusid || ""); const bans = db.prepare("SELECT * FROM bans WHERE playfabid = ?").all(player.playfabid); const ms = player.oculusid ? db.prepare("SELECT mothershipid FROM mothershipplayers WHERE userid = ?").get(player.oculusid) : null; const inv = await playfab.getuserinventory(player.playfabid).catch(() => null); const invCount = inv?.data?.data?.Inventory?.length || 0; const desc = "## 📋 Player Info\n**↓ IDs ↓**\n```[PlayFab ID] : " + (player.playfabid || "N/A") + "\n[Oculus ID] : " + (player.oculusid || "N/A") + "\n[Mothership ID] : " + (ms?.mothershipid || "N/A") + "\n[Display Name] : " + (player.displayname || "N/A") + "\n[Discord] : " + (dl ? "<@" + dl.discord_id + ">" : "Not linked") + "\n```\n**↓ Account ↓**\n```[Bans] : " + (bans?.length || 0) + "\n[Inventory Items] : " + invCount + "\n```"; return res.json({ type: 4, data: { embeds: [{ color: 3447003, description: desc }] } }); } // Slow commands — defer first, then run async const slowCmds = { ban: "ban", unban: "unban", grant: "grant", remove: "remove", removeall: "removeall" }; if (slowCmds[cmd]) { res.json({ type: 5 }); // Defer immediately // All follow-up runs asynchronously (async () => { if (!isLinked()) { await discordbot.editInteractionResponse(token, { content: "You must link your Discord using `/link` first." }).catch(() => {}); return; } const requiredRole = (cmd === "grant" || cmd === "remove") ? GRANT_ROLE : ADMIN_ROLE; if (!await hasDiscordRole(discordId, requiredRole)) { await discordbot.editInteractionResponse(token, { content: "You don't have permission." }).catch(() => {}); return; } const player = cmd === "removeall" ? null : resolvePlayer(identifier); if (cmd !== "removeall" && !player) { await discordbot.editInteractionResponse(token, { content: "Player not found." }).catch(() => {}); return; } if (cmd === "ban") { const reason = body.data?.options?.find(o => o.name === "reason")?.value || "No reason provided"; const banResult = await playfab.banusers([player.playfabid], "[Discord Ban] " + reason, 0).catch(e => ({ error: e.message })); if (banResult?.error) { await discordbot.editInteractionResponse(token, { content: "Ban failed: " + banResult.error }).catch(() => {}); } else { sendAuditLog("Ban", player.playfabid + " (" + (player.displayname || "?") + ")", "Reason: " + reason); await discordbot.editInteractionResponse(token, { content: "✅ **Banned** `" + player.playfabid + "` (" + (player.displayname || "?") + ")\nReason: " + reason }).catch(() => {}); } } else if (cmd === "unban") { const ubResult = await playfab.revokeallbans(player.playfabid).catch(e => ({ error: e.message })); if (ubResult?.error) { await discordbot.editInteractionResponse(token, { content: "Unban failed: " + ubResult.error }).catch(() => {}); } else { sendAuditLog("Unban", player.playfabid + " (" + (player.displayname || "?") + ")", ""); await discordbot.editInteractionResponse(token, { content: "✅ **Unbanned** `" + player.playfabid + "` (" + (player.displayname || "?") + ")" }).catch(() => {}); } } else if (cmd === "grant") { const items = []; for (const name of ["item", "item2", "item3"]) { const v = (body.data?.options?.find(o => o.name === name)?.value || "").trim(); if (v) items.push(v); } if (!items.length) { await discordbot.editInteractionResponse(token, { content: "No item specified." }).catch(() => {}); return; } const grantResult = await playfab.grantitemstouser(player.playfabid, items).catch(e => ({ error: e.message })); if (grantResult?.error) { await discordbot.editInteractionResponse(token, { content: "Grant failed: " + grantResult.error }).catch(() => {}); } else { sendAuditLog("Grant Item", player.playfabid + " (" + (player.displayname || "?") + ")", "Items: `" + items.join("`, `") + "`"); await discordbot.editInteractionResponse(token, { content: "✅ **Granted** " + items.length + " item(s) to `" + player.playfabid + "` (" + (player.displayname || "?") + ")\n`" + items.join("`, `") + "`" }).catch(() => {}); } } else if (cmd === "remove") { const items = []; for (const name of ["item", "item2", "item3"]) { const v = (body.data?.options?.find(o => o.name === name)?.value || "").trim(); if (v) items.push(v); } if (!items.length) { await discordbot.editInteractionResponse(token, { content: "No item specified." }).catch(() => {}); return; } const inv = await playfab.getuserinventory(player.playfabid).catch(() => null); if (!inv?.data?.data?.Inventory) { await discordbot.editInteractionResponse(token, { content: "Failed to get inventory or player has no items." }).catch(() => {}); return; } const instances = []; for (const itemId of items) { const found = inv.data.data.Inventory.filter(i => i.ItemId === itemId).map(i => i.ItemInstanceId); instances.push(...found); } if (!instances.length) { await discordbot.editInteractionResponse(token, { content: "Player does not have any of the specified item(s)." }).catch(() => {}); return; } const revokeResult = await playfab.revokeinventoryitems(player.playfabid, instances).catch(e => ({ error: e.message })); if (revokeResult?.error) { await discordbot.editInteractionResponse(token, { content: "Remove failed: " + revokeResult.error }).catch(() => {}); } else { sendAuditLog("Remove Item", player.playfabid + " (" + (player.displayname || "?") + ")", "Items: `" + items.join("`, `") + "` (" + instances.length + " instances)"); await discordbot.editInteractionResponse(token, { content: "✅ **Removed** " + instances.length + " instance(s) from `" + player.playfabid + "` (" + (player.displayname || "?") + ")\n`" + items.join("`, `") + "`" }).catch(() => {}); } } else if (cmd === "removeall") { const items = []; for (const name of ["item", "item2", "item3"]) { const v = (body.data?.options?.find(o => o.name === name)?.value || "").trim(); if (v) items.push(v); } if (!items.length) { await discordbot.editInteractionResponse(token, { content: "No item specified." }).catch(() => {}); return; } await discordbot.editInteractionResponse(token, { content: "⏳ Removing `" + items.join("`, `") + "` from all players... This may take a while." }).catch(() => {}); const allPlayers = db.prepare("SELECT playfabid FROM players").all(); let totalRevoked = 0, totalChecked = 0, totalPlayers = allPlayers.length; for (const p of allPlayers) { totalChecked++; if (totalChecked % 50 === 0) { const progress = ((totalChecked / totalPlayers) * 100).toFixed(1); try { await discordbot.editInteractionResponse(token, { content: "⏳ Removing `" + items.join("`, `") + "` from all players... " + totalChecked + "/" + totalPlayers + " (" + progress + "%) — revoked: " + totalRevoked }); } catch (_) {} } const inv = await playfab.getuserinventory(p.playfabid).catch(() => null); if (!inv?.data?.data?.Inventory) continue; const instances = []; for (const itemId of items) { const found = inv.data.data.Inventory.filter(i => i.ItemId === itemId).map(i => i.ItemInstanceId); instances.push(...found); } if (!instances.length) continue; await playfab.revokeinventoryitems(p.playfabid, instances).catch(() => {}); totalRevoked += instances.length; } sendAuditLog("Remove All", "All players", "Items: `" + items.join("`, `") + "` (" + totalRevoked + " instances)"); await discordbot.editInteractionResponse(token, { content: "✅ **Done.** Removed " + totalRevoked + " instance(s) of `" + items.join("`, `") + "` across " + totalPlayers + " players." }).catch(() => {}); } })(); return; } return res.json({ type: 4, data: { content: "Unknown command." } }); } res.json({ type: 1 }); } catch (e) { console.error("[interactions] error:", e.message); res.status(500).json({ error: e.message }); } }); // ─── WebSocket Bot (monke commands, status, channel rename) ── function startDiscordGateway() { if (!config.discord_bot_token) return; const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], }); client.once("ready", () => { console.log(`[bot] Logged in as ${client.user.tag}`); updatePlayerCountChannel(client); updateRoomListMessage(client); updateAuditLinksMessage(client); initBotCommands(); setInterval(() => { updatePlayerCountChannel(client); try { db.prepare("DELETE FROM redeemable_codes WHERE type = 'discord_link' AND end_time < datetime('now')").run(); } catch (_) {} }, 60000); setInterval(() => updateRoomListMessage(client), 10000); setInterval(() => updateAuditLinksMessage(client), 300000); }); client.on("messageCreate", async (message) => { if (message.author.bot) return; const content = message.content.toLowerCase().trim(); const count = wsserver.ccu ? wsserver.ccu() : 0; const total = db.prepare("SELECT COUNT(*) as c FROM players").get(); const totalC = total?.c || 0; const monkeCommands = { howmanymonke: 24, shortmonke: 6, shortshortmonke: 0.5, longmonke: 96, longlongmonke: 168, longlonglongmonke: 336 }; let matchedMonke = null; for (const [cmd, hours] of Object.entries(monkeCommands)) { if (fuzzyScore(cmd, content.replace(/[^a-z]/g, "")) > 0.65 || content.includes(cmd)) { matchedMonke = { cmd, hours }; break; } } if (matchedMonke) { if (config.discord_monke_channel && message.channel.id !== config.discord_monke_channel) return; const { cmd, hours } = matchedMonke; const labels = { shortshortmonke: "30 min", shortmonke: "6h", howmanymonke: "24h", longmonke: "4 days", longlongmonke: "7 days", longlonglongmonke: "14 days" }; const label = labels[cmd] || `${hours}h`; await message.channel.sendTyping(); const snapshots = getHistory(hours); if (!snapshots.length) return message.reply(`No monke data for the last ${label} :(`); // Generate graph via Python/matplotlib (exact match to Python bot) const { spawn } = require("child_process"); const pythonScript = path.join(__dirname, "monke_graph.py"); const py = spawn("python", [pythonScript], { stdio: ["pipe", "pipe", "ignore"] }); const chunks = []; py.stdout.on("data", c => chunks.push(c)); const exitCode = await new Promise(resolve => { py.on("close", resolve); py.stdin.write(JSON.stringify({ snapshots: snapshots.map(s => ({ createdat: s.createdat, online: s.online })) })); py.stdin.end(); }); if (exitCode !== 0 || chunks.length === 0) { return message.reply(`Player Count: **${count.toLocaleString()}**\n${label} (graph generation failed)`); } const buf = Buffer.concat(chunks); return message.reply({ content: `Player Count: **${count.toLocaleString()}**\n${label}`, files: [{ attachment: buf, name: "graph.png" }], }); } if (content === "!playercount" || content === "!pc") { return message.reply(`🦍 **${count}** players online / **${totalC}** total registered`); } if (content === "!stats") { try { const maps = db.prepare("SELECT COUNT(*) as c FROM sharedmaps").get(); const bans = db.prepare("SELECT COUNT(*) as c FROM bans").get(); const shifts = db.prepare("SELECT COUNT(*) as c FROM shifts WHERE completed = 1").get(); return message.reply({ embeds: [new EmbedBuilder().setColor(0x5865F2).setDescription("## 📊 Server Stats\n**↓ Stats ↓**\n```[Online] : " + count + "\n[Total Players] : " + totalC + "\n[Shifts Done] : " + (shifts?.c || 0) + "\n[Shared Maps] : " + (maps?.c || 0) + "\n[Bans] : " + (bans?.c || 0) + "\n```")] }); } catch { return message.reply("Error fetching stats."); } } }); // ─── Gateway Interaction Handler (slash commands + autocomplete) ── client.on("interactionCreate", async (interaction) => { try { // Autocomplete if (interaction.isAutocomplete()) { const focused = interaction.options.getFocused(true); if (focused.name.startsWith("item") && ["grant", "remove", "removeall"].includes(interaction.commandName)) { const value = interaction.options.getFocused().toLowerCase(); const matches = cosmeticsData.filter(c => !value || c.item_id.toLowerCase().includes(value) || c.override_display_name.toLowerCase().includes(value) ).slice(0, 25); await interaction.respond(matches.map(c => ({ name: c.label.slice(0, 100), value: c.item_id }))); } return; } // Button interactions (friend accept/deny) if (interaction.isButton()) { const [action, fromPfId] = interaction.customId.split(":"); if ((action === "friend_accept" || action === "friend_deny") && fromPfId) { await interaction.deferReply({ ephemeral: true }); const myLink = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(interaction.user.id); if (!myLink || !myLink.playfabid) return interaction.editReply({ content: "You must link your Discord using `/link` first." }); const fromLink = db.prepare("SELECT * FROM discord_links WHERE playfabid = ?").get(fromPfId); if (!fromLink) return interaction.editReply({ content: "That user no longer has a linked account." }); const request = db.prepare("SELECT * FROM friend_requests WHERE from_playfabid = ? AND to_discord_id = ? AND status = 'pending'").get(fromPfId, interaction.user.id); if (!request) return interaction.editReply({ content: "This request is no longer pending." }); if (action === "friend_accept") { db.prepare("UPDATE friend_requests SET status = 'accepted' WHERE id = ?").run(request.id); db.prepare("INSERT OR IGNORE INTO friendlinks (playerid, friendid) VALUES (?, ?)").run(fromPfId, myLink.playfabid); db.prepare("INSERT OR IGNORE INTO friendlinks (playerid, friendid) VALUES (?, ?)").run(myLink.playfabid, fromPfId); await interaction.editReply({ content: "✅ You accepted the friend request!" }); } else { db.prepare("UPDATE friend_requests SET status = 'denied' WHERE id = ?").run(request.id); await interaction.editReply({ content: "❌ You denied the friend request." }); } // Update the original message to show it was handled try { await interaction.message.edit({ components: [] }); } catch (_) {} } return; } // Slash commands if (!interaction.isChatInputCommand()) return; const cmd = interaction.commandName; const discordId = interaction.user.id; const count = wsserver.ccu ? wsserver.ccu() : 0; const total = db.prepare("SELECT COUNT(*) as c FROM players").get().total || 0; // Public commands if (cmd === "playercount") { return interaction.reply({ embeds: [{ color: 3447003, description: "## 🦍 Player Count\n**↓ Stats ↓**\n```[Online] : " + count + "\n[Total Registered] : " + total + "\n```" }] }); } if (cmd === "stats") { const maps = db.prepare("SELECT COUNT(*) as c FROM sharedmaps").get(); const bans = db.prepare("SELECT COUNT(*) as c FROM bans").get(); const shifts = db.prepare("SELECT COUNT(*) as c FROM shifts WHERE completed = 1").get(); return interaction.reply({ embeds: [{ color: 5763719, description: "## 📊 Server Stats\n**↓ Stats ↓**\n```[Online] : " + count + "\n[Total Players] : " + total + "\n[Shifts Done] : " + (shifts?.c || 0) + "\n[Shared Maps] : " + (maps?.c || 0) + "\n[Bans] : " + (bans?.c || 0) + "\n```" }] }); } if (cmd === "link") { if (interaction.channelId !== "1513875402890678324") return interaction.reply({ content: "Please use <#1513875402890678324> to link your account.", ephemeral: true }); const linked = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (linked) { const pfName = linked.playfabid ? db.prepare("SELECT displayname FROM players WHERE playfabid = ?").get(linked.playfabid) : null; return interaction.reply({ content: "✅ <@" + discordId + "> is already linked to `" + linked.playfabid + "`" + (pfName?.displayname ? " (" + pfName.displayname + ")" : "") + ". Use `/unlink` to unlink." }); } db.prepare("UPDATE redeemable_codes SET active = 0 WHERE type = 'discord_link' AND end_time < datetime('now')").run(); const existing = db.prepare("SELECT code FROM redeemable_codes WHERE type = 'discord_link' AND discord_id = ? AND active = 1 AND (end_time IS NULL OR end_time > datetime('now'))").get(discordId); if (existing) { return interaction.reply({ content: "<@" + discordId + "> already has a pending link code! Type this in the **Redemption Computer** in-game:\n\n`" + existing.code + "`\n\nThis code expires in 48 hours." }); } const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; let code = ""; for (let i = 0; i < 8; i++) code += chars[Math.floor(Math.random() * chars.length)]; const endTime = new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(); db.prepare("INSERT INTO redeemable_codes (code, type, discord_id, max_uses, end_time, created_by) VALUES (?, 'discord_link', ?, 1, ?, ?)").run(code, discordId, endTime, discordId); return interaction.reply({ content: "<@" + discordId + "> | " + interaction.user.username + " Type this code in the **Redemption Computer** in-game:\n\n`" + code + "`\n\nThis code expires in **48 hours**." }); } if (cmd === "unlink") { const linked = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (!linked) return interaction.reply({ content: "You don't have a linked account." }); db.prepare("DELETE FROM discord_links WHERE discord_id = ?").run(discordId); try { discordbot.sendChannelMessage("1513408264149274754", null, { color: 0x5865F2, description: "**Unlink Account**\n<@" + discordId + "> (`" + discordId + "`)\nUnlinked from: `" + linked.playfabid + "`\n", timestamp: new Date().toISOString() }); } catch (_) {} return interaction.reply({ content: "Your Discord has been unlinked from your in-game account." }); } if (cmd === "cancellink") { if (interaction.channelId !== "1513875402890678324") return interaction.reply({ content: "Please use <#1513875402890678324> to cancel a link code.", ephemeral: true }); const updated = db.prepare("UPDATE redeemable_codes SET active = 0 WHERE type = 'discord_link' AND discord_id = ? AND active = 1").run(discordId); if (updated.changes > 0) return interaction.reply({ content: "Cancelled your pending link code. You can now use `/link` to get a new one." }); return interaction.reply({ content: "You don't have any pending link code." }); } // ─── Friend Commands (require link, only in link channel) ── const BOT_OWNER_ID = "898859607391354891"; const friendIdentifier = interaction.options.getString("identifier") || ""; function parseMention(raw) { const m = raw.trim().match(/^<@!?(\d{17,20})>$/); return m ? m[1] : null; } const isOwner = discordId === BOT_OWNER_ID; if (cmd === "friendadd") { if (interaction.channelId !== "1481428446935777333") return interaction.reply({ content: "Please use <#1481428446935777333> for friend commands.", ephemeral: true }); const linked = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (!linked || !linked.playfabid) return interaction.reply({ content: "You must link your Discord using `/link` first." }); if (isOwner) { await interaction.deferReply().catch(() => {}); const target = resolvePlayer(friendIdentifier); if (!target) return interaction.editReply({ content: "Player not found." }); if (target.playfabid === linked.playfabid) return interaction.editReply({ content: "You can't friend yourself." }); const existing = db.prepare("SELECT 1 FROM friendlinks WHERE playerid = ? AND friendid = ?").get(linked.playfabid, target.playfabid); if (existing) return interaction.editReply({ content: "They're already your friend." }); db.prepare("INSERT INTO friendlinks (playerid, friendid) VALUES (?, ?)").run(linked.playfabid, target.playfabid); return interaction.editReply({ content: "✅ <@" + discordId + "> added **" + (target.displayname || target.playfabid) + "** as a friend!" }); } const targetDiscordId = parseMention(friendIdentifier); if (!targetDiscordId) return interaction.reply({ content: "Please mention the person you want to add (@username)." }); const targetLink = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(targetDiscordId); if (!targetLink || !targetLink.playfabid) return interaction.reply({ content: "That user hasn't linked their Discord yet." }); if (targetLink.playfabid === linked.playfabid) return interaction.reply({ content: "You can't friend yourself." }); const existing = db.prepare("SELECT 1 FROM friendlinks WHERE playerid = ? AND friendid = ?").get(linked.playfabid, targetLink.playfabid); if (existing) return interaction.reply({ content: "They're already your friend." }); const pending = db.prepare("SELECT 1 FROM friend_requests WHERE from_playfabid = ? AND to_discord_id = ? AND status = 'pending'").get(linked.playfabid, targetDiscordId); if (pending) return interaction.reply({ content: "You already have a pending request to that user." }); await interaction.deferReply().catch(() => {}); db.prepare("INSERT OR REPLACE INTO friend_requests (from_playfabid, to_discord_id, status) VALUES (?, ?, 'pending')").run(linked.playfabid, targetDiscordId); const row = new ActionRowBuilder().addComponents( new ButtonBuilder().setCustomId("friend_accept:" + linked.playfabid).setLabel("Accept").setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId("friend_deny:" + linked.playfabid).setLabel("Deny").setStyle(ButtonStyle.Danger), ); try { const targetUser = await client.users.fetch(targetDiscordId); await targetUser.send({ content: "📨 <@" + discordId + "> sent you a friend request!", components: [row] }); return interaction.editReply({ content: "📨 Friend request sent to <@" + targetDiscordId + ">! Check your DMs." }); } catch { return interaction.editReply({ content: "📨 <@" + discordId + "> sent a friend request to <@" + targetDiscordId + ">! (DMs disabled, use buttons below or `/friendaccept` / `/frienddeny`)", components: [row] }); } } if (cmd === "friendremove") { if (interaction.channelId !== "1481428446935777333") return interaction.reply({ content: "Please use <#1481428446935777333> for friend commands.", ephemeral: true }); const linked = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (!linked || !linked.playfabid) return interaction.reply({ content: "You must link your Discord using `/link` first." }); const target = isOwner ? resolvePlayer(friendIdentifier) : null; const targetPfId = target ? target.playfabid : null; if (!targetPfId) { const tdId = parseMention(friendIdentifier); if (!tdId) return interaction.reply({ content: "Please mention the person you want to remove (@username)." }); const tl = db.prepare("SELECT playfabid FROM discord_links WHERE discord_id = ?").get(tdId); if (!tl) return interaction.reply({ content: "That user hasn't linked their Discord." }); const existing = db.prepare("SELECT 1 FROM friendlinks WHERE playerid = ? AND friendid = ?").get(linked.playfabid, tl.playfabid); if (!existing) return interaction.reply({ content: "They're not in your friend list." }); db.prepare("DELETE FROM friendlinks WHERE playerid = ? AND friendid = ?").run(linked.playfabid, tl.playfabid); return interaction.reply({ content: "✅ <@" + discordId + "> removed <@" + tdId + "> from friends!" }); } if (target.playfabid === linked.playfabid) return interaction.reply({ content: "You can't unfriend yourself." }); const existing = db.prepare("SELECT 1 FROM friendlinks WHERE playerid = ? AND friendid = ?").get(linked.playfabid, target.playfabid); if (!existing) return interaction.reply({ content: "They're not in your friend list." }); db.prepare("DELETE FROM friendlinks WHERE playerid = ? AND friendid = ?").run(linked.playfabid, target.playfabid); return interaction.reply({ content: "✅ <@" + discordId + "> removed **" + (target.displayname || target.playfabid) + "** from friends!" }); } if (cmd === "friendaccept" || cmd === "frienddeny") { if (interaction.channelId !== "1481428446935777333") return interaction.reply({ content: "Please use <#1481428446935777333> for friend commands.", ephemeral: true }); const myLink = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (!myLink || !myLink.playfabid) return interaction.reply({ content: "You must link your Discord using `/link` first." }); const fromDiscordId = parseMention(friendIdentifier); if (!fromDiscordId) return interaction.reply({ content: "Please @mention the person who sent the request." }); const fromLink = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(fromDiscordId); if (!fromLink || !fromLink.playfabid) return interaction.reply({ content: "That user hasn't linked their Discord." }); const request = db.prepare("SELECT * FROM friend_requests WHERE from_playfabid = ? AND to_discord_id = ? AND status = 'pending'").get(fromLink.playfabid, discordId); if (!request) return interaction.reply({ content: "No pending request from that user." }); if (cmd === "friendaccept") { db.prepare("UPDATE friend_requests SET status = 'accepted' WHERE id = ?").run(request.id); db.prepare("INSERT OR IGNORE INTO friendlinks (playerid, friendid) VALUES (?, ?)").run(fromLink.playfabid, myLink.playfabid); db.prepare("INSERT OR IGNORE INTO friendlinks (playerid, friendid) VALUES (?, ?)").run(myLink.playfabid, fromLink.playfabid); return interaction.reply({ content: "✅ <@" + discordId + "> accepted <@" + fromDiscordId + ">'s friend request! You are now friends." }); } else { db.prepare("UPDATE friend_requests SET status = 'denied' WHERE id = ?").run(request.id); return interaction.reply({ content: "❌ <@" + discordId + "> denied <@" + fromDiscordId + ">'s friend request." }); } } if (cmd === "privacy") { if (interaction.channelId !== "1481428446935777333") return interaction.reply({ content: "Please use <#1481428446935777333> to set your privacy state.", ephemeral: true }); const linked = db.prepare("SELECT * FROM discord_links WHERE discord_id = ?").get(discordId); if (!linked || !linked.playfabid) return interaction.reply({ content: "You must link your Discord using `/link` first." }); const stateVal = interaction.options.getString("state"); const labels = { "0": "VISIBLE", "1": "PUBLIC_ONLY", "2": "HIDDEN" }; db.prepare("INSERT OR REPLACE INTO privacystates (playfabid, state) VALUES (?, ?)").run(linked.playfabid, labels[stateVal]); return interaction.reply({ content: "✅ Your privacy state is now **" + labels[stateVal] + "** (" + stateVal + ")." }); } if (cmd === "findpeople") { const bypassPrivacy = interaction.channelId === "1488422467058794516"; let sql; if (bypassPrivacy) { sql = "SELECT fp.playfabid, fp.roomid, fp.zone, fp.region, fp.nickname, p.displayname FROM friendpresence fp LEFT JOIN players p ON p.playfabid = fp.playfabid WHERE fp.roomid != '' ORDER BY fp.region, fp.nickname"; } else { sql = "SELECT fp.playfabid, fp.roomid, fp.zone, fp.region, fp.nickname, p.displayname FROM friendpresence fp LEFT JOIN players p ON p.playfabid = fp.playfabid LEFT JOIN privacystates ps ON ps.playfabid = fp.playfabid WHERE fp.roomid != '' AND (ps.state IS NULL OR ps.state != 'HIDDEN') ORDER BY fp.region, fp.nickname"; } const rows = db.prepare(sql).all(); if (!rows.length) return interaction.reply({ content: "No one is currently in a room." }); const groups = {}; for (const r of rows) { const key = r.region || "Unknown"; if (!groups[key]) groups[key] = []; groups[key].push("`" + (r.displayname || r.nickname || r.playfabid) + "` in **" + r.roomid + "**" + (r.zone ? " (" + r.zone + ")" : "")); } const desc = Object.entries(groups).map(([region, list]) => "**" + region + "** (" + list.length + ")\n" + list.join("\n")).join("\n\n"); return interaction.reply({ content: "## 👥 Players Online\n" + desc.slice(0, 1900) }); } if (cmd === "auditlinks") { const roles = interaction.member?.roles?.cache?.map(r => r.id) || []; if (!roles.includes("1412161751020998666")) return interaction.reply({ content: "You don't have permission." }); await interaction.deferReply(); const adminMembers = []; let lastId = ""; while (true) { const r = await discordbot.discordApi(`/guilds/${config.discord_guild_id}/members?limit=1000${lastId ? "&after=" + lastId : ""}`); if (r.status !== 200 || !r.data?.length) break; for (const m of r.data) { if (m.roles && m.roles.includes("1412161751020998666")) adminMembers.push(m); } lastId = r.data[r.data.length - 1].user?.id; if (!lastId || r.data.length < 1000) break; } if (!adminMembers.length) return interaction.editReply({ content: "No members with admin role found." }); let linked = 0, unlinked = 0; const lines = []; for (const m of adminMembers) { const dl = db.prepare("SELECT playfabid FROM discord_links WHERE discord_id = ?").get(m.user.id); if (dl) { linked++; lines.push("✅ <@" + m.user.id + "> → `" + dl.playfabid + "`"); } else { unlinked++; lines.push("❌ <@" + m.user.id + "> → **NOT LINKED**"); } } const desc = "## 🔗 Admin Link Audit\n**" + linked + " linked / " + unlinked + " unlinked**\n" + lines.join("\n"); return interaction.editReply({ content: desc.slice(0, 1900) }); } // ─── Admin Commands ─── const ADMIN_ROLE = "1412161751020998666"; const GRANT_ROLE = "1513426812359671808"; const AUDIT_CHANNEL = "1513408264149274754"; const identifier = interaction.options.getString("identifier") || ""; const member = interaction.member; function sendAuditLog(action, target, detail) { try { const avatarUrl = member.user.avatar ? "https://cdn.discordapp.com/avatars/" + member.user.id + "/" + member.user.avatar + "." + (member.user.avatar.startsWith("a_") ? "gif" : "png") : ""; discordbot.sendChannelMessage(AUDIT_CHANNEL, null, { color: 0x5865F2, author: { name: member.user.username, icon_url: avatarUrl }, description: "**" + action + "**\nBy: <@" + member.user.id + "> (`" + member.user.id + "`)\nTarget: `" + target + "`\n" + detail, timestamp: new Date().toISOString(), }).catch(() => {}); } catch (_) {} } function isLinked() { return !!db.prepare("SELECT 1 FROM discord_links WHERE discord_id = ?").get(discordId); } // Fast: linkstatus if (cmd === "linkstatus") { if (!isLinked()) return interaction.reply({ content: "You must link your Discord using `/link` first." }); const roles = member.roles?.cache?.map(r => r.id) || []; if (!roles.includes(ADMIN_ROLE)) return interaction.reply({ content: "You don't have permission." }); const player = resolvePlayer(identifier); if (!player) return interaction.reply({ content: "Player not found." }); const dl = db.prepare("SELECT * FROM discord_links WHERE playfabid = ? OR mothershipid = (SELECT mothershipid FROM mothershipplayers WHERE userid = ?)").get(player.playfabid, player.oculusid || ""); if (!dl) return interaction.reply({ content: "This player is not linked to any Discord account." }); return interaction.reply({ content: "## 🔗 Link Status\n**↓ Details ↓**\n```[PlayFab ID] : " + player.playfabid + "\n[Discord] : " + "<@" + dl.discord_id + ">\n[Discord ID] : " + dl.discord_id + "\n[Linked At] : " + (dl.linked_at || "N/A") + "\n```" }); } // Fast: playerinfo if (cmd === "playerinfo") { if (!isLinked()) return interaction.reply({ content: "You must link your Discord using `/link` first." }); const roles = member.roles?.cache?.map(r => r.id) || []; if (!roles.includes(ADMIN_ROLE)) return interaction.reply({ content: "You don't have permission." }); const player = resolvePlayer(identifier); if (!player) return interaction.reply({ content: "Player not found. Try PlayFab ID, Oculus ID, Mothership ID, Display Name, or Discord @mention." }); const dl = db.prepare("SELECT * FROM discord_links WHERE playfabid = ? OR mothershipid = (SELECT mothershipid FROM mothershipplayers WHERE userid = ?)").get(player.playfabid, player.oculusid || ""); const bans = db.prepare("SELECT * FROM bans WHERE playfabid = ?").all(player.playfabid); const ms = player.oculusid ? db.prepare("SELECT mothershipid FROM mothershipplayers WHERE userid = ?").get(player.oculusid) : null; const inv = await playfab.getuserinventory(player.playfabid).catch(() => null); const invCount = inv?.data?.data?.Inventory?.length || 0; const desc = "## 📋 Player Info\n**↓ IDs ↓**\n```[PlayFab ID] : " + (player.playfabid || "N/A") + "\n[Oculus ID] : " + (player.oculusid || "N/A") + "\n[Mothership ID] : " + (ms?.mothershipid || "N/A") + "\n[Display Name] : " + (player.displayname || "N/A") + "\n[Discord] : " + (dl ? "<@" + dl.discord_id + ">" : "Not linked") + "\n```\n**↓ Account ↓**\n```[Bans] : " + (bans?.length || 0) + "\n[Inventory Items] : " + invCount + "\n```"; return interaction.reply({ embeds: [{ color: 3447003, description: desc }] }); } // Slow commands — ban, unban, grant, remove, removeall if (cmd === "ban" || cmd === "unban" || cmd === "grant" || cmd === "remove" || cmd === "removeall") { const requiredRole = (cmd === "grant" || cmd === "remove") ? GRANT_ROLE : ADMIN_ROLE; const roles = member.roles?.cache?.map(r => r.id) || []; if (!isLinked()) return interaction.reply({ content: "You must link your Discord using `/link` first." }); if (!roles.includes(requiredRole)) return interaction.reply({ content: "You don't have permission." }); const player = cmd === "removeall" ? null : resolvePlayer(identifier); if (cmd !== "removeall" && !player) return interaction.reply({ content: "Player not found." }); await interaction.deferReply(); if (cmd === "ban") { const reason = interaction.options.getString("reason") || "No reason provided"; const banResult = await playfab.banusers([player.playfabid], "[Discord Ban] " + reason, 0).catch(e => ({ error: e.message })); if (banResult?.error) { await interaction.editReply({ content: "Ban failed: " + banResult.error }); } else { sendAuditLog("Ban", player.playfabid + " (" + (player.displayname || "?") + ")", "Reason: " + reason); await interaction.editReply({ content: "✅ **Banned** `" + player.playfabid + "` (" + (player.displayname || "?") + ")\nReason: " + reason }); } } else if (cmd === "unban") { const ubResult = await playfab.revokeallbans(player.playfabid).catch(e => ({ error: e.message })); if (ubResult?.error) { await interaction.editReply({ content: "Unban failed: " + ubResult.error }); } else { sendAuditLog("Unban", player.playfabid + " (" + (player.displayname || "?") + ")", ""); await interaction.editReply({ content: "✅ **Unbanned** `" + player.playfabid + "` (" + (player.displayname || "?") + ")" }); } } else if (cmd === "grant") { const items = []; for (const name of ["item", "item2", "item3"]) { const v = (interaction.options.getString(name) || "").trim(); if (v) items.push(v); } if (!items.length) { await interaction.editReply({ content: "No item specified." }); return; } const grantResult = await playfab.grantitemstouser(player.playfabid, items).catch(e => ({ error: e.message })); if (grantResult?.error) { await interaction.editReply({ content: "Grant failed: " + grantResult.error }); } else { sendAuditLog("Grant Item", player.playfabid + " (" + (player.displayname || "?") + ")", "Items: `" + items.join("`, `") + "`"); await interaction.editReply({ content: "✅ **Granted** " + items.length + " item(s) to `" + player.playfabid + "` (" + (player.displayname || "?") + ")\n`" + items.join("`, `") + "`" }); } } else if (cmd === "remove") { const items = []; for (const name of ["item", "item2", "item3"]) { const v = (interaction.options.getString(name) || "").trim(); if (v) items.push(v); } if (!items.length) { await interaction.editReply({ content: "No item specified." }); return; } const inv = await playfab.getuserinventory(player.playfabid).catch(() => null); if (!inv?.data?.data?.Inventory) { await interaction.editReply({ content: "Failed to get inventory or player has no items." }); return; } const instances = []; for (const itemId of items) { const found = inv.data.data.Inventory.filter(i => i.ItemId === itemId).map(i => i.ItemInstanceId); instances.push(...found); } if (!instances.length) { await interaction.editReply({ content: "Player does not have any of the specified item(s)." }); return; } const revokeResult = await playfab.revokeinventoryitems(player.playfabid, instances).catch(e => ({ error: e.message })); if (revokeResult?.error) { await interaction.editReply({ content: "Remove failed: " + revokeResult.error }); } else { sendAuditLog("Remove Item", player.playfabid + " (" + (player.displayname || "?") + ")", "Items: `" + items.join("`, `") + "` (" + instances.length + " instances)"); await interaction.editReply({ content: "✅ **Removed** " + instances.length + " instance(s) from `" + player.playfabid + "` (" + (player.displayname || "?") + ")\n`" + items.join("`, `") + "`" }); } } else if (cmd === "removeall") { const items = []; for (const name of ["item", "item2", "item3"]) { const v = (interaction.options.getString(name) || "").trim(); if (v) items.push(v); } if (!items.length) { await interaction.editReply({ content: "No item specified." }); return; } await interaction.editReply({ content: "⏳ Removing `" + items.join("`, `") + "` from all players... This may take a while." }); const allPlayers = db.prepare("SELECT playfabid FROM players").all(); let totalRevoked = 0, totalChecked = 0, totalPlayers = allPlayers.length; for (const p of allPlayers) { totalChecked++; if (totalChecked % 50 === 0) { const progress = ((totalChecked / totalPlayers) * 100).toFixed(1); try { await interaction.editReply({ content: "⏳ Removing `" + items.join("`, `") + "` from all players... " + totalChecked + "/" + totalPlayers + " (" + progress + "%) — revoked: " + totalRevoked }); } catch (_) {} } const inv = await playfab.getuserinventory(p.playfabid).catch(() => null); if (!inv?.data?.data?.Inventory) continue; const instances = []; for (const itemId of items) { const found = inv.data.data.Inventory.filter(i => i.ItemId === itemId).map(i => i.ItemInstanceId); instances.push(...found); } if (!instances.length) continue; await playfab.revokeinventoryitems(p.playfabid, instances).catch(() => {}); totalRevoked += instances.length; } sendAuditLog("Remove All", "All players", "Items: `" + items.join("`, `") + "` (" + totalRevoked + " instances)"); await interaction.editReply({ content: "✅ **Done.** Removed " + totalRevoked + " instance(s) of `" + items.join("`, `") + "` across " + totalPlayers + " players." }); } } // ─── Moderation Notification Commands (warn / mute / unmute) ── if (cmd === "warn" || cmd === "mute" || cmd === "unmute") { const roles = member.roles?.cache?.map(r => r.id) || []; if (!roles.includes(ADMIN_ROLE)) return interaction.reply({ content: "You don't have permission." }); const player = resolvePlayer(identifier); if (!player) return interaction.reply({ content: "Player not found." }); const ms = db.prepare("SELECT mothershipid FROM mothershipplayers WHERE userid = ?").get(player.oculusid || ""); const mothershipId = ms?.mothershipid || ""; if (!mothershipId) return interaction.reply({ content: "Player has no Mothership account." }); let title, body; if (cmd === "warn") { const reason = interaction.options.getString("reason") || "other"; const subreason = interaction.options.getString("subreason") || ""; title = "Warning"; body = reason + (subreason ? "|" + subreason : ""); } else if (cmd === "mute") { const minutes = interaction.options.getInteger("minutes") || 0; title = "Mute"; body = "voice|" + minutes + "|" + (minutes > 0 ? minutes * 60 : ""); } else { title = "Unmute"; body = ""; } const sent = wsserver.sendNotification(mothershipId, title, body); if (sent) { sendAuditLog(title, player.playfabid + " (" + (player.displayname || "?") + ")", body ? "Details: " + body : ""); await interaction.reply({ content: "✅ **" + title + "** sent to `" + player.playfabid + "` (" + (player.displayname || "?") + ")" + (body ? "\nBody: " + body : "") }); } else { await interaction.reply({ content: "⚠️ **" + title + "** — player is not connected via WebSocket." }); } } } catch (e) { console.error("[bot] interaction error:", e.message); try { if (interaction.deferred) await interaction.editReply({ content: "Error: " + e.message }); else if (!interaction.replied) await interaction.reply({ content: "Error: " + e.message }); } catch (_) {} } }); client.login(config.discord_bot_token).catch(e => console.error("[bot] Login failed:", e.message)); } startDiscordGateway();