// Twitch-/YouTube-Benachrichtigungen: pollt alle 5 min und postet in den // Social-Kanal. YouTube läuft ohne Key (RSS), Twitch braucht App-Credentials // (dev.twitch.tv → App registrieren → Client-ID + Secret im System-Tab). import { EmbedBuilder } from 'discord.js'; import { getSetting, setSetting } from '../db.js'; import { socialAnnounceChannelId, youtubeChannelId, twitchChannel, twitchClientId, twitchClientSecret, brandColor, brandColor2, brandFooter, } from '../runtime-settings.js'; import { moduleEnabled } from '../modules.js'; import { everyTuned } from '../tuning.js'; let twitchToken = null; // { token, expiresAt } async function announce(client, embed) { const channelId = socialAnnounceChannelId(); if (!channelId) return; const channel = await client.channels.fetch(channelId).catch(() => null); if (channel?.isTextBased()) await channel.send({ embeds: [embed] }).catch(() => {}); } /* ── YouTube (RSS, ohne API-Key) ───────────────────── */ async function checkYouTube(client) { if (!moduleEnabled('social')) return; const channelId = youtubeChannelId(); if (!channelId) return; const res = await fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`, { signal: AbortSignal.timeout(10_000), }); if (!res.ok) return; const xml = await res.text(); const videoId = xml.match(/([^<]+)<\/yt:videoId>/)?.[1]; const title = xml.match(/[\s\S]*?([^<]+)<\/title>/)?.[1]; if (!videoId) return; const last = getSetting('yt_last_video'); if (last === videoId) return; setSetting('yt_last_video', videoId); if (last === null) return; // Erststart: nur merken, nicht alte Videos posten await announce(client, new EmbedBuilder() .setColor(brandColor2()) .setTitle(`▶️ Neues Video: ${title ?? 'YouTube'}`) .setURL(`https://www.youtube.com/watch?v=${videoId}`) .setImage(`https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`) .setFooter({ text: brandFooter('YOUTUBE') })); console.log(`[social] Neues YouTube-Video angekündigt (${videoId})`); } /* ── Twitch (Helix, Client-Credentials) ────────────── */ async function getTwitchToken() { if (twitchToken && Date.now() < twitchToken.expiresAt) return twitchToken.token; const res = await fetch('https://id.twitch.tv/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: twitchClientId(), client_secret: twitchClientSecret(), grant_type: 'client_credentials', }), }); if (!res.ok) throw new Error(`Twitch-Token ${res.status}`); const data = await res.json(); twitchToken = { token: data.access_token, expiresAt: Date.now() + (data.expires_in - 60) * 1000 }; return twitchToken.token; } async function checkTwitch(client) { if (!moduleEnabled('social')) return; const login = twitchChannel(); if (!login || !twitchClientId() || !twitchClientSecret()) return; const token = await getTwitchToken(); const res = await fetch(`https://api.twitch.tv/helix/streams?user_login=${encodeURIComponent(login)}`, { headers: { 'Client-ID': twitchClientId(), Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(10_000), }); if (!res.ok) return; const stream = (await res.json()).data?.[0] ?? null; const wasLive = getSetting('twitch_live') === '1'; if (stream && !wasLive) { setSetting('twitch_live', '1'); await announce(client, new EmbedBuilder() .setColor(0x9146ff) .setTitle(`🔴 ${login} ist LIVE: ${stream.title ?? ''}`.slice(0, 250)) .setURL(`https://twitch.tv/${login}`) .setDescription(stream.game_name ? `Spielt **${stream.game_name}**` : null) .setImage(stream.thumbnail_url?.replace('{width}', '1280').replace('{height}', '720') ?? null) .setFooter({ text: brandFooter('TWITCH') })); console.log('[social] Twitch-Live angekündigt'); } else if (!stream && wasLive) { setSetting('twitch_live', '0'); } } export function startSocialNotify(client) { const tick = async () => { try { await checkYouTube(client); } catch (e) { console.error('[social] YouTube:', e.message); } try { await checkTwitch(client); } catch (e) { console.error('[social] Twitch:', e.message); } }; everyTuned('social_interval', 'minutes', tick, 'social'); }