Update backend, DB, Commands, Live Reloading
This commit is contained in:
@@ -1,4 +1,62 @@
|
||||
const api = require('./api');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
// Twitch API credentials (optional). If provided, we'll enrich embeds with user bio.
|
||||
const twitchClientId = process.env.TWITCH_CLIENT_ID || null;
|
||||
const twitchClientSecret = process.env.TWITCH_CLIENT_SECRET || null;
|
||||
let twitchAppToken = null; // cached app access token
|
||||
let twitchTokenExpires = 0;
|
||||
|
||||
// Cache of user login -> { description, profile_image_url, fetchedAt }
|
||||
const userInfoCache = new Map();
|
||||
|
||||
async function getAppToken() {
|
||||
if (!twitchClientId || !twitchClientSecret) return null;
|
||||
const now = Date.now();
|
||||
if (twitchAppToken && now < twitchTokenExpires - 60000) { // refresh 1 min early
|
||||
return twitchAppToken;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`https://id.twitch.tv/oauth2/token?client_id=${twitchClientId}&client_secret=${twitchClientSecret}&grant_type=client_credentials`, { method: 'POST' });
|
||||
const json = await res.json();
|
||||
twitchAppToken = json.access_token;
|
||||
twitchTokenExpires = now + (json.expires_in * 1000);
|
||||
return twitchAppToken;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch Twitch app token:', e && e.message ? e.message : e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserInfo(login) {
|
||||
if (!login) return null;
|
||||
const lower = login.toLowerCase();
|
||||
const cached = userInfoCache.get(lower);
|
||||
const now = Date.now();
|
||||
if (cached && now - cached.fetchedAt < 1000 * 60 * 30) { // 30 min cache
|
||||
return cached;
|
||||
}
|
||||
const token = await getAppToken();
|
||||
if (!token) return null;
|
||||
try {
|
||||
const res = await fetch(`https://api.twitch.tv/helix/users?login=${encodeURIComponent(lower)}`, {
|
||||
headers: {
|
||||
'Client-ID': twitchClientId,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const json = await res.json();
|
||||
const data = (json.data && json.data[0]) || null;
|
||||
if (data) {
|
||||
const info = { description: data.description || '', profile_image_url: data.profile_image_url || '', fetchedAt: now };
|
||||
userInfoCache.set(lower, info);
|
||||
return info;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch Twitch user info for', lower, e && e.message ? e.message : e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let polling = false;
|
||||
const pollIntervalMs = Number(process.env.TWITCH_POLL_INTERVAL_MS || 5000); // 5s default
|
||||
@@ -45,6 +103,10 @@ async function checkGuild(client, guild) {
|
||||
let channel = null;
|
||||
try {
|
||||
channel = await client.channels.fetch(channelId);
|
||||
if (channel.type !== 0) { // 0 is text channel
|
||||
console.error(`TwitchWatcher: channel ${channelId} is not a text channel (type: ${channel.type})`);
|
||||
channel = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`TwitchWatcher: failed to fetch channel ${channelId}:`, e && e.message ? e.message : e);
|
||||
channel = null;
|
||||
@@ -73,10 +135,16 @@ async function checkGuild(client, guild) {
|
||||
// mark announced for this session
|
||||
announced.set(key, { started_at: s.started_at || new Date().toISOString() });
|
||||
|
||||
// Build and send embed
|
||||
// Build and send embed (standardized layout)
|
||||
try {
|
||||
// Announce without per-guild log spam
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
// Attempt to enrich with user bio (description) if available
|
||||
let bio = '';
|
||||
try {
|
||||
const info = await fetchUserInfo(login);
|
||||
if (info && info.description) bio = info.description.slice(0, 200);
|
||||
} catch (_) {}
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x9146FF)
|
||||
.setTitle(s.title || `${s.user_name} is live`)
|
||||
@@ -87,10 +155,21 @@ async function checkGuild(client, guild) {
|
||||
{ name: 'Category', value: s.game_name || 'Unknown', inline: true },
|
||||
{ name: 'Viewers', value: String(s.viewer_count || 0), inline: true }
|
||||
)
|
||||
.setDescription(s.description || '')
|
||||
.setDescription(bio || (s.description || '').slice(0, 200))
|
||||
.setFooter({ text: `ehchadservices • Started: ${s.started_at ? new Date(s.started_at).toLocaleString() : 'unknown'}` });
|
||||
|
||||
await channel.send({ embeds: [embed] });
|
||||
// Determine message text (custom overrides default). Provide a plain text prefix if available.
|
||||
let prefixMsg = '';
|
||||
if (liveSettings.customMessage) {
|
||||
prefixMsg = liveSettings.customMessage;
|
||||
} else if (liveSettings.message) {
|
||||
prefixMsg = liveSettings.message;
|
||||
} else {
|
||||
prefixMsg = `🔴 ${s.user_name} is now live!`;
|
||||
}
|
||||
// Ensure we always hyperlink the title via embed; prefix is optional add above embed
|
||||
const payload = prefixMsg ? { content: prefixMsg, embeds: [embed] } : { embeds: [embed] };
|
||||
await channel.send(payload);
|
||||
console.log(`🔔 Announced live: ${login} - ${(s.title || '').slice(0, 80)}`);
|
||||
} catch (e) {
|
||||
console.error(`TwitchWatcher: failed to send announcement for ${login}:`, e && e.message ? e.message : e);
|
||||
@@ -108,6 +187,15 @@ async function poll(client) {
|
||||
if (polling) return;
|
||||
polling = true;
|
||||
console.log(`🔁 TwitchWatcher started, polling every ${Math.round(pollIntervalMs/1000)}s`);
|
||||
// Initial check on restart: send messages for currently live users
|
||||
try {
|
||||
const guilds = Array.from(client.guilds.cache.values());
|
||||
for (const g of guilds) {
|
||||
await checkGuild(client, g).catch(err => { console.error('TwitchWatcher: initial checkGuild error', err && err.message ? err.message : err); });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error during initial twitch check:', e && e.message ? e.message : e);
|
||||
}
|
||||
while (polling) {
|
||||
try {
|
||||
const guilds = Array.from(client.guilds.cache.values());
|
||||
|
||||
Reference in New Issue
Block a user