Community-Endgame: Alpha-Keys, Bewerbungen, Events, Stats, Triggers, /remind, Social, Temp-Voice + MEE6-Bot-Karte
- Alpha-Keys: Pool im Community-Tab, Ein-Klick-Verteilung per DM an alle Playtester (Key bleibt frei, wenn die DM geblockt wird) - Bewerbungs-Formulare: Builder im neuen Bewerbungen-Tab (bis 5 Fragen), Button → Discord-Modal → Review-Embed mit ✅/❌, Rolle + DM bei Entscheidung - Events: GuildScheduledEventCreate → Announce-Embed; öffentliche /events-Seite aus den Discord-Events (5-min-Cache) - Server-Stats: activity_daily (Nachrichten/Joins/Leaves) → Balken-Chart auf /level - Triggers (Auto-Antworten, 30s-Cooldown), /remind (DM-Scheduler), Twitch-Live (Helix, App-Creds write-only) + YouTube-RSS-Announcements, Temp-Voice (Join to Create, Cleanup bei Leerstand + Start) - Brand-Tab: MEE6-Style Bot-Identity-Karte (Avatar-Vorschau mit Status-Dot, Bot-Name via setUsername, Presence online/idle/dnd, Aktivität) - Neue Intents: GuildVoiceStates, GuildScheduledEvents; alles smoke-getestet Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// 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';
|
||||
|
||||
const CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
||||
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) {
|
||||
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) {
|
||||
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); }
|
||||
};
|
||||
setInterval(tick, CHECK_INTERVAL_MS);
|
||||
}
|
||||
Reference in New Issue
Block a user