Radio youtube #2
+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