Discord-Community-Server: Bauplan und Einrichtungsbefehl
Der Server ist kein Tool aus der Neunerliste, aber Broadcaster, Recap und Clipper posten dorthin. Deshalb steht die Struktur als Code im Repo und nicht nur in einer Oberflaeche. packages/discord: - permissions.ts Rechtebits als BigInt, das Feld ist 64 Bit breit - api.ts REST-Client: Warteschlange, Rate Limits, lesbare Fehler - plan.ts Rollen, Kanaele, Rechte, AutoMod, Willkommenstext - apply.ts traegt den Bauplan auf einen bestehenden Server auf Neuer Befehl `abdulive discord:setup [--server ID] [--vorschau]`. Zugeschnitten auf 5 bis 30 gleichzeitige Zuschauer: fuenf sichtbare Kanaele statt zwanzig, und eine selbst vergebene Rolle Live-Ping statt eines Rundrufs an alle bei jedem Stream. Wiederholbar statt einmalig: gefunden wird ueber Namen und frueheren Namen, danach wird angeglichen. Der zweite Lauf aendert nichts. Geloescht wird nie, und von Hand gesetzte Rechte-Ausnahmen bleiben stehen. Drei Dinge, die am echten Server nachgemessen und nicht angenommen wurden: - Discord schreibt Textkanaele klein und macht aus Leerzeichen Bindestriche; Emoji, Umlaute und U+2502 bleiben. normalizeName() bildet das ab, sonst geht bei jedem Lauf dieselbe Umbenennung erneut raus. - Kanalnamen lassen sich nur zweimal je zehn Minuten aendern, danach verlangt Discord bis zu neun Minuten Pause. Der Lauf wartet hoechstens eine Minute und meldet den Rest. - Eine Rolle kann nur Rollen unterhalb der eigenen verschieben, auch mit Administrator. rolePositions() rechnet den Platz unter der Bot-Rolle aus, statt blind 3, 2, 1 zu verlangen. Zugangsdaten laufen ueber den verschluesselten Speicher (ADR-0005): der Bot-Token wird verdeckt abgefragt, die drei Webhook-URLs werden nie ausgegeben und nie geloggt. Im Quelltext bleibt es bei ae/oe/ue, alles was ein Mitglied auf Discord liest steht in richtigem Deutsch. Ein Test haelt das fest. Begruendung, Fallstricke und die Schritte von Hand: docs/projects/discord-community.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d231e1c722
commit
3b744caf6a
13 files changed
+2210
-1
No files matched your search
@@ -557,6 +557,95 @@ async function listSources(profileName = 'default'): Promise<void> {
|
||||
|
||||
const rectStr = (r: Rect) => `x ${r.x}..${r.x + r.w}, y ${r.y}..${r.y + r.h}`;
|
||||
|
||||
// ---------------------------------------------------------------- discord
|
||||
|
||||
/** Liest eine Eingabe, ohne sie im Terminal anzuzeigen. */
|
||||
function promptHidden(frage: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const input = process.stdin;
|
||||
if (!input.isTTY) {
|
||||
reject(new Error(`${frage.trim()} - dafuer wird ein Terminal gebraucht.`));
|
||||
return;
|
||||
}
|
||||
process.stdout.write(frage);
|
||||
input.setRawMode(true);
|
||||
input.resume();
|
||||
input.setEncoding('utf8');
|
||||
let buf = '';
|
||||
const stop = (): void => { input.off('data', onData); input.setRawMode(false); input.pause(); };
|
||||
const onData = (chunk: string): void => {
|
||||
for (const c of chunk) {
|
||||
if (c === '\r' || c === '\n') { stop(); process.stdout.write('\n'); resolve(buf.trim()); return; }
|
||||
if (c === '\u0003') { stop(); process.stdout.write('\n'); reject(new Error('Abgebrochen.')); return; }
|
||||
if (c === '\u007f' || c === '\b') { buf = buf.slice(0, -1); continue; }
|
||||
buf += c;
|
||||
}
|
||||
};
|
||||
input.on('data', onData);
|
||||
});
|
||||
}
|
||||
|
||||
async function discordSetup(args: string[]): Promise<void> {
|
||||
const dry = args.includes('--vorschau');
|
||||
const { readSecrets, updateSecrets } = await import('#twitch/secrets.ts');
|
||||
const stored = readSecrets().discord ?? {};
|
||||
|
||||
const at = args.indexOf('--server');
|
||||
const fromArgs = at >= 0 ? args[at + 1] : undefined;
|
||||
const guildId = fromArgs ?? stored.guildId ?? process.env.DISCORD_GUILD_ID;
|
||||
if (!guildId || guildId.startsWith('--')) {
|
||||
throw new Error(
|
||||
'Keine Server-ID. Aufruf: abdulive discord:setup --server <ID>\n' +
|
||||
'Die ID steht in Discord im Rechtsklickmenue des Servers unter "Server-ID kopieren"\n' +
|
||||
'(dafuer in den Einstellungen unter "Erweitert" den Entwicklermodus einschalten).'
|
||||
);
|
||||
}
|
||||
|
||||
const token = stored.botToken || process.env.DISCORD_BOT_TOKEN ||
|
||||
await promptHidden('Discord-Bot-Token (Eingabe wird nicht angezeigt): ');
|
||||
if (!token) throw new Error('Ohne Bot-Token geht es nicht.');
|
||||
|
||||
const { Discord } = await import('#discord/api.ts');
|
||||
const { applyPlan } = await import('#discord/apply.ts');
|
||||
const result = await applyPlan(new Discord(token), guildId, { dryRun: dry });
|
||||
|
||||
if (!dry) updateSecrets((s) => { s.discord = { ...s.discord, botToken: token, guildId }; });
|
||||
|
||||
const mark: Record<string, string> = {
|
||||
erstellt: `${C.ok} NEU ${C.off}`,
|
||||
angepasst: `${C.ok} GESETZT${C.off}`,
|
||||
unveraendert: `${C.dim} steht ${C.off}`,
|
||||
uebersprungen: `${C.warn} UEBERSPRUNGEN${C.off}`,
|
||||
};
|
||||
|
||||
console.log(`\nServer: ${result.guild}${dry ? ` ${C.dim}(Vorschau - nichts geaendert)${C.off}` : ''}\n`);
|
||||
let kind = '';
|
||||
for (const s of result.steps) {
|
||||
if (s.kind !== kind) { console.log(`${C.dim}${s.kind}${C.off}`); kind = s.kind; }
|
||||
const note = s.note ? ` ${C.dim}(${s.note})${C.off}` : '';
|
||||
console.log(` ${mark[s.change] ?? s.change} ${s.name}${note}`);
|
||||
}
|
||||
|
||||
const skipped = result.steps.filter((s) => s.change === 'uebersprungen').length;
|
||||
console.log('');
|
||||
if (dry) {
|
||||
console.log('Nichts geaendert. Ohne --vorschau wird es angewendet.');
|
||||
return;
|
||||
}
|
||||
console.log(`${result.webhooks} Webhook-URLs im verschluesselten Speicher abgelegt.`);
|
||||
if (skipped) console.log(`${C.warn}${skipped} Punkt(e) uebersprungen - siehe Hinweise oben.${C.off}`);
|
||||
console.log(`
|
||||
Was jetzt noch von Hand gehoert:
|
||||
1. Dir selbst die Rolle "Abdu" geben und die Bot-Rolle in den
|
||||
Servereinstellungen ueber "Abdu" ziehen.
|
||||
2. Server-Einstellungen > Onboarding: "Live-Ping" als waehlbare Rolle
|
||||
anbieten, damit sie sich jeder selbst holen kann.
|
||||
3. Einladungslink erzeugen (ohne Ablaufdatum) und in Twitch-Panel,
|
||||
Kanalinfo und Chat-Befehl eintragen.
|
||||
4. Twitch-Konto in den Servereinstellungen unter "Integrationen"
|
||||
verbinden, dann bekommen Subs automatisch eine Rolle.`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- help
|
||||
|
||||
function help(): void {
|
||||
@@ -571,6 +660,8 @@ abdulive - Werkzeugkasten fuer Abdus Stream
|
||||
sources [profil] OBS-Quellen auflisten: was landet im 9:16-Ausschnitt?
|
||||
update [--force] Nach einer neuen Fassung sehen und sie bereitlegen
|
||||
render <datei> [profil] Rohdatei nach 9:16 rendern (zum Nachrendern)
|
||||
discord:setup [--server ID] [--vorschau]
|
||||
Discord-Server nach docs/projects/discord-community.md einrichten
|
||||
help Diese Uebersicht
|
||||
|
||||
Einrichtung nach dem Start: http://localhost:4792/setup
|
||||
@@ -601,6 +692,7 @@ try {
|
||||
break;
|
||||
}
|
||||
case 'render': await renderOne(args[0], args[1]); break;
|
||||
case 'discord:setup': await discordSetup(args); break;
|
||||
case 'help': case undefined: help(); break;
|
||||
default:
|
||||
console.error(`Unbekannter Befehl "${cmd}".`);
|
||||
|
||||
Reference in new issue
Block a user