// 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' | 'technikWebhook'; 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[]; }; /** Ein Stichwort, mit dem Forenbeitraege markiert werden. */ export type TagPlan = { /** Hoechstens 20 Zeichen, sonst lehnt Discord ab. */ name: string; emoji: string; /** Setzen duerfen nur Mods (MANAGE_THREADS), nicht der Verfasser. */ moderated?: boolean; }; export type ChannelPlan = { key: string; name: string; aliases?: string[]; type: number; /** Bei Foren sind das die Richtlinien, die ueber jedem neuen Beitrag stehen. */ topic?: string; /** `key` einer Kategorie aus CATEGORIES. */ category?: string; overwrites?: OverwritePlan[]; webhooks?: Array<{ name: string; slot: WebhookSlot }>; /** Nur Foren. Von Hand angelegte Tags bleiben daneben stehen. */ tags?: TagPlan[]; /** Nur Foren: die Reaktion, die unter jedem Beitrag schon angeboten wird. */ reaction?: string; }; // ---------------------------------------------------------------- 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, }, { // Hier landen Discords Beitrittsmeldungen (SYSTEM_CHANNEL). Beschreibbar, // damit der Winke-Knopf unter der Meldung funktioniert - der antwortet im // Namen dessen, der klickt. key: 'begruessung', name: '🙋│begrüßung', aliases: ['begrüßung', 'begruessung'], type: CH.TEXT, category: 'info', topic: 'Wer neu ist, taucht hier auf. Sag Hallo.', }, { 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' }], }, { // Von Abdu als Textkanal angelegt, seit 2026-10-17 ein Forum: ein Beitrag // je Spiel, abgestimmt wird per Daumen. Im Textkanal schlaegt nach drei // Wochen jemand dasselbe Spiel noch einmal vor, weil der erste Vorschlag // weggescrollt ist. Der erste Satz der Richtlinien ist Abdus eigener. // Die Stand-Tags setzen nur Mods, sonst markiert jeder sein Spiel als // "wird gespielt". key: 'spielvorschlaege', name: '🎮│spielvorschläge', aliases: ['spielvorschläge', 'spielvorschlaege'], type: CH.FORUM, category: 'community', topic: 'Ihr wollt sehen wie ich Spiele zocke, die euch gefallen? Schreibt gerne eure Vorschläge rein!\n\n' + 'Ein Beitrag pro Spiel. Gibt es das Spiel schon, Daumen hoch drücken statt noch einmal posten - ' + 'so sehe ich, was ihr am meisten sehen wollt.', reaction: '👍', tags: [ { name: 'Wird gespielt', emoji: '🎮', moderated: true }, { name: 'Gespielt', emoji: '✅', moderated: true }, { name: 'Eher nicht', emoji: '❌', moderated: true }, ], }, { // Ebenfalls von Abdu angelegt und ebenfalls ein Forum: ein Beitrag je // Clip, damit man sieht, was schon dran war. key: 'reaction', name: '👀│reaction-einsendungen', aliases: ['reaction-einsendungen'], type: CH.FORUM, category: 'community', topic: 'Ihr habt Clips oder Videos worauf ich reagieren sollte? Schickt hier die Clips rein!\n\n' + 'Ein Beitrag pro Clip, Link in den Beitrag.', reaction: '👍', tags: [ { name: 'Kommt dran', emoji: '🔜', moderated: true }, { name: 'Angeschaut', emoji: '✅', moderated: true }, ], }, { 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.', // Der Entwurfsmodus des Go-Live-Posts landet hier: dieselbe Nachricht, // die sonst in die Ankuendigungen ginge, nur ohne Ping und nur fuer Mods. // So sieht man das Ergebnis, bevor es der Server sieht. webhooks: [{ name: 'Technik', slot: 'technikWebhook' }], }, ]; // ---------------------------------------------------------------- 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 }; /** * `key` des Kanals, in den Discord "X ist beigetreten" schreibt. Ohne Angabe * nimmt Discord den ersten Kanal des Servers, und das war #allgemein: dort * gingen die Beitritte zwischen den Gespraechen unter, und umgekehrt. * Steht nicht in GUILD_SETTINGS, weil die ID erst nach den Kanaelen feststeht. */ export const SYSTEM_CHANNEL = 'begruessung'; /** * 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 > {spielvorschlaege} - was Abdu mal spielen sollte > {reaction} - Clips, auf die Abdu reagieren soll > {ankuendigungen} - Stream-Start und die Zusammenfassung danach **Bescheid wissen, wenn der Stream startet** Knopf drücken. Angepingt wird nur, wer das auch möchte, und ein zweiter Druck nimmt die Rolle wieder weg. Twitch: `; // ---------------------------------------------------------- Selbstbedienung export type SelfRolePlan = { /** Landet im Knopf und kommt bei jedem Klick zurueck. Nie aendern. */ customId: string; /** Name der Rolle aus ROLES. */ role: string; label: string; emoji: string; /** Antwort beim Holen und beim Abgeben - sieht nur, wer geklickt hat. */ onText: string; offText: string; }; /** * Rollen, die man sich per Knopf selbst gibt. * * Discords Onboarding kann das auch, verlangt aber sieben Vorgabekanaele mit * fuenf beschreibbaren. Der Server hat fuenf und drei. Zwei Kanaele anzulegen, * damit eine Schwelle passt, macht den Server leerer statt besser - also ein * Knopf unter dem Willkommenstext. Siehe docs/projects/discord-community.md. */ export const SELF_ROLES: SelfRolePlan[] = [ { customId: 'rolle:live-ping', role: 'Live-Ping', label: 'Sag mir Bescheid, wenn der Stream startet', emoji: '🔴', onText: 'Passt. Du wirst in den Ankündigungen angepingt, wenn der Stream startet. ' + 'Nochmal drücken nimmt es wieder weg.', offText: 'Erledigt, du wirst nicht mehr angepingt.', }, ]; // ---------------------------------------------------------------- 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; }; 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.';