Beitrittsmeldungen liefen nach #allgemein, weil Discord ohne Angabe den ersten Kanal nimmt. Jetzt gehen sie nach #begruessung (SYSTEM_CHANNEL). Die von Abdu angelegten Textkanaele spielvorschlaege und reaction-einsendungen sind jetzt Foren im Bauplan: ein Beitrag je Spiel bzw. Clip, 👍 als Standard-Reaktion, Stand-Tags nur fuer Mods. apply.ts kann dafuer Tags und Standard-Reaktion anlegen und nachziehen; fremde Tags bleiben mit ihrer ID stehen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
421 lines
15 KiB
TypeScript
421 lines
15 KiB
TypeScript
// 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: <https://twitch.tv/abdulive_>`;
|
|
|
|
// ---------------------------------------------------------- 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<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.';
|