Update backend, DB, Commands, Live Reloading

This commit is contained in:
2025-10-09 02:17:33 -04:00
parent 6a78ec6453
commit 2ae7202445
22 changed files with 1283 additions and 249 deletions

View File

@@ -158,6 +158,214 @@ app.get('/api/twitch/streams', async (req, res) => {
}
});
// Kick API helpers (web scraping since no public API)
async function getKickStreamsForUsers(usernames = []) {
try {
if (!usernames || usernames.length === 0) return [];
const results = [];
for (const username of usernames) {
try {
// Use Kick's API endpoint to check if user is live
const url = `https://kick.com/api/v1/channels/${encodeURIComponent(username)}`;
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'application/json',
'Referer': 'https://kick.com/'
},
timeout: 5000 // 5 second timeout
});
if (response.status === 200 && response.data) {
const data = response.data;
if (data.livestream && data.livestream.is_live) {
results.push({
is_live: true,
user_login: username,
user_name: data.user?.username || username,
title: data.livestream.session_title || `${username} is live`,
viewer_count: data.livestream.viewer_count || 0,
started_at: data.livestream.start_time,
url: `https://kick.com/${username}`,
thumbnail_url: data.livestream.thumbnail?.url || null,
category: data.category?.name || 'Unknown',
description: data.user?.bio || ''
});
} else {
// User exists but not live
results.push({
is_live: false,
user_login: username,
user_name: data.user?.username || username,
title: null,
viewer_count: 0,
started_at: null,
url: `https://kick.com/${username}`,
thumbnail_url: null,
category: null,
description: data.user?.bio || ''
});
}
} else {
// User not found or API error
results.push({
is_live: false,
user_login: username,
user_name: username,
title: null,
viewer_count: 0,
started_at: null,
url: `https://kick.com/${username}`,
thumbnail_url: null,
category: null,
description: ''
});
}
} catch (e) {
// If API fails with 403, try web scraping as fallback
if (e.response && e.response.status === 403) {
// console.log(`API blocked for ${username}, trying web scraping fallback...`);
try {
const pageUrl = `https://kick.com/${encodeURIComponent(username)}`;
const pageResponse = await axios.get(pageUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Cache-Control': 'max-age=0'
},
timeout: 5000
});
if (pageResponse.status === 200) {
const html = pageResponse.data;
// Check for live stream indicators in the HTML
const isLive = html.includes('"is_live":true') || html.includes('"is_live": true') ||
html.includes('data-is-live="true"') || html.includes('isLive:true');
if (isLive) {
// Try to extract stream info from HTML
let title = `${username} is live`;
let viewerCount = 0;
let category = 'Unknown';
// Extract title
const titleMatch = html.match(/"session_title"\s*:\s*"([^"]+)"/) || html.match(/"title"\s*:\s*"([^"]+)"/);
if (titleMatch) {
title = titleMatch[1].replace(/\\"/g, '"');
}
// Extract viewer count
const viewerMatch = html.match(/"viewer_count"\s*:\s*(\d+)/);
if (viewerMatch) {
viewerCount = parseInt(viewerMatch[1]);
}
// Extract category
const categoryMatch = html.match(/"category"\s*:\s*{\s*"name"\s*:\s*"([^"]+)"/);
if (categoryMatch) {
category = categoryMatch[1];
}
results.push({
is_live: true,
user_login: username,
user_name: username,
title: title,
viewer_count: viewerCount,
started_at: new Date().toISOString(),
url: `https://kick.com/${username}`,
thumbnail_url: null,
category: category,
description: ''
});
} else {
// User exists but not live
results.push({
is_live: false,
user_login: username,
user_name: username,
title: null,
viewer_count: 0,
started_at: null,
url: `https://kick.com/${username}`,
thumbnail_url: null,
category: null,
description: ''
});
}
} else {
throw e; // Re-throw if page request also fails
}
} catch (scrapeError) {
console.error(`Web scraping fallback also failed for ${username}:`, scrapeError.message || scrapeError);
// Return offline status on error
results.push({
is_live: false,
user_login: username,
user_name: username,
title: null,
viewer_count: 0,
started_at: null,
url: `https://kick.com/${username}`,
thumbnail_url: null,
category: null,
description: ''
});
}
} else {
console.error(`Error checking Kick user ${username}:`, e && e.response && e.response.status ? `HTTP ${e.response.status}` : e.message || e);
// Return offline status on error
results.push({
is_live: false,
user_login: username,
user_name: username,
title: null,
viewer_count: 0,
started_at: null,
url: `https://kick.com/${username}`,
thumbnail_url: null,
category: null,
description: ''
});
}
}
// Small delay between requests to be respectful to Kick's servers
await new Promise(r => setTimeout(r, 200));
}
return results;
} catch (e) {
console.error('Error in getKickStreamsForUsers:', e && e.response && e.response.data ? e.response.data : e.message || e);
return [];
}
}
// Proxy endpoint for frontend/bot to request Kick stream status for usernames (comma separated)
app.get('/api/kick/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 getKickStreamsForUsers(users);
res.json(streams);
} catch (err) {
console.error('Error in /api/kick/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';
@@ -563,7 +771,8 @@ 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: '' });
const ln = settings.liveNotifications || { enabled: false, twitchUser: '', channelId: '', message: '', customMessage: '' };
return res.json(ln);
} catch (err) {
console.error('Error fetching live-notifications settings:', err);
res.status(500).json({ success: false, message: 'Internal Server Error' });
@@ -572,16 +781,21 @@ app.get('/api/servers/:guildId/live-notifications', async (req, res) => {
app.post('/api/servers/:guildId/live-notifications', async (req, res) => {
const { guildId } = req.params;
const { enabled, twitchUser, channelId } = req.body || {};
const { enabled, twitchUser, channelId, message, customMessage } = req.body || {};
try {
const existing = (await pgClient.getServerSettings(guildId)) || {};
const currentLn = existing.liveNotifications || {};
existing.liveNotifications = {
enabled: !!enabled,
twitchUser: twitchUser || '',
channelId: channelId || ''
channelId: channelId || '',
message: message || '',
customMessage: customMessage || '',
users: currentLn.users || [], // preserve existing users
kickUsers: currentLn.kickUsers || [] // preserve existing kick users
};
await pgClient.upsertServerSettings(guildId, existing);
try { publishEvent(guildId, 'liveNotificationsUpdate', { enabled: !!enabled, channelId, twitchUser }); } catch (e) {}
try { publishEvent(guildId, 'liveNotificationsUpdate', { enabled: !!enabled, channelId, twitchUser, message: existing.liveNotifications.message, customMessage: existing.liveNotifications.customMessage }); } catch (e) {}
return res.json({ success: true });
} catch (err) {
console.error('Error saving live-notifications settings:', err);
@@ -608,10 +822,18 @@ app.post('/api/servers/:guildId/twitch-users', async (req, res) => {
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: [] };
if (!existing.liveNotifications) existing.liveNotifications = { enabled: false, channelId: '', users: [], kickUsers: [] };
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) {}
// Optional push to bot process for immediate cache update
try {
if (process.env.BOT_PUSH_URL) {
await axios.post(`${process.env.BOT_PUSH_URL.replace(/\/$/, '')}/internal/set-settings`, { guildId, settings: existing }, {
headers: process.env.BOT_SECRET ? { 'x-bot-secret': process.env.BOT_SECRET } : {}
}).catch(() => {});
}
} catch (_) {}
res.json({ success: true });
} catch (err) {
console.error('Error adding twitch user:', err);
@@ -623,10 +845,18 @@ 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: [] };
if (!existing.liveNotifications) existing.liveNotifications = { enabled: false, channelId: '', users: [], kickUsers: [] };
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) {}
// Optional push to bot process for immediate cache update
try {
if (process.env.BOT_PUSH_URL) {
await axios.post(`${process.env.BOT_PUSH_URL.replace(/\/$/, '')}/internal/set-settings`, { guildId, settings: existing }, {
headers: process.env.BOT_SECRET ? { 'x-bot-secret': process.env.BOT_SECRET } : {}
}).catch(() => {});
}
} catch (_) {}
res.json({ success: true });
} catch (err) {
console.error('Error removing twitch user:', err);
@@ -634,6 +864,69 @@ app.delete('/api/servers/:guildId/twitch-users/:username', async (req, res) => {
}
});
// DISABLED: Kick users list management for a guild (temporarily disabled)
/*
app.get('/api/servers/:guildId/kick-users', async (req, res) => {
const { guildId } = req.params;
try {
const settings = (await pgClient.getServerSettings(guildId)) || {};
const users = (settings.liveNotifications && settings.liveNotifications.kickUsers) || [];
res.json(users);
} catch (err) {
console.error('Error fetching kick users:', err);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.post('/api/servers/:guildId/kick-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: [], kickUsers: [] };
existing.liveNotifications.kickUsers = Array.from(new Set([...(existing.liveNotifications.kickUsers || []), username.toLowerCase().trim()]));
await pgClient.upsertServerSettings(guildId, existing);
try { publishEvent(guildId, 'kickUsersUpdate', { users: existing.liveNotifications.kickUsers || [] }); } catch (e) {}
// Optional push to bot process for immediate cache update
try {
if (process.env.BOT_PUSH_URL) {
await axios.post(`${process.env.BOT_PUSH_URL.replace(/\/$/, '')}/internal/set-settings`, { guildId, settings: existing }, {
headers: process.env.BOT_SECRET ? { 'x-bot-secret': process.env.BOT_SECRET } : {}
}).catch(() => {});
}
} catch (_) {}
res.json({ success: true });
} catch (err) {
console.error('Error adding kick user:', err);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.delete('/api/servers/:guildId/kick-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: [], kickUsers: [] };
existing.liveNotifications.kickUsers = (existing.liveNotifications.kickUsers || []).filter(u => u.toLowerCase() !== (username || '').toLowerCase());
await pgClient.upsertServerSettings(guildId, existing);
try { publishEvent(guildId, 'kickUsersUpdate', { users: existing.liveNotifications.kickUsers || [] }); } catch (e) {}
// Optional push to bot process for immediate cache update
try {
if (process.env.BOT_PUSH_URL) {
await axios.post(`${process.env.BOT_PUSH_URL.replace(/\/$/, '')}/internal/set-settings`, { guildId, settings: existing }, {
headers: process.env.BOT_SECRET ? { 'x-bot-secret': process.env.BOT_SECRET } : {}
}).catch(() => {});
}
} catch (_) {}
res.json({ success: true });
} catch (err) {
console.error('Error removing kick user:', err);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
*/
app.get('/', (req, res) => {
res.send('Hello from the backend!');
});
@@ -661,7 +954,9 @@ app.get('/api/servers/:guildId/commands', async (req, res) => {
const toggles = guildSettings.commandToggles || {};
const protectedCommands = ['manage-commands', 'help'];
const commands = Array.from(bot.client.commands.values()).map(cmd => {
const commands = Array.from(bot.client.commands.values())
.filter(cmd => !cmd.dev) // Filter out dev commands
.map(cmd => {
const isLocked = protectedCommands.includes(cmd.name);
const isEnabled = isLocked ? true : (toggles[cmd.name] !== false && cmd.enabled !== false);
return {

View File

@@ -16,10 +16,10 @@
"cors": "^2.8.5",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.5",
"express": "^4.19.2"
,"pg": "^8.11.0",
"pg-format": "^1.0.4"
,"node-fetch": "^2.6.7"
"express": "^4.19.2",
"pg": "^8.11.0",
"pg-format": "^1.0.4",
"node-fetch": "^2.6.7"
},
"devDependencies": {
"nodemon": "^3.1.3"