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 addInvite(guildId, invite) { const path = `/api/servers/${guildId}/invites`; try { const res = await tryFetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(invite), }); 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; } } 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: '' }; } module.exports = { getServerSettings, upsertServerSettings, getCommands, toggleCommand, listInvites, addInvite, deleteInvite, getTwitchUsers, addTwitchUser, deleteTwitchUser, tryFetchTwitchStreams, _rawGetTwitchStreams, getKickUsers, addKickUser, deleteKickUser, getWelcomeLeaveSettings, getAutoroleSettings };