299 lines
10 KiB
JavaScript
299 lines
10 KiB
JavaScript
const fetch = require('node-fetch');
|
|
|
|
// Resolve backend candidates (env or common local addresses). We'll try them in order
|
|
// for each request so the bot can still reach the backend even when it binds to
|
|
// a specific non-loopback IP.
|
|
const envBase = process.env.BACKEND_BASE ? process.env.BACKEND_BASE.replace(/\/$/, '') : null;
|
|
const host = process.env.BACKEND_HOST || process.env.HOST || '127.0.0.1';
|
|
const port = process.env.BACKEND_PORT || process.env.PORT || '3002';
|
|
const CANDIDATES = [envBase, `http://${host}:${port}`, `http://localhost:${port}`, `http://127.0.0.1:${port}`].filter(Boolean);
|
|
|
|
async function tryFetch(url, opts = {}) {
|
|
// Try each candidate base until one responds successfully
|
|
for (const base of CANDIDATES) {
|
|
const target = `${base.replace(/\/$/, '')}${url}`;
|
|
try {
|
|
const res = await fetch(target, opts);
|
|
if (res && (res.ok || res.status === 204)) {
|
|
return res;
|
|
}
|
|
// if this candidate returned a non-ok status, log and continue trying others
|
|
console.error(`Candidate ${base} returned ${res.status} ${res.statusText} for ${target}`);
|
|
} catch (e) {
|
|
// network error for this candidate; try next
|
|
// console.debug(`Candidate ${base} failed:`, e && e.message ? e.message : e);
|
|
}
|
|
}
|
|
// none of the candidates succeeded
|
|
return null;
|
|
}
|
|
|
|
async function safeFetchJsonPath(path, opts = {}) {
|
|
const res = await tryFetch(path, opts);
|
|
if (!res) return null;
|
|
try {
|
|
return await res.json();
|
|
} catch (e) {
|
|
console.error('Failed to parse JSON from backend response:', e && e.message ? e.message : e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getServerSettings(guildId) {
|
|
const path = `/api/servers/${guildId}/settings`;
|
|
const json = await safeFetchJsonPath(path);
|
|
return json || {};
|
|
}
|
|
|
|
async function upsertServerSettings(guildId, settings) {
|
|
const path = `/api/servers/${guildId}/settings`;
|
|
try {
|
|
const res = await tryFetch(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(settings),
|
|
});
|
|
return res && res.ok;
|
|
} catch (e) {
|
|
console.error(`Failed to upsert settings for ${guildId}:`, e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function getCommands(guildId) {
|
|
const path = `/api/servers/${guildId}/commands`;
|
|
const json = await safeFetchJsonPath(path);
|
|
return json || [];
|
|
}
|
|
|
|
async function toggleCommand(guildId, cmdName, enabled) {
|
|
const path = `/api/servers/${guildId}/commands/${cmdName}/toggle`;
|
|
try {
|
|
const res = await tryFetch(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
return res && res.ok;
|
|
} catch (e) {
|
|
console.error(`Failed to toggle command ${cmdName} for ${guildId}:`, e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function listInvites(guildId) {
|
|
const path = `/api/servers/${guildId}/invites`;
|
|
const json = await safeFetchJsonPath(path);
|
|
return json || [];
|
|
}
|
|
|
|
async function listReactionRoles(guildId) {
|
|
const path = `/api/servers/${guildId}/reaction-roles`;
|
|
const json = await safeFetchJsonPath(path);
|
|
return json || [];
|
|
}
|
|
|
|
async function addInvite(guildId, invite) {
|
|
const path = `/api/servers/${guildId}/invites`;
|
|
try {
|
|
// If invite is an object with code property, it's already created - send full data
|
|
// If it's just channelId/maxAge/etc, it's for creation
|
|
const isExistingInvite = invite && typeof invite === 'object' && invite.code;
|
|
|
|
const body = isExistingInvite ? {
|
|
code: invite.code,
|
|
url: invite.url,
|
|
channelId: invite.channelId,
|
|
maxUses: invite.maxUses,
|
|
maxAge: invite.maxAge,
|
|
temporary: invite.temporary,
|
|
createdAt: invite.createdAt
|
|
} : invite;
|
|
|
|
const res = await tryFetch(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
return res && res.ok;
|
|
} catch (e) {
|
|
console.error(`Failed to add invite for ${guildId}:`, e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function deleteInvite(guildId, code) {
|
|
const path = `/api/servers/${guildId}/invites/${code}`;
|
|
try {
|
|
const res = await tryFetch(path, { method: 'DELETE' });
|
|
return res && res.ok;
|
|
} catch (e) {
|
|
console.error(`Failed to delete invite ${code} for ${guildId}:`, e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function updateReactionRole(guildId, id, updates) {
|
|
const path = `/api/servers/${guildId}/reaction-roles/${id}`;
|
|
try {
|
|
const res = await tryFetch(path, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updates),
|
|
});
|
|
if (!res) return null;
|
|
try { return await res.json(); } catch (e) { return null; }
|
|
} catch (e) {
|
|
console.error(`Failed to update reaction role ${id} for ${guildId}:`, e && e.message ? e.message : e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function deleteReactionRole(guildId, id) {
|
|
const path = `/api/servers/${guildId}/reaction-roles/${id}`;
|
|
try {
|
|
const res = await tryFetch(path, { method: 'DELETE' });
|
|
return res && res.ok;
|
|
} catch (e) {
|
|
console.error(`Failed to delete reaction role ${id} for ${guildId}:`, e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
module.exports = { getServerSettings, upsertServerSettings, getCommands, toggleCommand, listInvites, addInvite, deleteInvite };
|
|
// Twitch users helpers
|
|
async function getTwitchUsers(guildId) {
|
|
const path = `/api/servers/${guildId}/twitch-users`;
|
|
const json = await safeFetchJsonPath(path);
|
|
return json || [];
|
|
}
|
|
|
|
async function addTwitchUser(guildId, username) {
|
|
const path = `/api/servers/${guildId}/twitch-users`;
|
|
try {
|
|
const res = await tryFetch(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username }),
|
|
});
|
|
return res && res.ok;
|
|
} catch (e) {
|
|
console.error(`Failed to add twitch user ${username} for ${guildId}:`, e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function deleteTwitchUser(guildId, username) {
|
|
const path = `/api/servers/${guildId}/twitch-users/${encodeURIComponent(username)}`;
|
|
try {
|
|
const res = await tryFetch(path, { method: 'DELETE' });
|
|
return res && res.ok;
|
|
} catch (e) {
|
|
console.error(`Failed to delete twitch user ${username} for ${guildId}:`, e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Fetch stream status via backend proxy endpoint /api/twitch/streams?users=a,b,c
|
|
async function tryFetchTwitchStreams(usersCsv) {
|
|
const path = `/api/twitch/streams?users=${encodeURIComponent(usersCsv || '')}`;
|
|
const json = await safeFetchJsonPath(path);
|
|
return json || [];
|
|
}
|
|
|
|
// Raw direct call helper (not used in most environments) — kept for legacy watcher
|
|
async function _rawGetTwitchStreams(usersCsv) {
|
|
// Try direct backend candidate first
|
|
const path = `/api/twitch/streams?users=${encodeURIComponent(usersCsv || '')}`;
|
|
const res = await tryFetch(path);
|
|
if (!res) return [];
|
|
try { return await res.json(); } catch (e) { return []; }
|
|
}
|
|
|
|
// Kick users helpers
|
|
async function getKickUsers(guildId) {
|
|
const path = `/api/servers/${guildId}/kick-users`;
|
|
const json = await safeFetchJsonPath(path);
|
|
return json || [];
|
|
}
|
|
|
|
async function addKickUser(guildId, username) {
|
|
const path = `/api/servers/${guildId}/kick-users`;
|
|
try {
|
|
const res = await tryFetch(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username }),
|
|
});
|
|
return res && res.ok;
|
|
} catch (e) {
|
|
console.error(`Failed to add kick user ${username} for ${guildId}:`, e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function deleteKickUser(guildId, username) {
|
|
const path = `/api/servers/${guildId}/kick-users/${encodeURIComponent(username)}`;
|
|
try {
|
|
const res = await tryFetch(path, { method: 'DELETE' });
|
|
return res && res.ok;
|
|
} catch (e) {
|
|
console.error(`Failed to delete kick user ${username} for ${guildId}:`, e && e.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function getWelcomeLeaveSettings(guildId) {
|
|
const path = `/api/servers/${guildId}/welcome-leave-settings`;
|
|
const json = await safeFetchJsonPath(path);
|
|
return json || { welcome: { enabled: false }, leave: { enabled: false } };
|
|
}
|
|
|
|
async function getAutoroleSettings(guildId) {
|
|
const path = `/api/servers/${guildId}/autorole-settings`;
|
|
const json = await safeFetchJsonPath(path);
|
|
return json || { enabled: false, roleId: '' };
|
|
}
|
|
|
|
async function reconcileInvites(guildId, currentDiscordInvites) {
|
|
try {
|
|
// Get invites from database
|
|
const dbInvites = await listInvites(guildId) || [];
|
|
|
|
// Find invites in database that no longer exist in Discord
|
|
const discordInviteCodes = new Set(currentDiscordInvites.map(inv => inv.code));
|
|
const deletedInvites = dbInvites.filter(dbInv => !discordInviteCodes.has(dbInv.code));
|
|
|
|
// Delete each invite that no longer exists
|
|
for (const invite of deletedInvites) {
|
|
console.log(`🗑️ Reconciling deleted invite ${invite.code} for guild ${guildId}`);
|
|
await deleteInvite(guildId, invite.code);
|
|
|
|
// Publish SSE event for frontend update
|
|
try {
|
|
await tryFetch('/api/events/publish', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
event: 'inviteDeleted',
|
|
data: { code: invite.code, guildId }
|
|
})
|
|
});
|
|
} catch (sseErr) {
|
|
console.error('Failed to publish SSE event for reconciled invite deletion:', sseErr);
|
|
}
|
|
}
|
|
|
|
if (deletedInvites.length > 0) {
|
|
console.log(`✅ Reconciled ${deletedInvites.length} deleted invites for guild ${guildId}`);
|
|
}
|
|
|
|
return deletedInvites.length;
|
|
} catch (e) {
|
|
console.error(`Failed to reconcile invites for guild ${guildId}:`, e && e.message ? e.message : e);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
module.exports = { getServerSettings, upsertServerSettings, getCommands, toggleCommand, listInvites, addInvite, deleteInvite, listReactionRoles, updateReactionRole, deleteReactionRole, getTwitchUsers, addTwitchUser, deleteTwitchUser, tryFetchTwitchStreams, _rawGetTwitchStreams, getKickUsers, addKickUser, deleteKickUser, getWelcomeLeaveSettings, getAutoroleSettings, reconcileInvites };
|