Files
d4rkbot/src/bot/social-notify.js
T
D4rkst3randClaude Fable 5 197c2a1ada
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Werte: 19 feste Zahlen im Panel einstellbar
XP-Beträge, Wartezeiten, Prüf-Intervalle, Obergrenzen und Postzeiten
standen als Konstanten im Code. Wer den Server-Monitor seltener prüfen
lassen wollte oder das Level-System anders austarieren, musste die
Quelldateien anfassen.

src/tuning.js hält sie jetzt an einer Stelle, mit Standard, erlaubtem
Bereich und Erklärung. Der neue Config-Bereich „Werte" zeigt sie nach
Themen gruppiert; neben jedem Feld steht der erlaubte Bereich und der
Standard, angepasste Werte lassen sich einzeln zurücksetzen.

Die Grenzen sind nicht Kosmetik: ein Vertipper beim Prüf-Intervall würde
sonst den Bot in eine Schleife im Sekundentakt schicken. Gespeichert wird
ganz oder gar nicht — ein ungültiger Wert im Formular lässt auch die
gültigen daneben unverändert, statt die Hälfte zu schreiben.

Intervalle laufen nicht mehr über setInterval mit festem Abstand, sondern
über everyTuned: der Wert wird vor jeder Runde neu gelesen. Ein geändertes
Intervall greift damit ab dem nächsten Durchlauf, ohne Neustart. Ein
Fehler in einer Runde beendet die Schleife nicht.

Beim Testen aufgefallen und behoben: Number('') ist 0 und nicht NaN — ein
nie gesetzter Wert wäre dadurch auf sein Minimum gefallen statt auf den
Standard. Das Level-System hätte also 1 XP pro Nachricht vergeben und der
Monitor jede Minute geprüft.

Der Fetch-Wrapper im Frontend hat die Fehlermeldung des Servers verworfen;
jetzt steht im Panel „XP pro Nachricht: 1–500 XP" statt „fehlgeschlagen".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:49:34 +02:00

107 lines
4.5 KiB
JavaScript

// 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>([^<]+)<\/yt:videoId>/)?.[1];
const title = xml.match(/<entry>[\s\S]*?<title>([^<]+)<\/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');
}