Update backend, DB, Commands, Live Reloading
This commit is contained in:
309
backend/index.js
309
backend/index.js
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user