require('dotenv').config({ path: __dirname + '/.env' }); const express = require('express'); const cors = require('cors'); const app = express(); const port = process.env.PORT || 3001; const host = process.env.HOST || '0.0.0.0'; const corsOrigin = process.env.CORS_ORIGIN || null; // e.g. 'http://example.com' or '*' or 'http://127.0.0.1:3000' // Convenience base URLs (override if you want fully-qualified URLs) const BACKEND_BASE = process.env.BACKEND_BASE || `http://${host}:${port}`; const FRONTEND_BASE = process.env.FRONTEND_BASE || 'http://localhost:3000'; if (corsOrigin) { app.use(cors({ origin: corsOrigin })); } else { app.use(cors()); } app.use(express.json()); const axios = require('axios'); const crypto = require('crypto'); // Twitch API helpers (uses TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET from env) let _twitchToken = null; let _twitchTokenExpiry = 0; async function getTwitchAppToken() { if (_twitchToken && Date.now() < _twitchTokenExpiry - 60000) return _twitchToken; const id = process.env.TWITCH_CLIENT_ID; const secret = process.env.TWITCH_CLIENT_SECRET; if (!id || !secret) return null; try { const resp = await axios.post('https://id.twitch.tv/oauth2/token', null, { params: { client_id: id, client_secret: secret, grant_type: 'client_credentials' }, }); _twitchToken = resp.data.access_token; const expiresIn = Number(resp.data.expires_in) || 3600; _twitchTokenExpiry = Date.now() + expiresIn * 1000; return _twitchToken; } catch (e) { console.error('Failed to fetch Twitch app token:', e && e.response && e.response.data ? e.response.data : e.message || e); return null; } } async function getTwitchStreamsForUsers(usernames = []) { try { if (!usernames || usernames.length === 0) return []; const token = await getTwitchAppToken(); const id = process.env.TWITCH_CLIENT_ID; const params = new URLSearchParams(); for (const u of usernames) params.append('user_login', u.toLowerCase()); const headers = {}; if (id) headers['Client-Id'] = id; if (token) headers['Authorization'] = `Bearer ${token}`; const url = `https://api.twitch.tv/helix/streams?${params.toString()}`; const resp = await axios.get(url, { headers }); // resp.data.data is an array of live streams const live = resp.data && resp.data.data ? resp.data.data : []; // Map by user_login // Fetch user info (bio, profile image) so we can include it in the response const uniqueLogins = Array.from(new Set(usernames.map(u => (u || '').toLowerCase()))).filter(Boolean); const users = {}; if (uniqueLogins.length > 0) { const userParams = new URLSearchParams(); for (const u of uniqueLogins) userParams.append('login', u); try { const usersUrl = `https://api.twitch.tv/helix/users?${userParams.toString()}`; const usersResp = await axios.get(usersUrl, { headers }); const usersData = usersResp.data && usersResp.data.data ? usersResp.data.data : []; for (const u of usersData) { users[(u.login || '').toLowerCase()] = { id: u.id, login: u.login, display_name: u.display_name, description: u.description, profile_image_url: u.profile_image_url, }; } } catch (e) { // ignore user fetch errors } } // collect game ids from live streams to resolve game names const gameIds = Array.from(new Set((live || []).map(s => s.game_id).filter(Boolean))); const games = {}; if (gameIds.length > 0) { try { const gamesParams = new URLSearchParams(); for (const idv of gameIds) gamesParams.append('id', idv); const gamesUrl = `https://api.twitch.tv/helix/games?${gamesParams.toString()}`; const gamesResp = await axios.get(gamesUrl, { headers }); const gamesData = gamesResp.data && gamesResp.data.data ? gamesResp.data.data : []; for (const g of gamesData) { games[g.id] = g.name; } } catch (e) { // ignore } } // Build response for every requested username (include user info even when offline) const map = {}; for (const u of uniqueLogins) { map[u] = { is_live: false, user_login: u, user_name: (users[u] && users[u].display_name) || u, title: null, viewer_count: 0, started_at: null, url: `https://www.twitch.tv/${u}`, thumbnail_url: null, description: (users[u] && users[u].description) || null, profile_image_url: (users[u] && users[u].profile_image_url) || null, game_name: null, }; } for (const s of live) { const login = (s.user_login || '').toLowerCase(); // twitch returns thumbnail_url like ".../{width}x{height}.jpg" — replace with a sensible size const rawThumb = s.thumbnail_url || null; const thumb = rawThumb ? rawThumb.replace('{width}', '1280').replace('{height}', '720') : null; map[login] = { is_live: true, user_login: s.user_login, user_name: s.user_name, title: s.title, viewer_count: s.viewer_count, started_at: s.started_at, url: `https://www.twitch.tv/${s.user_login}`, thumbnail_url: thumb, description: (users[login] && users[login].description) || null, profile_image_url: (users[login] && users[login].profile_image_url) || null, game_name: (s.game_id && games[s.game_id]) || null, }; } return Object.values(map); } catch (e) { console.error('Error fetching twitch streams:', e && e.response && e.response.data ? e.response.data : e.message || e); return []; } } // Proxy endpoint for frontend/bot to request stream status for usernames (comma separated) app.get('/api/twitch/streams', async (req, res) => { const q = req.query.users || req.query.user || ''; const users = q.split(',').map(s => (s || '').trim()).filter(Boolean); try { const streams = await getTwitchStreamsForUsers(users); res.json(streams); } catch (err) { console.error('Error in /api/twitch/streams:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); // Invite token helpers: short-lived HMAC-signed token so frontend can authorize invite deletes const INVITE_TOKEN_TTL_MS = 5 * 60 * 1000; // 5 minutes const inviteTokenSecret = process.env.INVITE_TOKEN_SECRET || process.env.ENCRYPTION_KEY || 'fallback-invite-secret'; function generateInviteToken(guildId) { const payload = JSON.stringify({ gid: guildId, iat: Date.now() }); const payloadB64 = Buffer.from(payload).toString('base64url'); const h = crypto.createHmac('sha256', inviteTokenSecret).update(payloadB64).digest('base64url'); return `${payloadB64}.${h}`; } function verifyInviteToken(token) { try { if (!token) return null; const parts = token.split('.'); if (parts.length !== 2) return null; const [payloadB64, sig] = parts; const expected = crypto.createHmac('sha256', inviteTokenSecret).update(payloadB64).digest('base64url'); if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return null; const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8')); if (!payload || !payload.gid || !payload.iat) return null; if (Date.now() - payload.iat > INVITE_TOKEN_TTL_MS) return null; return payload; } catch (e) { return null; } } app.get('/auth/discord', (req, res) => { const redirectUri = `${BACKEND_BASE}/auth/discord/callback`; const url = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=identify%20guilds`; res.redirect(url); }); // Provide a short-lived invite token for frontend actions (delete). Not a replacement for proper auth app.get('/api/servers/:guildId/invite-token', (req, res) => { const { guildId } = req.params; try { const token = generateInviteToken(guildId); res.json({ token }); } catch (err) { res.status(500).json({ success: false, message: 'Failed to generate token' }); } }); app.get('/auth/discord/callback', async (req, res) => { const code = req.query.code; if (!code) { return res.status(400).send('No code provided'); } try { const params = new URLSearchParams(); params.append('client_id', process.env.DISCORD_CLIENT_ID); params.append('client_secret', process.env.DISCORD_CLIENT_SECRET); params.append('grant_type', 'authorization_code'); params.append('code', code); params.append('redirect_uri', `${BACKEND_BASE}/auth/discord/callback`); const response = await axios.post('https://discord.com/api/oauth2/token', params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); const { access_token } = response.data; const userResponse = await axios.get('https://discord.com/api/users/@me', { headers: { Authorization: `Bearer ${access_token}`, }, }); const guildsResponse = await axios.get('https://discord.com/api/users/@me/guilds', { headers: { Authorization: `Bearer ${access_token}`, }, }); const adminGuilds = guildsResponse.data.filter(guild => (guild.permissions & 0x8) === 0x8); const user = userResponse.data; // fetch user data (theme, preferences) from Postgres try { const udata = await (require('./pg')).getUserData(user.id); user.theme = (udata && udata.theme) ? udata.theme : 'light'; } catch (e) { console.error('Error fetching user data:', e); user.theme = 'light'; } const guilds = adminGuilds; res.redirect(`${FRONTEND_BASE}/dashboard?user=${encodeURIComponent(JSON.stringify(user))}&guilds=${encodeURIComponent(JSON.stringify(guilds))}`); } catch (error) { console.error('Error during Discord OAuth2 callback:', error); res.status(500).send('Internal Server Error'); } }); const { readDb, writeDb } = require('./db'); // Require DATABASE_URL for Postgres persistence (full transition) if (!process.env.DATABASE_URL) { console.error('DATABASE_URL is not set. The backend now requires a Postgres database. Set DATABASE_URL in backend/.env'); process.exit(1); } const pgClient = require('./pg'); pgClient.ensureSchema().catch(err => { console.error('Error ensuring PG schema:', err); process.exit(1); }); console.log('Postgres enabled for persistence'); // Simple Server-Sent Events (SSE) broadcaster const sseClients = new Map(); // key: guildId or '*' -> array of res function publishEvent(guildId, type, payload) { const msg = `event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`; // send to guild-specific subscribers const list = sseClients.get(guildId) || []; for (const res of list.slice()) { try { res.write(msg); } catch (e) { /* ignore write errors */ } } // send to global subscribers const global = sseClients.get('*') || []; for (const res of global.slice()) { try { res.write(msg); } catch (e) { /* ignore */ } } } app.get('/api/events', (req, res) => { const guildId = req.query.guildId || '*'; res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders && res.flushHeaders(); // send an initial ping res.write(`event: connected\ndata: ${JSON.stringify({ guildId })}\n\n`); if (!sseClients.has(guildId)) sseClients.set(guildId, []); sseClients.get(guildId).push(res); req.on('close', () => { const arr = sseClients.get(guildId) || []; const idx = arr.indexOf(res); if (idx !== -1) arr.splice(idx, 1); }); }); app.get('/api/servers/:guildId/settings', async (req, res) => { const { guildId } = req.params; try { const settings = await pgClient.getServerSettings(guildId); return res.json(settings || { pingCommand: false }); } catch (err) { console.error('Error fetching settings:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.post('/api/servers/:guildId/settings', async (req, res) => { const { guildId } = req.params; const newSettings = req.body || {}; try { const existing = (await pgClient.getServerSettings(guildId)) || {}; const merged = { ...existing, ...newSettings }; await pgClient.upsertServerSettings(guildId, merged); return res.json({ success: true }); } catch (err) { console.error('Error saving settings:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); // Toggle a single command for a guild (preserves other toggles) app.post('/api/servers/:guildId/commands/:cmdName/toggle', async (req, res) => { const { guildId, cmdName } = req.params; const { enabled } = req.body; // boolean const protectedCommands = ['help', 'manage-commands']; if (protectedCommands.includes(cmdName)) { return res.status(403).json({ success: false, message: 'This command is locked and cannot be toggled.' }); } try { if (typeof enabled !== 'boolean') return res.status(400).json({ success: false, message: 'Missing or invalid "enabled" boolean in request body' }); const existing = (await pgClient.getServerSettings(guildId)) || {}; if (!existing.commandToggles) existing.commandToggles = {}; existing.commandToggles[cmdName] = enabled; await pgClient.upsertServerSettings(guildId, existing); // notify SSE subscribers about command toggle change try { publishEvent(guildId, 'commandToggle', { cmdName, enabled }); } catch (e) {} try { // if bot is loaded in same process, notify it to update cache if (bot && bot.setGuildSettings) { bot.setGuildSettings(guildId, existing); } } catch (notifyErr) { // ignore if bot isn't accessible } // If a remote bot push URL is configured, notify it with the new settings try { const botPushUrl = process.env.BOT_PUSH_URL || null; const botSecret = process.env.BOT_SECRET || null; if (botPushUrl) { const headers = { 'Content-Type': 'application/json' }; if (botSecret) headers['x-bot-secret'] = botSecret; await axios.post(`${botPushUrl.replace(/\/$/, '')}/internal/set-settings`, { guildId, settings: existing }, { headers }); } } catch (pushErr) { // ignore push failures } return res.json({ success: true, cmdName, enabled }); } catch (err) { console.error('Error toggling command:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.get('/api/servers/:guildId/bot-status', (req, res) => { const { guildId } = req.params; const guild = bot.client.guilds.cache.get(guildId); if (guild) { res.json({ isBotInServer: true }); } else { res.json({ isBotInServer: false }); } }); app.get('/api/client-id', (req, res) => { res.json({ clientId: process.env.DISCORD_CLIENT_ID }); }); app.post('/api/user/theme', async (req, res) => { const { userId, theme } = req.body; try { await pgClient.upsertUserData(userId, { theme }); res.json({ success: true }); } catch (err) { console.error('Error saving user theme:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.post('/api/servers/:guildId/leave', async (req, res) => { const { guildId } = req.params; try { const guild = await bot.client.guilds.fetch(guildId); if (guild) { await guild.leave(); res.json({ success: true }); } else { res.status(404).json({ success: false, message: 'Bot is not in the specified server' }); } } catch (error) { console.error('Error leaving server:', error); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.get('/api/servers/:guildId/channels', async (req, res) => { const { guildId } = req.params; const guild = bot.client.guilds.cache.get(guildId); if (!guild) { return res.json([]); } try { const channels = await guild.channels.fetch(); const textChannels = channels.filter(channel => channel.type === 0).map(channel => ({ id: channel.id, name: channel.name })); res.json(textChannels); } catch (error) { console.error('Error fetching channels:', error); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.get('/api/servers/:guildId/welcome-leave-settings', async (req, res) => { const { guildId } = req.params; try { const settings = (await pgClient.getServerSettings(guildId)) || {}; const welcomeLeaveSettings = { welcome: { enabled: settings.welcomeEnabled || false, channel: settings.welcomeChannel || '', message: settings.welcomeMessage || 'Welcome to the server, {user}!', customMessage: settings.welcomeCustomMessage || '', }, leave: { enabled: settings.leaveEnabled || false, channel: settings.leaveChannel || '', message: settings.leaveMessage || '{user} has left the server.', customMessage: settings.leaveCustomMessage || '', }, }; res.json(welcomeLeaveSettings); } catch (err) { console.error('Error fetching welcome/leave settings:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.post('/api/servers/:guildId/welcome-leave-settings', async (req, res) => { const { guildId } = req.params; const newSettings = req.body; try { const existing = (await pgClient.getServerSettings(guildId)) || {}; const merged = { ...existing }; merged.welcomeEnabled = newSettings.welcome.enabled; merged.welcomeChannel = newSettings.welcome.channel; merged.welcomeMessage = newSettings.welcome.message; merged.welcomeCustomMessage = newSettings.welcome.customMessage; merged.leaveEnabled = newSettings.leave.enabled; merged.leaveChannel = newSettings.leave.channel; merged.leaveMessage = newSettings.leave.message; merged.leaveCustomMessage = newSettings.leave.customMessage; await pgClient.upsertServerSettings(guildId, merged); return res.json({ success: true }); } catch (err) { console.error('Error saving welcome/leave settings:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.get('/api/servers/:guildId/roles', async (req, res) => { const { guildId } = req.params; const guild = bot.client.guilds.cache.get(guildId); if (!guild) { return res.json([]); } try { const rolesCollection = await guild.roles.fetch(); // Exclude @everyone (role.id === guild.id), exclude managed roles, and only include roles below the bot's highest role const botHighest = guild.members.me.roles.highest.position; const manageable = rolesCollection .filter(role => role.id !== guild.id && !role.managed && role.position < botHighest) .sort((a, b) => b.position - a.position) .map(role => ({ id: role.id, name: role.name, color: role.hexColor })); res.json(manageable); } catch (error) { console.error('Error fetching roles:', error); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.get('/api/servers/:guildId/autorole-settings', async (req, res) => { const { guildId } = req.params; try { const settings = (await pgClient.getServerSettings(guildId)) || {}; const autoroleSettings = settings.autorole || { enabled: false, roleId: '' }; res.json(autoroleSettings); } catch (err) { console.error('Error fetching autorole settings:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.post('/api/servers/:guildId/autorole-settings', async (req, res) => { const { guildId } = req.params; const { enabled, roleId } = req.body; try { if (pgClient) { const existing = (await pgClient.getServerSettings(guildId)) || {}; existing.autorole = { enabled, roleId }; await pgClient.upsertServerSettings(guildId, existing); return res.json({ success: true }); } const db = readDb(); if (!db[guildId]) db[guildId] = {}; db[guildId].autorole = { enabled, roleId }; writeDb(db); res.json({ success: true }); } catch (err) { console.error('Error saving autorole settings:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); // Live notifications (Twitch) - per-guild settings app.get('/api/servers/:guildId/live-notifications', async (req, res) => { const { guildId } = req.params; try { const settings = (await pgClient.getServerSettings(guildId)) || {}; return res.json(settings.liveNotifications || { enabled: false, twitchUser: '', channelId: '' }); } catch (err) { console.error('Error fetching live-notifications settings:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.post('/api/servers/:guildId/live-notifications', async (req, res) => { const { guildId } = req.params; const { enabled, twitchUser, channelId } = req.body || {}; try { const existing = (await pgClient.getServerSettings(guildId)) || {}; existing.liveNotifications = { enabled: !!enabled, twitchUser: twitchUser || '', channelId: channelId || '' }; await pgClient.upsertServerSettings(guildId, existing); try { publishEvent(guildId, 'liveNotificationsUpdate', { enabled: !!enabled, channelId, twitchUser }); } catch (e) {} return res.json({ success: true }); } catch (err) { console.error('Error saving live-notifications settings:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); // Twitch users list management for a guild app.get('/api/servers/:guildId/twitch-users', async (req, res) => { const { guildId } = req.params; try { const settings = (await pgClient.getServerSettings(guildId)) || {}; const users = (settings.liveNotifications && settings.liveNotifications.users) || []; res.json(users); } catch (err) { console.error('Error fetching twitch users:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.post('/api/servers/:guildId/twitch-users', async (req, res) => { const { guildId } = req.params; const { username } = req.body || {}; if (!username) return res.status(400).json({ success: false, message: 'Missing username' }); try { const existing = (await pgClient.getServerSettings(guildId)) || {}; if (!existing.liveNotifications) existing.liveNotifications = { enabled: false, channelId: '', users: [] }; existing.liveNotifications.users = Array.from(new Set([...(existing.liveNotifications.users || []), username.toLowerCase().trim()])); await pgClient.upsertServerSettings(guildId, existing); try { publishEvent(guildId, 'twitchUsersUpdate', { users: existing.liveNotifications.users || [] }); } catch (e) {} res.json({ success: true }); } catch (err) { console.error('Error adding twitch user:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.delete('/api/servers/:guildId/twitch-users/:username', async (req, res) => { const { guildId, username } = req.params; try { const existing = (await pgClient.getServerSettings(guildId)) || {}; if (!existing.liveNotifications) existing.liveNotifications = { enabled: false, channelId: '', users: [] }; existing.liveNotifications.users = (existing.liveNotifications.users || []).filter(u => u.toLowerCase() !== (username || '').toLowerCase()); await pgClient.upsertServerSettings(guildId, existing); try { publishEvent(guildId, 'twitchUsersUpdate', { users: existing.liveNotifications.users || [] }); } catch (e) {} res.json({ success: true }); } catch (err) { console.error('Error removing twitch user:', err); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.get('/', (req, res) => { res.send('Hello from the backend!'); }); // Return list of bot commands and per-guild enabled/disabled status app.get('/api/servers/:guildId/commands', async (req, res) => { try { const { guildId } = req.params; const guildSettings = (await pgClient.getServerSettings(guildId)) || {}; const toggles = guildSettings.commandToggles || {}; const protectedCommands = ['manage-commands', 'help']; const commands = Array.from(bot.client.commands.values()).map(cmd => { const isLocked = protectedCommands.includes(cmd.name); const isEnabled = isLocked ? true : (toggles[cmd.name] !== false && cmd.enabled !== false); return { name: cmd.name, description: cmd.description || 'No description.', enabled: isEnabled, locked: isLocked, hasSlashBuilder: !!cmd.builder, }; }); res.json(commands); } catch (error) { console.error('Error returning commands:', error); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); // INVITES: create, list, delete app.get('/api/servers/:guildId/invites', async (req, res) => { try { const { guildId } = req.params; const saved = await pgClient.listInvites(guildId); // try to enrich with live data where possible const guild = bot.client.guilds.cache.get(guildId); let liveInvites = []; if (guild) { try { const fetched = await guild.invites.fetch(); liveInvites = Array.from(fetched.values()); } catch (e) { // ignore fetch errors } } const combined = saved.map(inv => { const live = liveInvites.find(li => li.code === inv.code); return { ...inv, uses: live ? live.uses : inv.uses || 0, maxUses: inv.maxUses || (live ? live.maxUses : 0), maxAge: inv.maxAge || (live ? live.maxAge : 0), }; }); res.json(combined); } catch (error) { console.error('Error listing invites:', error); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.post('/api/servers/:guildId/invites', async (req, res) => { try { const { guildId } = req.params; const { channelId, maxAge, maxUses, temporary } = req.body || {}; const guild = bot.client.guilds.cache.get(guildId); if (!guild) return res.status(404).json({ success: false, message: 'Guild not found' }); let channel = null; if (channelId) { try { channel = await guild.channels.fetch(channelId); } catch (e) { channel = null; } } if (!channel) { // fall back to first text channel const channels = await guild.channels.fetch(); channel = channels.find(c => c.type === 0) || channels.first(); } if (!channel) return res.status(400).json({ success: false, message: 'No channel available to create invite' }); const inviteOptions = { maxAge: typeof maxAge === 'number' ? maxAge : 0, maxUses: typeof maxUses === 'number' ? maxUses : 0, temporary: !!temporary, unique: true, }; const invite = await channel.createInvite(inviteOptions); // persist invite to Postgres const item = { code: invite.code, url: invite.url, channelId: channel.id, createdAt: new Date().toISOString(), maxUses: invite.maxUses || inviteOptions.maxUses || 0, maxAge: invite.maxAge || inviteOptions.maxAge || 0, temporary: !!invite.temporary, }; await pgClient.addInvite({ code: item.code, guildId, url: item.url, channelId: item.channelId, createdAt: item.createdAt, maxUses: item.maxUses, maxAge: item.maxAge, temporary: item.temporary }); res.json({ success: true, invite: item }); } catch (error) { console.error('Error creating invite:', error); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); app.delete('/api/servers/:guildId/invites/:code', async (req, res) => { // Require a short-lived invite token issued by /api/servers/:guildId/invite-token const providedToken = req.headers['x-invite-token']; const payload = verifyInviteToken(providedToken); if (!payload || payload.gid !== req.params.guildId) { return res.status(401).json({ success: false, message: 'Unauthorized: missing or invalid invite token' }); } try { const { guildId, code } = req.params; const guild = bot.client.guilds.cache.get(guildId); // Try to delete on Discord if possible if (guild) { try { // fetch invites and delete matching code const fetched = await guild.invites.fetch(); const inv = fetched.find(i => i.code === code); if (inv) await inv.delete(); } catch (e) { // ignore } } if (pgClient) { await pgClient.deleteInvite(guildId, code); } else { const db = readDb(); if (db[guildId] && db[guildId].invites) { db[guildId].invites = db[guildId].invites.filter(i => i.code !== code); writeDb(db); } } res.json({ success: true }); } catch (error) { console.error('Error deleting invite:', error); res.status(500).json({ success: false, message: 'Internal Server Error' }); } }); const bot = require('../discord-bot'); bot.login(); // Dev/testing endpoint: force a live announcement app.post('/internal/test-live', express.json(), async (req, res) => { const { guildId, username, title } = req.body || {}; if (!guildId || !username) return res.status(400).json({ success: false, message: 'guildId and username required' }); try { const stream = { user_login: username.toLowerCase(), user_name: username, title: title || `${username} is live (test)`, viewer_count: 1, started_at: new Date().toISOString(), url: `https://www.twitch.tv/${username.toLowerCase()}`, thumbnail_url: null, description: 'Test notification', profile_image_url: null, game_name: 'Testing', }; const result = await bot.announceLive(guildId, stream); res.json(result); } catch (e) { console.error('Error in /internal/test-live:', e && e.message ? e.message : e); res.status(500).json({ success: false, message: 'Internal error' }); } }); app.listen(port, host, () => { console.log(`Server is running on ${host}:${port}`); });