Pause fuer YouTube -- und der Bot bleibt draussen, wenn er rausgeht
Zwei Befunde aus dem Betrieb. 1. Der Bot ging aus dem leeren Kanal und kam sofort wieder rein. `stoppen()` hielt erst den Spieler an und loeschte dann aus `laufend`. `player.stop()` meldet den Leerlauf aber SYNCHRON -- gemessen mit @discordjs/voice 0.19: der Idle-Melder laeuft mitten in stop(), noch vor der naechsten Zeile. Er fand den Eintrag also noch, hielt das Ende fuer "Titel vorbei" und schaltete weiter: naechster Titel oder zurueck auf den Heimsender. Von aussen genau das gemeldete Bild. Jetzt wird erst geloescht, dann angehalten. Nachgestellt mit einem echten AudioPlayer und beiden Reihenfolgen -- die Kontrolle ist dabei die alte Reihenfolge: sie schaltet nachweislich weiter (1x), die neue nicht (0x). Ohne diese Gegenprobe koennte der Test etwas ganz anderes messen. 2. Im YouTube-Modus fehlte die Pause. `pausieren()` gibt es jetzt, ausdruecklich nur fuer YouTube. Ein Sender laesst sich nicht sinnvoll anhalten: er laeuft beim Betreiber weiter, waehrend hier die Leitung volllaeuft, und beim Fortsetzen kaeme eine Konserve, die immer weiter hinterherhinkt. Fuer einen Sender heisst Pause Stopp -- dafuer gibt es den Stopp-Knopf, und der Pause-Knopf graut aus. Die verstrichene Zeit rechnet Pausen heraus, sonst liefe der Fortschrittsbalken nach dem Fortsetzen um die Pausenlaenge voraus. `verstrichen()` steht an EINER Stelle, die sich Discord-Tafel und Panel teilen -- zwei Rechnungen waeren zwei Gelegenheiten, die Pause zu vergessen. Die Musik-Tafel bekommt eine zweite Knopfreihe: mit der Pause waeren es sechs Knoepfe, Discord laesst fuenf je Reihe zu. Platz ist reichlich (vier von fuenf Reihen), und die Trennung ist ohnehin sinnvoller -- oben, was gerade klingt, unten, was mit der Liste passiert. Nachgezaehlt: 2 Reihen ohne Liste, 4 mit. Beim Quellenwechsel wird ausdruecklich fortgesetzt: der Spieler ueberlebt den Wechsel, und stand er auf Pause, waere der neue Titel stumm gestartet waehrend der Eintrag "laeuft" sagt. Nebenbei: mein Feld-Abgleich hielt eine zweite Kopie der Feldliste von `radioStand()` und driftete beim ersten neuen Feld prompt ab -- er meldete `pausiert` als fehlend, obwohl die API es liefert. Er liest die Form jetzt aus der Quelle, statt sie doppelt zu pflegen.
This commit is contained in:
@@ -2224,6 +2224,7 @@ export default function Settings({ me }) {
|
|||||||
</span>
|
</span>
|
||||||
<div className="radio-now-text">
|
<div className="radio-now-text">
|
||||||
<strong>
|
<strong>
|
||||||
|
{radio.laeuft.pausiert && '⏸️ '}
|
||||||
{radio.laeuft.art === 'youtube'
|
{radio.laeuft.art === 'youtube'
|
||||||
? radio.laeuft.titel
|
? radio.laeuft.titel
|
||||||
: radio.laeuft.sender}
|
: radio.laeuft.sender}
|
||||||
@@ -2321,6 +2322,14 @@ export default function Settings({ me }) {
|
|||||||
<option key={x.id} value={x.id}>{x.emoji || '📻'} {x.name}</option>
|
<option key={x.id} value={x.id}>{x.emoji || '📻'} {x.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{/* Pause gibt es nur bei YouTube: ein Dauerstrom laeuft
|
||||||
|
beim Sender weiter, waehrend hier die Leitung volllaeuft --
|
||||||
|
beim Fortsetzen kaeme eine Konserve, die hinterherhinkt. */}
|
||||||
|
<button className="btn btn-ghost"
|
||||||
|
disabled={radioLaedt || radio.laeuft?.art !== 'youtube'}
|
||||||
|
onClick={() => radioSteuern('pause')}>
|
||||||
|
{radio.laeuft?.pausiert ? '▶️ Weiter' : '⏸️ Pause'}
|
||||||
|
</button>
|
||||||
<button className="btn btn-ghost" disabled={radioLaedt || !radio.laeuft}
|
<button className="btn btn-ghost" disabled={radioLaedt || !radio.laeuft}
|
||||||
onClick={() => radioSteuern('skip')}>
|
onClick={() => radioSteuern('skip')}>
|
||||||
⏭️ Überspringen
|
⏭️ Überspringen
|
||||||
|
|||||||
+32
-9
@@ -28,6 +28,7 @@ import {
|
|||||||
} from '../db.js';
|
} from '../db.js';
|
||||||
import {
|
import {
|
||||||
laeuft, weiter, spielen, beiAenderung, tafelAktualisieren, tafelnMelden, radioQuelle,
|
laeuft, weiter, spielen, beiAenderung, tafelAktualisieren, tafelnMelden, radioQuelle,
|
||||||
|
pausieren, verstrichen,
|
||||||
} from './radio.js';
|
} from './radio.js';
|
||||||
import {
|
import {
|
||||||
aufloesen, ytdlpVorhanden, letzterFehler, TREFFER,
|
aufloesen, ytdlpVorhanden, letzterFehler, TREFFER,
|
||||||
@@ -95,9 +96,10 @@ export function musikEmbed(client, guildId) {
|
|||||||
|
|
||||||
if (jetzt?.quelle?.art === 'youtube') {
|
if (jetzt?.quelle?.art === 'youtube') {
|
||||||
const t = jetzt.quelle.titel;
|
const t = jetzt.quelle.titel;
|
||||||
const verstrichen = Math.floor((Date.now() - jetzt.seit) / 1000);
|
// Pausen abgezogen — sonst behauptet der Balken, ein angehaltener
|
||||||
const balken = fortschritt(verstrichen, t.dauer);
|
// Titel liefe weiter, und läuft beim Fortsetzen voraus.
|
||||||
embed.setDescription(`**${kurz(t.titel, 200)}**\n`
|
const balken = fortschritt(verstrichen(jetzt), t.dauer);
|
||||||
|
embed.setDescription(`${jetzt.pausiertSeit ? '⏸️ ' : ''}**${kurz(t.titel, 200)}**\n`
|
||||||
+ `${t.kanal ? `_${kurz(t.kanal, 80)}_\n` : ''}`
|
+ `${t.kanal ? `_${kurz(t.kanal, 80)}_\n` : ''}`
|
||||||
+ (balken ? `${balken}\n` : '🔴 _Livestream_\n')
|
+ (balken ? `${balken}\n` : '🔴 _Livestream_\n')
|
||||||
+ `in <#${jetzt.channelId}>`);
|
+ `in <#${jetzt.channelId}>`);
|
||||||
@@ -149,7 +151,7 @@ export function musikKomponenten(guildId) {
|
|||||||
const reihen = [];
|
const reihen = [];
|
||||||
|
|
||||||
if (liste.length > 0) {
|
if (liste.length > 0) {
|
||||||
const optionen = (praefix) => liste.map((e, i) => {
|
const optionen = () => liste.map((e, i) => {
|
||||||
const o = new StringSelectMenuOptionBuilder()
|
const o = new StringSelectMenuOptionBuilder()
|
||||||
.setLabel(kurz(`${i + 1}. ${e.titel}`, 100))
|
.setLabel(kurz(`${i + 1}. ${e.titel}`, 100))
|
||||||
.setValue(String(e.id));
|
.setValue(String(e.id));
|
||||||
@@ -171,13 +173,30 @@ export function musikKomponenten(guildId) {
|
|||||||
// an der Radio-Tafel: ein Knopf, der sich drücken lässt und nichts
|
// an der Radio-Tafel: ein Knopf, der sich drücken lässt und nichts
|
||||||
// bewirkt, sieht aus wie ein kaputter Knopf.
|
// bewirkt, sieht aus wie ein kaputter Knopf.
|
||||||
const nichtsDa = liste.length === 0;
|
const nichtsDa = liste.length === 0;
|
||||||
|
const ytLaeuft = jetzt?.quelle?.art === 'youtube';
|
||||||
|
const pausiert = Boolean(jetzt?.pausiertSeit);
|
||||||
|
|
||||||
|
// Zwei Knopfreihen statt einer: mit der Pause wären es sechs Knöpfe, und
|
||||||
|
// Discord lässt nur fünf je Reihe zu. Platz ist da — zwei Auswahlmenüs
|
||||||
|
// plus zwei Knopfreihen sind vier von fünf möglichen Reihen.
|
||||||
|
//
|
||||||
|
// Die Trennung ist ausserdem die sinnvollere: oben, was gerade klingt,
|
||||||
|
// unten, was mit der Liste passiert.
|
||||||
|
reihen.push(new ActionRowBuilder().addComponents(
|
||||||
|
// Pause gibt es nur bei YouTube. Warum, steht bei `pausieren()` in
|
||||||
|
// `radio.js` — kurz: einen Dauerstrom anzuhalten heisst, ihm
|
||||||
|
// hinterherzuhinken.
|
||||||
|
pausiert
|
||||||
|
? knopf('musik_pause', 'Weiter', '▶️', ButtonStyle.Success)
|
||||||
|
: knopf('musik_pause', 'Pause', '⏸️', ButtonStyle.Primary, !ytLaeuft),
|
||||||
|
knopf('musik_skip', 'Überspringen', '⏭️', ButtonStyle.Primary,
|
||||||
|
nichtsDa && !ytLaeuft),
|
||||||
|
knopf('musik_heim', 'Zum Radio', '📻', ButtonStyle.Secondary,
|
||||||
|
!ytLaeuft || !jetzt.heimStationId),
|
||||||
|
));
|
||||||
reihen.push(new ActionRowBuilder().addComponents(
|
reihen.push(new ActionRowBuilder().addComponents(
|
||||||
knopf('musik_add', 'Einreihen…', '▶️', ButtonStyle.Success),
|
knopf('musik_add', 'Einreihen…', '▶️', ButtonStyle.Success),
|
||||||
knopf('musik_skip', 'Überspringen', '⏭️', ButtonStyle.Primary,
|
|
||||||
nichtsDa && jetzt?.quelle?.art !== 'youtube'),
|
|
||||||
knopf('musik_leeren', 'Leeren', '🗑️', ButtonStyle.Danger, nichtsDa),
|
knopf('musik_leeren', 'Leeren', '🗑️', ButtonStyle.Danger, nichtsDa),
|
||||||
knopf('musik_heim', 'Zum Radio', '📻', ButtonStyle.Secondary,
|
|
||||||
!jetzt || jetzt.quelle?.art !== 'youtube' || !jetzt.heimStationId),
|
|
||||||
knopf('musik_refresh', 'Aktualisieren', '🔄'),
|
knopf('musik_refresh', 'Aktualisieren', '🔄'),
|
||||||
));
|
));
|
||||||
return reihen;
|
return reihen;
|
||||||
@@ -336,7 +355,7 @@ async function zielKanal(interaction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const IDS = ['musik_add', 'musik_skip', 'musik_leeren', 'musik_heim', 'musik_refresh',
|
const IDS = ['musik_add', 'musik_skip', 'musik_leeren', 'musik_heim', 'musik_refresh',
|
||||||
'musik_vor', 'musik_weg'];
|
'musik_pause', 'musik_vor', 'musik_weg'];
|
||||||
|
|
||||||
/** Knöpfe, Menüs und das Eingabefenster der Musik-Tafel */
|
/** Knöpfe, Menüs und das Eingabefenster der Musik-Tafel */
|
||||||
export async function handleMusikInteraction(interaction) {
|
export async function handleMusikInteraction(interaction) {
|
||||||
@@ -438,6 +457,10 @@ export async function handleMusikInteraction(interaction) {
|
|||||||
// und das über den drei Sekunden liegt.
|
// und das über den drei Sekunden liegt.
|
||||||
await interaction.deferUpdate();
|
await interaction.deferUpdate();
|
||||||
await weiter(interaction.client, guildId, { erzwungen: true, kanalId: kanal.id });
|
await weiter(interaction.client, guildId, { erzwungen: true, kanalId: kanal.id });
|
||||||
|
} else if (customId === 'musik_pause') {
|
||||||
|
// Kein Aufschub nötig: Anhalten passiert im Speicher, ohne Netz.
|
||||||
|
pausieren(guildId);
|
||||||
|
await interaction.deferUpdate();
|
||||||
} else if (customId === 'musik_leeren') {
|
} else if (customId === 'musik_leeren') {
|
||||||
queueClear(guildId);
|
queueClear(guildId);
|
||||||
await interaction.deferUpdate();
|
await interaction.deferUpdate();
|
||||||
|
|||||||
+74
-2
@@ -503,17 +503,80 @@ export function lautstaerkeSetzen(guildId, wunsch) {
|
|||||||
return wert;
|
return wert;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wie lange läuft der aktuelle Titel schon — Pausen abgezogen.
|
||||||
|
*
|
||||||
|
* An einer Stelle, weil drei Stellen es brauchen: die Musik-Tafel, das Panel
|
||||||
|
* und der Fortschrittsbalken. Wer das je einzeln rechnet, vergisst an einer
|
||||||
|
* davon die Pause, und dann behauptet ein Balken, ein angehaltener Titel
|
||||||
|
* liefe weiter.
|
||||||
|
*
|
||||||
|
* @returns {number} Sekunden
|
||||||
|
*/
|
||||||
|
export function verstrichen(eintrag) {
|
||||||
|
if (!eintrag) return 0;
|
||||||
|
const offen = eintrag.pausiertSeit ? Date.now() - eintrag.pausiertSeit : 0;
|
||||||
|
const netto = Date.now() - eintrag.seit - (eintrag.pauseGesamt ?? 0) - offen;
|
||||||
|
return Math.max(0, Math.floor(netto / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Läuft gerade etwas, das angehalten ist? */
|
||||||
|
export const istPausiert = (guildId) => Boolean(laufend.get(guildId)?.pausiertSeit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anhalten und weiterlaufen lassen.
|
||||||
|
*
|
||||||
|
* **Nur für YouTube, und das ist kein Vergessen.** Ein Sender ist ein
|
||||||
|
* Dauerstrom: hält man ihn an, läuft er beim Sender weiter, während sich
|
||||||
|
* hier die Leitung füllt. Beim Fortsetzen käme dann nicht „dort, wo es
|
||||||
|
* aufhörte", sondern eine Konserve, die immer weiter hinterherhinkt. Für
|
||||||
|
* einen Sender heisst Pause deshalb Stopp — und dafür gibt es den
|
||||||
|
* Stopp-Knopf.
|
||||||
|
*
|
||||||
|
* @param {string} guildId
|
||||||
|
* @param {boolean|null} wunsch true/false, oder null zum Umschalten
|
||||||
|
* @returns {boolean|null} der neue Zustand, oder null wenn nicht möglich
|
||||||
|
*/
|
||||||
|
export function pausieren(guildId, wunsch = null) {
|
||||||
|
const s = laufend.get(guildId);
|
||||||
|
if (!s || s.quelle?.art !== 'youtube') return null;
|
||||||
|
const soll = wunsch === null ? !s.pausiertSeit : Boolean(wunsch);
|
||||||
|
if (soll === Boolean(s.pausiertSeit)) return soll;
|
||||||
|
if (soll) {
|
||||||
|
// `true` schiebt Stille nach, statt die Verbindung stumm hängen zu
|
||||||
|
// lassen — sonst hält Discord den Sprecher für abgestürzt.
|
||||||
|
s.player.pause(true);
|
||||||
|
s.pausiertSeit = Date.now();
|
||||||
|
} else {
|
||||||
|
s.pauseGesamt = (s.pauseGesamt ?? 0) + (Date.now() - s.pausiertSeit);
|
||||||
|
s.pausiertSeit = null;
|
||||||
|
s.player.unpause();
|
||||||
|
}
|
||||||
|
return soll;
|
||||||
|
}
|
||||||
|
|
||||||
/** Alles abräumen — Verbindung, Spieler, ffmpeg */
|
/** Alles abräumen — Verbindung, Spieler, ffmpeg */
|
||||||
export function stoppen(guildId) {
|
export function stoppen(guildId) {
|
||||||
const s = laufend.get(guildId);
|
const s = laufend.get(guildId);
|
||||||
let abgeraeumt;
|
let abgeraeumt;
|
||||||
if (s) {
|
if (s) {
|
||||||
s.player.stop(true);
|
// Reihenfolge ist hier alles: **erst aus der Karte, dann anhalten.**
|
||||||
ketteAbraeumen(s);
|
//
|
||||||
|
// `player.stop()` meldet den Leerlauf SYNCHRON — gemessen am
|
||||||
|
// 28.08.2026 mit @discordjs/voice 0.19: der Idle-Melder läuft
|
||||||
|
// mitten in `stop()`, noch bevor die nächste Zeile dran ist. Stand
|
||||||
|
// `laufend.delete` danach, fand der Melder den Eintrag noch, hielt
|
||||||
|
// das Ende für „Titel vorbei" und schaltete weiter — auf den
|
||||||
|
// nächsten Titel oder zurück auf den Heimsender.
|
||||||
|
//
|
||||||
|
// Von aussen sah das so aus: der letzte verlässt den Kanal, der Bot
|
||||||
|
// geht raus und steht sofort wieder drin. Genau so gemeldet.
|
||||||
laufend.delete(guildId);
|
laufend.delete(guildId);
|
||||||
// Sonst zählt der nächste Start ab dem alten Stand weiter und bricht
|
// Sonst zählt der nächste Start ab dem alten Stand weiter und bricht
|
||||||
// womöglich sofort wieder ab.
|
// womöglich sofort wieder ab.
|
||||||
fehlschlaege.delete(guildId);
|
fehlschlaege.delete(guildId);
|
||||||
|
s.player.stop(true);
|
||||||
|
ketteAbraeumen(s);
|
||||||
// Die Statuszeile bleibt sonst stehen und behauptet stundenlang, es
|
// Die Statuszeile bleibt sonst stehen und behauptet stundenlang, es
|
||||||
// liefe ein Lied, das längst vorbei ist. Leerer Text räumt sie ab.
|
// liefe ein Lied, das längst vorbei ist. Leerer Text räumt sie ab.
|
||||||
//
|
//
|
||||||
@@ -675,6 +738,11 @@ export async function spielen(kanal, quelle) {
|
|||||||
// die Vorgabe ist leise, damit niemand erschrickt.
|
// die Vorgabe ist leise, damit niemand erschrickt.
|
||||||
resource.volume?.setVolume(radioLautstaerke() / 100);
|
resource.volume?.setVolume(radioLautstaerke() / 100);
|
||||||
player.play(resource);
|
player.play(resource);
|
||||||
|
// Der Spieler wird über Quellenwechsel hinweg wiederverwendet. Stand er
|
||||||
|
// beim Umschalten auf Pause, bliebe der neue Titel stumm stehen — und der
|
||||||
|
// neue Eintrag sagt „läuft". Zwei Aussagen, eine Wirklichkeit: hier wird
|
||||||
|
// sie geradegezogen.
|
||||||
|
player.unpause();
|
||||||
verbindung.subscribe(player);
|
verbindung.subscribe(player);
|
||||||
|
|
||||||
// Faengt der Ton wirklich an? Wenn ffmpeg fehlt oder der Sender nichts
|
// Faengt der Ton wirklich an? Wenn ffmpeg fehlt oder der Sender nichts
|
||||||
@@ -733,6 +801,10 @@ export async function spielen(kanal, quelle) {
|
|||||||
heimStationId: quelle.art === 'radio' ? quelle.station.id : (vorher?.heimStationId ?? null),
|
heimStationId: quelle.art === 'radio' ? quelle.station.id : (vorher?.heimStationId ?? null),
|
||||||
titel: quelle.art === 'youtube' ? quelle.titel.titel : null,
|
titel: quelle.art === 'youtube' ? quelle.titel.titel : null,
|
||||||
seit: Date.now(), tonLaeuft: null, client: kanal.client,
|
seit: Date.now(), tonLaeuft: null, client: kanal.client,
|
||||||
|
// Angehalten seit wann, und wie viel Pause insgesamt schon drin war.
|
||||||
|
// Ohne das zweite liefe der Fortschrittsbalken nach dem Fortsetzen
|
||||||
|
// um die Pausenlänge voraus.
|
||||||
|
pausiertSeit: null, pauseGesamt: 0,
|
||||||
};
|
};
|
||||||
laufend.set(guildId, eintrag);
|
laufend.set(guildId, eintrag);
|
||||||
// Neuer Anlauf für die Statuszeile: vielleicht wurde das Recht inzwischen
|
// Neuer Anlauf für die Statuszeile: vielleicht wurde das Recht inzwischen
|
||||||
|
|||||||
+16
-3
@@ -40,7 +40,7 @@ import { lastResults, GAME_ICONS, spielName, spielListe, zugangStand } from '../
|
|||||||
import { istLsServer, modBasis } from '../bot/ls-farm.js';
|
import { istLsServer, modBasis } from '../bot/ls-farm.js';
|
||||||
import {
|
import {
|
||||||
titelHolen, laeuft, spielen, stoppen, weiter, radioQuelle,
|
titelHolen, laeuft, spielen, stoppen, weiter, radioQuelle,
|
||||||
lautstaerkeSetzen, ffmpegVorhanden, tafelnMelden, titelMerken,
|
lautstaerkeSetzen, ffmpegVorhanden, tafelnMelden, titelMerken, pausieren, verstrichen,
|
||||||
} from '../bot/radio.js';
|
} from '../bot/radio.js';
|
||||||
import { einreihen, eintraegeAufnehmen, ggfAnwerfen } from '../bot/radio-musik.js';
|
import { einreihen, eintraegeAufnehmen, ggfAnwerfen } from '../bot/radio-musik.js';
|
||||||
import {
|
import {
|
||||||
@@ -469,8 +469,10 @@ export function registerApiRoutes(app, client) {
|
|||||||
kanal: t?.kanal ?? null,
|
kanal: t?.kanal ?? null,
|
||||||
// Sekunden statt eines Zeitstempels: der Browser muesste sonst
|
// Sekunden statt eines Zeitstempels: der Browser muesste sonst
|
||||||
// gegen die Uhr des Servers rechnen, und die beiden gehen selten
|
// gegen die Uhr des Servers rechnen, und die beiden gehen selten
|
||||||
// gleich.
|
// gleich. `verstrichen()` zieht Pausen ab -- dieselbe Rechnung
|
||||||
verstrichen: Math.floor((Date.now() - jetzt.seit) / 1000),
|
// wie in der Discord-Tafel, nicht eine zweite daneben.
|
||||||
|
verstrichen: verstrichen(jetzt),
|
||||||
|
pausiert: Boolean(jetzt.pausiertSeit),
|
||||||
dauer: t ? t.dauer : null,
|
dauer: t ? t.dauer : null,
|
||||||
kanalId: jetzt.channelId,
|
kanalId: jetzt.channelId,
|
||||||
kanalName: kanal?.name ?? null,
|
kanalName: kanal?.name ?? null,
|
||||||
@@ -540,6 +542,17 @@ export function registerApiRoutes(app, client) {
|
|||||||
// das am Ergebnis sehen.
|
// das am Ergebnis sehen.
|
||||||
await tafelnMelden(client, guildId);
|
await tafelnMelden(client, guildId);
|
||||||
return { ok: true, lautstaerke: wert, laeuft: radioStand(guild) };
|
return { ok: true, lautstaerke: wert, laeuft: radioStand(guild) };
|
||||||
|
} else if (aktion === 'pause') {
|
||||||
|
// Gibt null zurueck, wenn nichts oder ein Sender laeuft --
|
||||||
|
// dann ist Pause nicht vorgesehen, und das wird gesagt statt
|
||||||
|
// still nichts zu tun.
|
||||||
|
const neu = pausieren(guildId, b.wert === undefined ? null : Boolean(b.wert));
|
||||||
|
if (neu === null) {
|
||||||
|
return reply.code(400).send({
|
||||||
|
error: 'Pause gibt es nur bei YouTube -- ein Sender laesst sich nicht anhalten, '
|
||||||
|
+ 'nur stoppen.',
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (aktion === 'skip') {
|
} else if (aktion === 'skip') {
|
||||||
const kanalId = zielId();
|
const kanalId = zielId();
|
||||||
if (!kanalId) return reply.code(400).send({ error: 'Kein Sprachkanal.' });
|
if (!kanalId) return reply.code(400).send({ error: 'Kein Sprachkanal.' });
|
||||||
|
|||||||
Reference in New Issue
Block a user