swapped to a new db locally hosted
This commit is contained in:
522
backend/index.js
522
backend/index.js
@@ -21,6 +21,143 @@ 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';
|
||||
@@ -103,8 +240,14 @@ app.get('/auth/discord/callback', async (req, res) => {
|
||||
const adminGuilds = guildsResponse.data.filter(guild => (guild.permissions & 0x8) === 0x8);
|
||||
|
||||
const user = userResponse.data;
|
||||
const db = readDb();
|
||||
user.theme = db.users && db.users[user.id] ? db.users[user.id].theme : 'light';
|
||||
// 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) {
|
||||
@@ -115,41 +258,117 @@ app.get('/auth/discord/callback', async (req, res) => {
|
||||
|
||||
const { readDb, writeDb } = require('./db');
|
||||
|
||||
app.get('/api/servers/:guildId/settings', (req, res) => {
|
||||
const { guildId } = req.params;
|
||||
const db = readDb();
|
||||
const settings = db[guildId] || { pingCommand: false };
|
||||
res.json(settings);
|
||||
// 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.post('/api/servers/:guildId/settings', (req, res) => {
|
||||
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 || {};
|
||||
const db = readDb();
|
||||
if (!db[guildId]) db[guildId] = {};
|
||||
// Merge incoming settings with existing settings to avoid overwriting unrelated keys
|
||||
db[guildId] = { ...db[guildId], ...newSettings };
|
||||
writeDb(db);
|
||||
res.json({ success: true });
|
||||
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', (req, res) => {
|
||||
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.' });
|
||||
}
|
||||
const db = readDb();
|
||||
if (!db[guildId]) db[guildId] = {};
|
||||
if (!db[guildId].commandToggles) db[guildId].commandToggles = {};
|
||||
if (typeof enabled === 'boolean') {
|
||||
db[guildId].commandToggles[cmdName] = enabled;
|
||||
writeDb(db);
|
||||
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' });
|
||||
}
|
||||
return res.status(400).json({ success: false, message: 'Missing or invalid "enabled" boolean in request body' });
|
||||
});
|
||||
|
||||
app.get('/api/servers/:guildId/bot-status', (req, res) => {
|
||||
@@ -166,18 +385,15 @@ app.get('/api/client-id', (req, res) => {
|
||||
res.json({ clientId: process.env.DISCORD_CLIENT_ID });
|
||||
});
|
||||
|
||||
app.post('/api/user/theme', (req, res) => {
|
||||
app.post('/api/user/theme', async (req, res) => {
|
||||
const { userId, theme } = req.body;
|
||||
const db = readDb();
|
||||
if (!db.users) {
|
||||
db.users = {};
|
||||
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' });
|
||||
}
|
||||
if (!db.users[userId]) {
|
||||
db.users[userId] = {};
|
||||
}
|
||||
db.users[userId].theme = theme;
|
||||
writeDb(db);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.post('/api/servers/:guildId/leave', async (req, res) => {
|
||||
@@ -212,51 +428,54 @@ app.get('/api/servers/:guildId/channels', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/servers/:guildId/welcome-leave-settings', (req, res) => {
|
||||
app.get('/api/servers/:guildId/welcome-leave-settings', async (req, res) => {
|
||||
const { guildId } = req.params;
|
||||
const db = readDb();
|
||||
const settings = db[guildId] || {};
|
||||
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 || '',
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
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', (req, res) => {
|
||||
app.post('/api/servers/:guildId/welcome-leave-settings', async (req, res) => {
|
||||
const { guildId } = req.params;
|
||||
const newSettings = req.body;
|
||||
const db = readDb();
|
||||
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;
|
||||
|
||||
if (!db[guildId]) {
|
||||
db[guildId] = {};
|
||||
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' });
|
||||
}
|
||||
|
||||
db[guildId].welcomeEnabled = newSettings.welcome.enabled;
|
||||
db[guildId].welcomeChannel = newSettings.welcome.channel;
|
||||
db[guildId].welcomeMessage = newSettings.welcome.message;
|
||||
db[guildId].welcomeCustomMessage = newSettings.welcome.customMessage;
|
||||
|
||||
db[guildId].leaveEnabled = newSettings.leave.enabled;
|
||||
db[guildId].leaveChannel = newSettings.leave.channel;
|
||||
db[guildId].leaveMessage = newSettings.leave.message;
|
||||
db[guildId].leaveCustomMessage = newSettings.leave.customMessage;
|
||||
|
||||
writeDb(db);
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.get('/api/servers/:guildId/roles', async (req, res) => {
|
||||
@@ -280,26 +499,113 @@ app.get('/api/servers/:guildId/roles', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/servers/:guildId/autorole-settings', (req, res) => {
|
||||
app.get('/api/servers/:guildId/autorole-settings', async (req, res) => {
|
||||
const { guildId } = req.params;
|
||||
const db = readDb();
|
||||
const settings = db[guildId] || {};
|
||||
const autoroleSettings = settings.autorole || { enabled: false, roleId: '' };
|
||||
res.json(autoroleSettings);
|
||||
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', (req, res) => {
|
||||
app.post('/api/servers/:guildId/autorole-settings', async (req, res) => {
|
||||
const { guildId } = req.params;
|
||||
const { enabled, roleId } = req.body;
|
||||
const db = readDb();
|
||||
|
||||
if (!db[guildId]) {
|
||||
db[guildId] = {};
|
||||
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' });
|
||||
}
|
||||
});
|
||||
|
||||
db[guildId].autorole = { enabled, roleId };
|
||||
writeDb(db);
|
||||
// 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) => {
|
||||
@@ -307,11 +613,10 @@ app.get('/', (req, res) => {
|
||||
});
|
||||
|
||||
// Return list of bot commands and per-guild enabled/disabled status
|
||||
app.get('/api/servers/:guildId/commands', (req, res) => {
|
||||
app.get('/api/servers/:guildId/commands', async (req, res) => {
|
||||
try {
|
||||
const { guildId } = req.params;
|
||||
const db = readDb();
|
||||
const guildSettings = db[guildId] || {};
|
||||
const guildSettings = (await pgClient.getServerSettings(guildId)) || {};
|
||||
const toggles = guildSettings.commandToggles || {};
|
||||
const protectedCommands = ['manage-commands', 'help'];
|
||||
|
||||
@@ -338,8 +643,7 @@ app.get('/api/servers/:guildId/commands', (req, res) => {
|
||||
app.get('/api/servers/:guildId/invites', async (req, res) => {
|
||||
try {
|
||||
const { guildId } = req.params;
|
||||
const db = readDb();
|
||||
const saved = (db[guildId] && db[guildId].invites) ? db[guildId].invites : [];
|
||||
const saved = await pgClient.listInvites(guildId);
|
||||
|
||||
// try to enrich with live data where possible
|
||||
const guild = bot.client.guilds.cache.get(guildId);
|
||||
@@ -377,7 +681,7 @@ app.post('/api/servers/:guildId/invites', async (req, res) => {
|
||||
const guild = bot.client.guilds.cache.get(guildId);
|
||||
if (!guild) return res.status(404).json({ success: false, message: 'Guild not found' });
|
||||
|
||||
let channel = null;
|
||||
let channel = null;
|
||||
if (channelId) {
|
||||
try { channel = await guild.channels.fetch(channelId); } catch (e) { channel = null; }
|
||||
}
|
||||
@@ -397,9 +701,7 @@ app.post('/api/servers/:guildId/invites', async (req, res) => {
|
||||
|
||||
const invite = await channel.createInvite(inviteOptions);
|
||||
|
||||
const db = readDb();
|
||||
if (!db[guildId]) db[guildId] = {};
|
||||
if (!db[guildId].invites) db[guildId].invites = [];
|
||||
// persist invite to Postgres
|
||||
|
||||
const item = {
|
||||
code: invite.code,
|
||||
@@ -411,8 +713,7 @@ app.post('/api/servers/:guildId/invites', async (req, res) => {
|
||||
temporary: !!invite.temporary,
|
||||
};
|
||||
|
||||
db[guildId].invites.push(item);
|
||||
writeDb(db);
|
||||
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) {
|
||||
@@ -430,7 +731,6 @@ app.delete('/api/servers/:guildId/invites/:code', async (req, res) => {
|
||||
}
|
||||
try {
|
||||
const { guildId, code } = req.params;
|
||||
const db = readDb();
|
||||
const guild = bot.client.guilds.cache.get(guildId);
|
||||
|
||||
// Try to delete on Discord if possible
|
||||
@@ -445,9 +745,14 @@ app.delete('/api/servers/:guildId/invites/:code', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (db[guildId] && db[guildId].invites) {
|
||||
db[guildId].invites = db[guildId].invites.filter(i => i.code !== code);
|
||||
writeDb(db);
|
||||
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 });
|
||||
@@ -461,6 +766,31 @@ 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}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user