Die Fernbedienung: Radio vom Panel aus steuern
Neun Endpunkte unter /api/radio: Stand, Steuern, Warteschlange (einreihen, entfernen, leeren, sortieren), Suche und der yt-dlp-Block. Alles geht durch dieselben Funktionen wie die Discord-Tafeln. Eine zweite Abspiellogik neben radio.js waere eine zweite Gelegenheit, sich anders zu verhalten als die erste. Start, Stopp, Skip, Sender und Lautstaerke liegen als Aktionen auf einem Endpunkt statt auf fuenf: alle brauchen dieselbe Server-Aufloesung, dieselbe Rechtepruefung und antworten mit demselben Stand. Fuenf Geruest- Kopien waeren fuenf Gelegenheiten, eine davon zu vergessen. Ein Suchbegriff wird auch hier nicht geraten -- die Treffer gehen zurueck, gewaehlt wird im Panel. Genau wie in Discord. Die cookies.txt bekommt einen eigenen Endpunkt statt eines Feldes in den Einstellungen: der Inhalt ist mehrzeilig und enthaelt Sitzungsschluessel eines echten Kontos. Er gehoert in eine Datei mit 0600 neben die Datenbank, und zurueck kommt nie der Inhalt, nur Groesse und Datum. Nur der Owner darf ihn setzen, nicht das Team. listVoiceChannels() prueft die Rechte mit: ein Kanal, den das Panel anbietet und in den der Bot dann nicht darf, sieht aus wie ein kaputter Knopf. Beim Schreiben aufgefallen und behoben: radioState() wurde benutzt, aber nicht importiert -- das haette beim ersten Senderwechsel aus dem Panel geknallt. Der Syntaxcheck sieht so etwas nicht, das Laden des Moduls schon. Geprueft mit echtem Fastify, signiertem Sitzungs-Cookie und gefaelschtem Discord-Client: 24 Faelle, davon acht Gegenproben, die fehlschlagen muessen (ohne Anmeldung 401, unbekannte Aktion, Sender ohne ID, Skip ohne Kanal, leere Eingabe, kaputte Adresse, Sortieren ohne Angabe, Cookies ohne Netscape-Kennzeile). Alle 24 wie erwartet.
This commit is contained in:
+326
-1
@@ -29,6 +29,8 @@ import {
|
|||||||
savePage, getPage, listPages, listPublishedPages, deletePage,
|
savePage, getPage, listPages, listPublishedPages, deletePage,
|
||||||
lsState, lsModSizes, setLsModSize,
|
lsState, lsModSizes, setLsModSize,
|
||||||
listStations, createStation, updateStation, deleteStation,
|
listStations, createStation, updateStation, deleteStation,
|
||||||
|
queueList, queueRemove, queueClear, queueReorder, queueVor, queueStand,
|
||||||
|
setRadioState, clearRadioState, radioState,
|
||||||
} from '../db.js';
|
} from '../db.js';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import { moduleStates, moduleDetail, setModuleEnabled, moduleEnabled, moduleFavorites, setModuleFavorites, moduleSettingKeys, MODULE_GROUPS, MODULES, MAX_FAVORITES } from '../modules.js';
|
import { moduleStates, moduleDetail, setModuleEnabled, moduleEnabled, moduleFavorites, setModuleFavorites, moduleSettingKeys, MODULE_GROUPS, MODULES, MAX_FAVORITES } from '../modules.js';
|
||||||
@@ -36,7 +38,15 @@ import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js';
|
|||||||
import { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js';
|
import { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js';
|
||||||
import { lastResults, GAME_ICONS, spielName, spielListe, zugangStand } from '../bot/server-monitor.js';
|
import { lastResults, GAME_ICONS, spielName, spielListe, zugangStand } from '../bot/server-monitor.js';
|
||||||
import { istLsServer, modBasis } from '../bot/ls-farm.js';
|
import { istLsServer, modBasis } from '../bot/ls-farm.js';
|
||||||
import { titelHolen } from '../bot/radio.js';
|
import {
|
||||||
|
titelHolen, laeuft, spielen, stoppen, weiter, radioQuelle,
|
||||||
|
lautstaerkeSetzen, ffmpegVorhanden, tafelnMelden, titelMerken,
|
||||||
|
} from '../bot/radio.js';
|
||||||
|
import { einreihen, eintraegeAufnehmen, ggfAnwerfen } from '../bot/radio-musik.js';
|
||||||
|
import {
|
||||||
|
ytdlpVorhanden, ytdlpFassung, cookiesStand, cookiesSchreiben,
|
||||||
|
selbstAktualisieren, letzterFehler, argumenteVorschau, aufloesen, TREFFER,
|
||||||
|
} from '../bot/youtube.js';
|
||||||
import { watchdogState, watchedServices } from '../bot/watchdog.js';
|
import { watchdogState, watchedServices } from '../bot/watchdog.js';
|
||||||
import { listRules, setRuleEnabled } from '../bot/automod.js';
|
import { listRules, setRuleEnabled } from '../bot/automod.js';
|
||||||
import {
|
import {
|
||||||
@@ -51,6 +61,9 @@ import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-men
|
|||||||
import { computeNextRun } from './scheduled-posts.js';
|
import { computeNextRun } from './scheduled-posts.js';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { removeDevlog } from '../bot/devlog-archive.js';
|
import { removeDevlog } from '../bot/devlog-archive.js';
|
||||||
|
import {
|
||||||
|
radioLautstaerke, LAUT_MIN, LAUT_MAX, LAUT_SCHRITT, youtubeAn,
|
||||||
|
} from '../runtime-settings.js';
|
||||||
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo, brandColor, brandFooter, brandName, giteaApiToken, votingChannelId, discordGuildId, discordInviteUrl, legalInfo, hubUrl, botUrl, welcomeCard, playtesterRoleId } from '../runtime-settings.js';
|
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo, brandColor, brandFooter, brandName, giteaApiToken, votingChannelId, discordGuildId, discordInviteUrl, legalInfo, hubUrl, botUrl, welcomeCard, playtesterRoleId } from '../runtime-settings.js';
|
||||||
import { xpForLevel } from '../bot/levels.js';
|
import { xpForLevel } from '../bot/levels.js';
|
||||||
import { getMilestones } from '../gitea-api.js';
|
import { getMilestones } from '../gitea-api.js';
|
||||||
@@ -418,6 +431,291 @@ export function registerApiRoutes(app, client) {
|
|||||||
return { erreichbar: sender != null || titel != null, sender, titel };
|
return { erreichbar: sender != null || titel != null, sender, titel };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Radio-Fernbedienung ---
|
||||||
|
//
|
||||||
|
// Was die Discord-Tafeln koennen, kann das Panel auch — und zwar ueber
|
||||||
|
// dieselben Funktionen. Eine zweite Abspiellogik neben `radio.js` waere
|
||||||
|
// eine zweite Gelegenheit, sich anders zu verhalten als die erste.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Welcher Server ist gemeint?
|
||||||
|
*
|
||||||
|
* Der Bot kann in mehreren stehen; das Panel hat aber nur eine Ansicht.
|
||||||
|
* Reihenfolge: ausdruecklich mitgegeben, sonst der eingestellte, sonst —
|
||||||
|
* wenn es nur einen gibt — eben der. Bei mehreren ohne Einstellung wird
|
||||||
|
* *nicht* geraten, sondern gesagt, dass die Angabe fehlt.
|
||||||
|
*/
|
||||||
|
function radioGuild(request) {
|
||||||
|
const gewuenscht = String(request.body?.guildId ?? request.query?.guildId ?? '').trim();
|
||||||
|
if (gewuenscht) return client.guilds.cache.get(gewuenscht) ?? null;
|
||||||
|
const eingestellt = discordGuildId();
|
||||||
|
if (eingestellt) return client.guilds.cache.get(eingestellt) ?? null;
|
||||||
|
return client.guilds.cache.size === 1 ? [...client.guilds.cache.values()][0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Der laufende Stand, so wie ihn das Panel braucht */
|
||||||
|
function radioStand(guild) {
|
||||||
|
const jetzt = guild ? laeuft(guild.id) : null;
|
||||||
|
if (!jetzt) return null;
|
||||||
|
const kanal = guild.channels.cache.get(jetzt.channelId);
|
||||||
|
const heim = jetzt.heimStationId ? getStation(jetzt.heimStationId) : null;
|
||||||
|
const t = jetzt.quelle?.art === 'youtube' ? jetzt.quelle.titel : null;
|
||||||
|
return {
|
||||||
|
art: jetzt.quelle?.art ?? null,
|
||||||
|
titel: t ? t.titel : (jetzt.titel ?? null),
|
||||||
|
sender: jetzt.quelle?.art === 'radio' ? jetzt.quelle.station.name : null,
|
||||||
|
url: t?.url ?? null,
|
||||||
|
thumb: t?.thumb ?? null,
|
||||||
|
kanal: t?.kanal ?? null,
|
||||||
|
// Sekunden statt eines Zeitstempels: der Browser muesste sonst
|
||||||
|
// gegen die Uhr des Servers rechnen, und die beiden gehen selten
|
||||||
|
// gleich.
|
||||||
|
verstrichen: Math.floor((Date.now() - jetzt.seit) / 1000),
|
||||||
|
dauer: t ? t.dauer : null,
|
||||||
|
kanalId: jetzt.channelId,
|
||||||
|
kanalName: kanal?.name ?? null,
|
||||||
|
tonLaeuft: jetzt.tonLaeuft,
|
||||||
|
heimStation: heim ? { id: heim.id, name: heim.name } : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/api/radio/status', async (request, reply) => {
|
||||||
|
if (requireScope(request, reply, 'settings')) return;
|
||||||
|
const guild = radioGuild(request);
|
||||||
|
const guildId = guild?.id ?? null;
|
||||||
|
return {
|
||||||
|
guildId,
|
||||||
|
guildName: guild?.name ?? null,
|
||||||
|
// Mehrere Server und keine Einstellung: dann fehlt die Angabe,
|
||||||
|
// und das steht hier statt eines stillen leeren Standes.
|
||||||
|
mehrdeutig: !guild && client.guilds.cache.size > 1,
|
||||||
|
laeuft: radioStand(guild),
|
||||||
|
lautstaerke: radioLautstaerke(),
|
||||||
|
grenzen: { min: LAUT_MIN, max: LAUT_MAX, schritt: LAUT_SCHRITT },
|
||||||
|
warteschlange: guildId ? queueList(guildId) : [],
|
||||||
|
stand: guildId ? queueStand(guildId) : { n: 0, dauer: 0 },
|
||||||
|
sprachkanaele: listVoiceChannels(),
|
||||||
|
werkzeuge: {
|
||||||
|
ffmpeg: ffmpegVorhanden(),
|
||||||
|
ytdlp: ytdlpVorhanden(),
|
||||||
|
ytdlpFassung: ytdlpFassung(),
|
||||||
|
youtubeAn: youtubeAn(),
|
||||||
|
cookies: cookiesStand(),
|
||||||
|
argumente: argumenteVorschau(),
|
||||||
|
letzterFehler: letzterFehler(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start, Stopp, Ueberspringen, Sender, Lautstaerke.
|
||||||
|
*
|
||||||
|
* Ein Endpunkt mit einer Aktion statt fuenf Endpunkten: alle brauchen
|
||||||
|
* dieselbe Server-Aufloesung, dieselbe Rechtepruefung und antworten mit
|
||||||
|
* demselben Stand. Fuenfmal dasselbe Gerüst waere fuenfmal die Gelegenheit,
|
||||||
|
* eines davon zu vergessen.
|
||||||
|
*/
|
||||||
|
app.post('/api/radio/steuern', async (request, reply) => {
|
||||||
|
if (requireScope(request, reply, 'settings')) return;
|
||||||
|
const guild = radioGuild(request);
|
||||||
|
if (!guild) return reply.code(400).send({ error: 'Kein Server gewaehlt.' });
|
||||||
|
const guildId = guild.id;
|
||||||
|
const b = request.body ?? {};
|
||||||
|
const aktion = String(b.aktion ?? '');
|
||||||
|
|
||||||
|
/** Der Zielkanal: mitgegeben, sonst der laufende */
|
||||||
|
const zielId = () => String(b.kanalId ?? '') || laeuft(guildId)?.channelId || null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (aktion === 'stopp') {
|
||||||
|
stoppen(guildId);
|
||||||
|
clearRadioState(guildId);
|
||||||
|
} else if (aktion === 'laut') {
|
||||||
|
const wert = lautstaerkeSetzen(guildId, Number(b.wert));
|
||||||
|
// Frueh zurueck, und zwar an der Protokollierung vorbei: am
|
||||||
|
// Schieber entstehen viele Werte hintereinander, und ein
|
||||||
|
// Protokoll, das zu neunzig Prozent aus Lautstaerke besteht,
|
||||||
|
// ist keines mehr. Zurueck kommt der tatsaechlich gesetzte
|
||||||
|
// Wert, nicht der gewuenschte — wer am Anschlag dreht, soll
|
||||||
|
// das am Ergebnis sehen.
|
||||||
|
await tafelnMelden(client, guildId);
|
||||||
|
return { ok: true, lautstaerke: wert, laeuft: radioStand(guild) };
|
||||||
|
} else if (aktion === 'skip') {
|
||||||
|
const kanalId = zielId();
|
||||||
|
if (!kanalId) return reply.code(400).send({ error: 'Kein Sprachkanal.' });
|
||||||
|
await weiter(client, guildId, { erzwungen: true, kanalId });
|
||||||
|
} else if (aktion === 'sender' || aktion === 'heim') {
|
||||||
|
const stationId = aktion === 'heim'
|
||||||
|
? laeuft(guildId)?.heimStationId
|
||||||
|
: Number(b.stationId);
|
||||||
|
const station = stationId ? getStation(Number(stationId)) : null;
|
||||||
|
if (!station) return reply.code(400).send({ error: 'Sender nicht gefunden.' });
|
||||||
|
const kanalId = zielId();
|
||||||
|
if (!kanalId) return reply.code(400).send({ error: 'Kein Sprachkanal gewaehlt.' });
|
||||||
|
const kanal = await client.channels.fetch(kanalId).catch(() => null);
|
||||||
|
if (!kanal?.isVoiceBased()) return reply.code(400).send({ error: 'Das ist kein Sprachkanal.' });
|
||||||
|
await spielen(kanal, radioQuelle(station));
|
||||||
|
// Ohne diesen Merker kaeme der Bot nach einem Neustart nicht
|
||||||
|
// zurueck — und die Radio-Tafel wuesste nicht, was laeuft.
|
||||||
|
const vorhanden = radioState(guildId);
|
||||||
|
setRadioState({
|
||||||
|
guildId,
|
||||||
|
channelId: kanalId,
|
||||||
|
stationId: station.id,
|
||||||
|
panelChannelId: vorhanden?.panel_channel_id,
|
||||||
|
panelMessageId: vorhanden?.panel_message_id,
|
||||||
|
});
|
||||||
|
// Den Titel gleich mitholen: `titelMerken` schreibt ihn auch
|
||||||
|
// in die Statuszeile am Sprachkanal, sonst steht dort bis zum
|
||||||
|
// naechsten Abgleich der Titel des vorigen Senders.
|
||||||
|
const { titel } = await titelHolen(station.url);
|
||||||
|
titelMerken(client, guildId, titel);
|
||||||
|
} else {
|
||||||
|
return reply.code(400).send({ error: `Unbekannte Aktion: ${aktion}` });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ein gescheiterter Beitritt bringt die Diagnose aus `radio.js`
|
||||||
|
// mit — die ist hier deutlich mehr wert als ein 500er.
|
||||||
|
return reply.code(400).send({ error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
logAudit(getSessionUser(request), 'radio gesteuert', aktion);
|
||||||
|
await tafelnMelden(client, guildId);
|
||||||
|
return { ok: true, laeuft: radioStand(guild), warteschlange: queueList(guildId) };
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Warteschlange ---
|
||||||
|
|
||||||
|
app.post('/api/radio/warteschlange', async (request, reply) => {
|
||||||
|
if (requireScope(request, reply, 'settings')) return;
|
||||||
|
const guild = radioGuild(request);
|
||||||
|
if (!guild) return reply.code(400).send({ error: 'Kein Server gewaehlt.' });
|
||||||
|
const guildId = guild.id;
|
||||||
|
const b = request.body ?? {};
|
||||||
|
const wunschVon = getSessionUser(request)?.username ?? null;
|
||||||
|
|
||||||
|
// Zwei Wege hier hinein: eine Eingabe (Adresse oder Suchbegriff) oder
|
||||||
|
// ein fertiger Treffer, den das Panel vorher ausgewaehlt hat.
|
||||||
|
let eingereiht;
|
||||||
|
if (b.treffer) {
|
||||||
|
eingereiht = eintraegeAufnehmen(guildId, [{
|
||||||
|
video_id: String(b.treffer.video_id ?? ''),
|
||||||
|
url: String(b.treffer.url ?? ''),
|
||||||
|
titel: String(b.treffer.titel ?? ''),
|
||||||
|
dauer: b.treffer.dauer ?? null,
|
||||||
|
kanal: b.treffer.kanal ?? null,
|
||||||
|
thumb: b.treffer.thumb ?? null,
|
||||||
|
}], wunschVon);
|
||||||
|
} else {
|
||||||
|
const r = await einreihen(guildId, String(b.eingabe ?? ''), { wunschVon });
|
||||||
|
if (r.fehler) return reply.code(400).send({ error: r.fehler });
|
||||||
|
// Ein Suchbegriff wird nicht geraten: die Treffer gehen zurueck,
|
||||||
|
// gewaehlt wird im Panel. Genau wie in Discord.
|
||||||
|
if (r.treffer) return { treffer: r.treffer, suche: true };
|
||||||
|
eingereiht = r.eingereiht;
|
||||||
|
}
|
||||||
|
|
||||||
|
const angeworfen = await ggfAnwerfen(client, guildId,
|
||||||
|
String(b.kanalId ?? '') || null);
|
||||||
|
logAudit(getSessionUser(request), 'radio eingereiht',
|
||||||
|
eingereiht.map((e) => e.titel).join(', ').slice(0, 200));
|
||||||
|
await tafelnMelden(client, guildId);
|
||||||
|
return {
|
||||||
|
ok: true, eingereiht, angeworfen,
|
||||||
|
warteschlange: queueList(guildId), laeuft: radioStand(guild),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/radio/suche', async (request, reply) => {
|
||||||
|
if (requireScope(request, reply, 'settings')) return;
|
||||||
|
const text = String(request.body?.text ?? '').trim();
|
||||||
|
if (!text) return reply.code(400).send({ error: 'Nichts eingegeben.' });
|
||||||
|
const r = await aufloesen(text, { max: TREFFER });
|
||||||
|
if (r.fehler) return reply.code(400).send({ error: r.fehler });
|
||||||
|
return { treffer: r.treffer, suche: r.suche };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/radio/warteschlange/:id', async (request, reply) => {
|
||||||
|
if (requireScope(request, reply, 'settings')) return;
|
||||||
|
const guild = radioGuild(request);
|
||||||
|
if (!guild) return reply.code(400).send({ error: 'Kein Server gewaehlt.' });
|
||||||
|
const weg = queueRemove(Number(request.params.id));
|
||||||
|
await tafelnMelden(client, guild.id);
|
||||||
|
return { deleted: weg, warteschlange: queueList(guild.id) };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/radio/warteschlange', async (request, reply) => {
|
||||||
|
if (requireScope(request, reply, 'settings')) return;
|
||||||
|
const guild = radioGuild(request);
|
||||||
|
if (!guild) return reply.code(400).send({ error: 'Kein Server gewaehlt.' });
|
||||||
|
const n = queueClear(guild.id);
|
||||||
|
logAudit(getSessionUser(request), 'warteschlange geleert', `${n} Titel`);
|
||||||
|
await tafelnMelden(client, guild.id);
|
||||||
|
return { geleert: n, warteschlange: [] };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/radio/warteschlange/sortieren', async (request, reply) => {
|
||||||
|
if (requireScope(request, reply, 'settings')) return;
|
||||||
|
const guild = radioGuild(request);
|
||||||
|
if (!guild) return reply.code(400).send({ error: 'Kein Server gewaehlt.' });
|
||||||
|
const b = request.body ?? {};
|
||||||
|
// Zwei Formen, weil das Panel umsortiert und Discord nur „nach vorn"
|
||||||
|
// kennt — beide landen in derselben Pruefung in der Datenbank, die
|
||||||
|
// fremde IDs nicht anfasst.
|
||||||
|
if (b.vor) queueVor(guild.id, Number(b.vor));
|
||||||
|
else if (Array.isArray(b.ids)) queueReorder(guild.id, b.ids);
|
||||||
|
else return reply.code(400).send({ error: 'Weder `ids` noch `vor` mitgegeben.' });
|
||||||
|
await tafelnMelden(client, guild.id);
|
||||||
|
return { ok: true, warteschlange: queueList(guild.id) };
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- yt-dlp ---
|
||||||
|
|
||||||
|
app.get('/api/radio/youtube', async (request, reply) => {
|
||||||
|
if (requireScope(request, reply, 'settings')) return;
|
||||||
|
return {
|
||||||
|
ytdlp: ytdlpVorhanden(),
|
||||||
|
fassung: ytdlpFassung(),
|
||||||
|
an: youtubeAn(),
|
||||||
|
cookies: cookiesStand(),
|
||||||
|
argumente: argumenteVorschau(),
|
||||||
|
letzterFehler: letzterFehler(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die `cookies.txt` setzen oder loeschen.
|
||||||
|
*
|
||||||
|
* Eigener Endpunkt statt eines Feldes in den Einstellungen: der Inhalt ist
|
||||||
|
* mehrzeilig und enthaelt Sitzungsschluessel eines echten Kontos. Er
|
||||||
|
* gehoert in eine Datei mit 0600 neben die Datenbank und nicht in eine
|
||||||
|
* Tabelle, die das Panel andernorts auch anzeigt.
|
||||||
|
*
|
||||||
|
* Zurueck kommt nie der Inhalt, nur Groesse und Datum.
|
||||||
|
*/
|
||||||
|
app.put('/api/radio/youtube/cookies', async (request, reply) => {
|
||||||
|
if (requireAdmin(request, reply)) return;
|
||||||
|
try {
|
||||||
|
const r = await cookiesSchreiben(request.body?.inhalt ?? '');
|
||||||
|
logAudit(getSessionUser(request), 'yt-cookies',
|
||||||
|
r.gesetzt ? 'gesetzt' : 'geloescht');
|
||||||
|
return { ok: true, cookies: cookiesStand() };
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(400).send({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/radio/youtube/update', async (request, reply) => {
|
||||||
|
if (requireAdmin(request, reply)) return;
|
||||||
|
try {
|
||||||
|
const r = await selbstAktualisieren();
|
||||||
|
logAudit(getSessionUser(request), 'yt-dlp aktualisiert', r.fassung ?? '?');
|
||||||
|
return { ok: true, ...r };
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(400).send({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Ticket-Anliegen. Ohne Eintrag bleibt es beim einen Knopf — deshalb ist
|
// Ticket-Anliegen. Ohne Eintrag bleibt es beim einen Knopf — deshalb ist
|
||||||
// eine leere Liste kein Fehler, sondern der Normalfall fuer kleine Server.
|
// eine leere Liste kein Fehler, sondern der Normalfall fuer kleine Server.
|
||||||
app.get('/api/ticket-categories', async (request, reply) => {
|
app.get('/api/ticket-categories', async (request, reply) => {
|
||||||
@@ -938,6 +1236,33 @@ ${rssItems}
|
|||||||
return channels.sort((a, b) => a.name.localeCompare(b.name));
|
return channels.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprachkanäle, in die der Bot auch wirklich hineinkommt.
|
||||||
|
*
|
||||||
|
* Bewusst mit Rechteprüfung und nicht bloss nach Kanaltyp: ein Kanal, den
|
||||||
|
* das Panel anbietet und in den der Bot dann nicht darf, sieht aus wie ein
|
||||||
|
* kaputter Knopf. Was hier fehlt, fehlt aus einem Grund — deshalb steht
|
||||||
|
* der auch dabei.
|
||||||
|
*/
|
||||||
|
function listVoiceChannels() {
|
||||||
|
const channels = [];
|
||||||
|
for (const guild of client.guilds.cache.values()) {
|
||||||
|
for (const ch of guild.channels.cache.values()) {
|
||||||
|
if (!ch.isVoiceBased?.() || ch.viewable === false) continue;
|
||||||
|
const rechte = guild.members.me ? ch.permissionsFor(guild.members.me) : null;
|
||||||
|
channels.push({
|
||||||
|
id: ch.id,
|
||||||
|
name: ch.name,
|
||||||
|
guild: guild.name,
|
||||||
|
guildId: guild.id,
|
||||||
|
besetzt: ch.members?.filter?.((m) => !m.user.bot).size ?? 0,
|
||||||
|
darfRein: Boolean(rechte?.has('Connect') && rechte?.has('Speak')),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return channels.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
/** Rollen, die der Bot vergeben kann (unter seiner höchsten Rolle, nicht managed) */
|
/** Rollen, die der Bot vergeben kann (unter seiner höchsten Rolle, nicht managed) */
|
||||||
function listRoles() {
|
function listRoles() {
|
||||||
const roles = [];
|
const roles = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user