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
@@ -0,0 +1,308 @@
|
||||
// Der Bauplan des Community-Servers. Deklarativ, damit man ihn lesen und
|
||||
// im Pull Request diskutieren kann, statt ihn in einer Oberflaeche zu suchen.
|
||||
//
|
||||
// Leitgedanke: der Kanal hat 5 bis 30 gleichzeitige Zuschauer. Ein Server mit
|
||||
// zwanzig Kanaelen wirkt bei dieser Groesse leer, und ein leerer Kanal haelt
|
||||
// niemanden. Fuenf sichtbare Kanaele, in denen etwas passiert, sind besser.
|
||||
// Wann welcher Kanal dazukommt, steht in docs/projects/discord-community.md.
|
||||
//
|
||||
// Jeder Eintrag hat einen `key`, der sich nie aendert, und einen `name`, der
|
||||
// sich aendern darf. Gefunden wird ueber `name` und `aliases`, verwiesen wird
|
||||
// ueber `key` - so wird aus einer Umbenennung eine Umbenennung und kein
|
||||
// zweiter Kanal daneben.
|
||||
|
||||
import { bits } from './permissions.ts';
|
||||
import { CH } from './api.ts';
|
||||
|
||||
// Zur Schreibweise: Kommentare und Bezeichner bleiben bei ae/oe/ue wie im
|
||||
// restlichen Repo. Alles, was ein Mitglied auf Discord zu sehen bekommt -
|
||||
// Kanalnamen, Themen, Rollennamen, der Willkommenstext, AutoMod-Meldungen -
|
||||
// steht in richtigem Deutsch. "Sei anstaendig" liest sich wie ein Tippfehler,
|
||||
// und der erste Eindruck eines Servers ist der Text darin.
|
||||
|
||||
/** Schluessel, unter denen eine Webhook-URL im Zugangsdaten-Speicher landet. */
|
||||
export type WebhookSlot = 'goLiveWebhook' | 'recapWebhook' | 'clipsWebhook';
|
||||
|
||||
export type RolePlan = {
|
||||
name: string;
|
||||
color: number;
|
||||
/** Eigene Gruppe in der Mitgliederliste. */
|
||||
hoist: boolean;
|
||||
mentionable: boolean;
|
||||
permissions: bigint;
|
||||
/** Wofuer es die Rolle gibt - landet in der Vorschau und in der Doku. */
|
||||
purpose: string;
|
||||
};
|
||||
|
||||
export type OverwritePlan = {
|
||||
/** Rollenname, oder '@everyone' fuer die Grundrolle. */
|
||||
role: string;
|
||||
allow?: bigint;
|
||||
deny?: bigint;
|
||||
};
|
||||
|
||||
export type CategoryPlan = {
|
||||
key: string;
|
||||
name: string;
|
||||
/** Frueher benutzte Namen. Wird einer gefunden, wird umbenannt. */
|
||||
aliases?: string[];
|
||||
overwrites?: OverwritePlan[];
|
||||
};
|
||||
|
||||
export type ChannelPlan = {
|
||||
key: string;
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
type: number;
|
||||
topic?: string;
|
||||
/** `key` einer Kategorie aus CATEGORIES. */
|
||||
category?: string;
|
||||
overwrites?: OverwritePlan[];
|
||||
webhooks?: Array<{ name: string; slot: WebhookSlot }>;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------- Rechte
|
||||
|
||||
/**
|
||||
* Was ein normales Mitglied ueberall darf. Bewusst als vollstaendige Liste
|
||||
* gesetzt statt auf Discords Vorgabe zu vertrauen - die aendert sich, und
|
||||
* "jeder darf alle anpingen" ist der Unterschied zwischen einem Server, den
|
||||
* man anlaesst, und einem, den man stummschaltet.
|
||||
*/
|
||||
export const EVERYONE = bits(
|
||||
'CREATE_INSTANT_INVITE', // Mitglieder duerfen einladen. Wachstum vor Kontrolle.
|
||||
'VIEW_CHANNEL', 'READ_MESSAGE_HISTORY',
|
||||
'SEND_MESSAGES', 'SEND_MESSAGES_IN_THREADS', 'CREATE_PUBLIC_THREADS',
|
||||
'ADD_REACTIONS', 'EMBED_LINKS', 'ATTACH_FILES',
|
||||
'USE_EXTERNAL_EMOJIS', 'USE_EXTERNAL_STICKERS', 'USE_APPLICATION_COMMANDS',
|
||||
'SEND_POLLS', 'CHANGE_NICKNAME',
|
||||
'CONNECT', 'SPEAK', 'STREAM', 'USE_VAD', 'USE_SOUNDBOARD',
|
||||
);
|
||||
// Nicht enthalten und damit verboten: MENTION_EVERYONE, SEND_TTS_MESSAGES,
|
||||
// CREATE_PRIVATE_THREADS und saemtliche Verwaltungsrechte.
|
||||
|
||||
const MOD = EVERYONE | bits(
|
||||
'KICK_MEMBERS', 'BAN_MEMBERS', 'MODERATE_MEMBERS',
|
||||
'MANAGE_MESSAGES', 'MANAGE_THREADS', 'MANAGE_NICKNAMES',
|
||||
'MUTE_MEMBERS', 'DEAFEN_MEMBERS', 'MOVE_MEMBERS',
|
||||
'VIEW_AUDIT_LOG', 'MENTION_EVERYONE',
|
||||
);
|
||||
// Kein MANAGE_GUILD, kein MANAGE_ROLES, kein MANAGE_CHANNELS: Moderation
|
||||
// heisst aufraeumen, nicht umbauen. Wer umbauen soll, bekommt es einzeln.
|
||||
|
||||
// Abdu bekommt ADMINISTRATOR. Das Recht schliesst alle anderen ein und setzt
|
||||
// jede Kanal-Ausnahme ausser Kraft - auch kuenftige. Wer diese Rolle traegt,
|
||||
// kann den Server komplett umbauen und loeschen, deshalb gehoert sie an genau
|
||||
// ein Konto und an kein zweites.
|
||||
const ABDU = bits('ADMINISTRATOR');
|
||||
|
||||
export const ROLES: RolePlan[] = [
|
||||
{
|
||||
name: 'Abdu',
|
||||
color: 0x9146ff,
|
||||
hoist: true,
|
||||
mentionable: true,
|
||||
permissions: ABDU,
|
||||
purpose:
|
||||
'Der Kanal selbst. Steht ganz oben in der Mitgliederliste und traegt ' +
|
||||
'Administrator, darf also alles. Nur fuer Abdus eigenes Konto.',
|
||||
},
|
||||
{
|
||||
name: 'Mod',
|
||||
color: 0x5865f2,
|
||||
hoist: true,
|
||||
mentionable: true,
|
||||
permissions: MOD,
|
||||
purpose: 'Raeumt auf: loeschen, stummschalten, im Zweifel rauswerfen.',
|
||||
},
|
||||
{
|
||||
name: 'Stammgast',
|
||||
color: 0x4fd1c5,
|
||||
hoist: true,
|
||||
mentionable: false,
|
||||
permissions: EVERYONE | bits('PRIORITY_SPEAKER'),
|
||||
purpose:
|
||||
'Auszeichnung fuer Leute, die regelmaessig da sind. Bei dieser Kanalgroesse ' +
|
||||
'ist sichtbare Zugehoerigkeit der wirksamste Hebel - die Rolle faerbt den ' +
|
||||
'Namen und sortiert ihn in der Mitgliederliste nach oben.',
|
||||
},
|
||||
{
|
||||
name: 'Live-Ping',
|
||||
color: 0x99aab5,
|
||||
hoist: false,
|
||||
mentionable: true,
|
||||
permissions: EVERYONE,
|
||||
purpose:
|
||||
'Wird bei Stream-Start angepingt - selbst vergeben, nicht automatisch. ' +
|
||||
'Ein Rundruf an alle bei jedem Stream ist der schnellste Weg, dass der ' +
|
||||
'Server stummgeschaltet wird.',
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------- Kanaele
|
||||
//
|
||||
// Zu den Namen: Discord schreibt Textkanaele klein und macht aus Leerzeichen
|
||||
// Bindestriche. Emoji, das Trennzeichen U+2502 und Umlaute bleiben unveraendert
|
||||
// stehen - nachgemessen am echten Server, nicht angenommen.
|
||||
|
||||
export const CATEGORIES: CategoryPlan[] = [
|
||||
{ key: 'info', name: '📌 Info', aliases: ['Info', 'Informationen'] },
|
||||
{ key: 'community', name: '🎮 Community', aliases: ['Community', 'Textkanäle'] },
|
||||
{
|
||||
key: 'intern',
|
||||
name: '🔒 Intern',
|
||||
aliases: ['Intern'],
|
||||
// Fuer normale Mitglieder gibt es diese Kategorie nicht.
|
||||
overwrites: [
|
||||
{ role: '@everyone', deny: bits('VIEW_CHANNEL') },
|
||||
{ role: 'Mod', allow: bits('VIEW_CHANNEL') },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** Nur lesen: schreiben darf hier niemand ausser Mods und Webhooks. */
|
||||
const READONLY: OverwritePlan[] = [
|
||||
{ role: '@everyone', deny: bits('SEND_MESSAGES', 'CREATE_PUBLIC_THREADS') },
|
||||
];
|
||||
|
||||
/** Lesen, aber diskutieren nur in Threads - haelt die Ankuendigungen sauber. */
|
||||
const ANNOUNCE: OverwritePlan[] = [
|
||||
{
|
||||
role: '@everyone',
|
||||
deny: bits('SEND_MESSAGES'),
|
||||
allow: bits('ADD_REACTIONS', 'CREATE_PUBLIC_THREADS', 'SEND_MESSAGES_IN_THREADS'),
|
||||
},
|
||||
];
|
||||
|
||||
export const CHANNELS: ChannelPlan[] = [
|
||||
{
|
||||
key: 'willkommen',
|
||||
name: '👋│willkommen',
|
||||
aliases: ['willkommen', 'willkommen-und-richtlinien'],
|
||||
type: CH.TEXT,
|
||||
category: 'info',
|
||||
topic: 'Was das hier ist und die drei Regeln.',
|
||||
overwrites: READONLY,
|
||||
},
|
||||
{
|
||||
key: 'ankuendigungen',
|
||||
name: '📢│ankündigungen',
|
||||
aliases: ['ankuendigungen', 'ankündigungen'],
|
||||
type: CH.TEXT,
|
||||
category: 'info',
|
||||
topic: 'Stream-Start und die Zusammenfassung danach. Reden gerne in den Threads.',
|
||||
overwrites: ANNOUNCE,
|
||||
webhooks: [
|
||||
{ name: 'Go-Live', slot: 'goLiveWebhook' },
|
||||
{ name: 'Stream-Recap', slot: 'recapWebhook' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'allgemein',
|
||||
name: '💬│allgemein',
|
||||
aliases: ['allgemein'],
|
||||
type: CH.TEXT,
|
||||
category: 'community',
|
||||
topic: 'Der Hauptkanal. Im Zweifel hier.',
|
||||
},
|
||||
{
|
||||
key: 'clips',
|
||||
name: '🎬│clips',
|
||||
aliases: ['clips'],
|
||||
type: CH.TEXT,
|
||||
category: 'community',
|
||||
topic: 'Gute Momente aus dem Stream. Der Clip-Knopf postet hier hin.',
|
||||
webhooks: [{ name: 'Clips', slot: 'clipsWebhook' }],
|
||||
},
|
||||
{
|
||||
key: 'voice',
|
||||
name: '🔊 Zusammen zocken',
|
||||
aliases: ['zusammen-zocken', 'Zusammen zocken'],
|
||||
type: CH.VOICE,
|
||||
category: 'community',
|
||||
},
|
||||
{
|
||||
key: 'mod-chat',
|
||||
name: '🔨│mod-chat',
|
||||
aliases: ['mod-chat'],
|
||||
type: CH.TEXT,
|
||||
category: 'intern',
|
||||
topic: 'Nur Mods.',
|
||||
},
|
||||
{
|
||||
key: 'technik-log',
|
||||
name: '🤖│technik-log',
|
||||
aliases: ['technik-log'],
|
||||
type: CH.TEXT,
|
||||
category: 'intern',
|
||||
topic: 'Meldungen des Toolkits: Probeläufe, Fehler, AutoMod-Treffer.',
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------- Server
|
||||
|
||||
/**
|
||||
* Servereinstellungen.
|
||||
* - verification_level 2 verlangt ein Konto, das aelter als fuenf Minuten
|
||||
* ist. Kostet echte Leute nichts und haelt Wegwerf-Konten draussen, was bei
|
||||
* einem oeffentlich von Twitch aus verlinkten Server der Normalfall ist.
|
||||
* - default_message_notifications 1 heisst: nur bei Erwaehnung. Sonst
|
||||
* klingelt jede Nachricht bei jedem, und der Server wird stummgeschaltet.
|
||||
*/
|
||||
export const GUILD_SETTINGS = {
|
||||
verification_level: 2,
|
||||
explicit_content_filter: 2,
|
||||
default_message_notifications: 1,
|
||||
system_channel_flags: 4, // Einrichtungstipps unterdruecken, Beitritte zeigen
|
||||
};
|
||||
|
||||
/**
|
||||
* Wird einmalig in #willkommen gepostet, wenn der Kanal noch leer ist.
|
||||
* {key} wird durch die Erwaehnung des Kanals mit diesem `key` ersetzt.
|
||||
*/
|
||||
export const WELCOME = `**Willkommen bei abdulive_**
|
||||
|
||||
Das hier ist der Ort zwischen den Streams. Wer mitliest, gehört dazu.
|
||||
|
||||
**Drei Regeln**
|
||||
> **1.** Sei anständig. Keine Beleidigungen, kein Rassismus, kein Sexismus.
|
||||
> **2.** Kein Spam, keine Werbung, keine fremden Streams im Chat.
|
||||
> **3.** Wenn ein Mod etwas sagt, ist das erledigt. Diskussion per DM.
|
||||
|
||||
**Wo was passiert**
|
||||
> {allgemein} - alles andere
|
||||
> {clips} - die guten Momente
|
||||
> {ankuendigungen} - Stream-Start und die Zusammenfassung danach
|
||||
|
||||
**Bescheid wissen, wenn der Stream startet**
|
||||
Hol dir die Rolle **Live-Ping**. Dann wird nur angepingt, wer das auch möchte.
|
||||
|
||||
Twitch: <https://twitch.tv/abdulive_>`;
|
||||
|
||||
// ---------------------------------------------------------------- AutoMod
|
||||
|
||||
/**
|
||||
* Zwei Regeln, die Discord selbst durchsetzt - ohne Bot, auch nachts um drei.
|
||||
* Mehr braucht es bei dieser Groesse nicht; Wortfilter kosten mehr Aerger
|
||||
* durch Fehlalarme, als sie an Spam verhindern.
|
||||
*/
|
||||
export type AutomodPlan = {
|
||||
name: string;
|
||||
/** Frueher benutzte Namen, damit umbenannt statt doppelt angelegt wird. */
|
||||
aliases?: string[];
|
||||
trigger_type: number;
|
||||
trigger_metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const AUTOMOD: AutomodPlan[] = [
|
||||
{
|
||||
name: 'Erwähnungs-Spam',
|
||||
aliases: ['Erwaehnungs-Spam'],
|
||||
trigger_type: 5,
|
||||
trigger_metadata: { mention_total_limit: 5 },
|
||||
},
|
||||
{ name: 'Spam-Erkennung', trigger_type: 3, trigger_metadata: {} },
|
||||
];
|
||||
|
||||
/** Was jemand zu sehen bekommt, dessen Nachricht AutoMod abgefangen hat. */
|
||||
export const AUTOMOD_BLOCK = 'Das hat die Spam-Erkennung abgefangen. Bei Fragen an die Mods.';
|
||||
Reference in new issue
Block a user