Discord: eigener Kanal fuer Beitritte, Spielvorschlaege und Reactions als Forum
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>
This commit is contained in:
1 parent
81ab865de0
commit
5494c6d24d
7 files changed
+211
-9
No files matched your search
+5
-1
@@ -216,7 +216,11 @@ und [Clips](projects/clip-shorts.md) liegen fertig im verschlüsselten Speicher
|
|||||||
unter `goLiveWebhook`, `recapWebhook` und `clipsWebhook`. Wer diese Werkzeuge
|
unter `goLiveWebhook`, `recapWebhook` und `clipsWebhook`. Wer diese Werkzeuge
|
||||||
baut, muss sich um Discord nicht mehr kümmern, sondern nur noch posten.
|
baut, muss sich um Discord nicht mehr kümmern, sondern nur noch posten.
|
||||||
|
|
||||||
Sieben Kanäle, vier Rollen, zwei AutoMod-Regeln. Der Entwurf und seine
|
Zehn Kanäle, vier Rollen, zwei AutoMod-Regeln. Seit 2026-10-17 dabei:
|
||||||
|
`🙋│begrüßung` für Discords Beitrittsmeldungen (liefen vorher in
|
||||||
|
`#allgemein`), und die Foren `🎮│spielvorschläge` und
|
||||||
|
`👀│reaction-einsendungen`. Beide hatte Abdu als Textkanäle angelegt, sie sind
|
||||||
|
jetzt Foren mit Tags, die nur Mods setzen, und 👍 als Standard-Reaktion. Der Entwurf und seine
|
||||||
Begründung stehen in [`projects/discord-community.md`](projects/discord-community.md);
|
Begründung stehen in [`projects/discord-community.md`](projects/discord-community.md);
|
||||||
die Kurzfassung ist, dass bei 5 bis 30 Zuschauern fünf sichtbare Kanäle besser
|
die Kurzfassung ist, dass bei 5 bis 30 Zuschauern fünf sichtbare Kanäle besser
|
||||||
sind als zwanzig leere.
|
sind als zwanzig leere.
|
||||||
|
|||||||
@@ -33,9 +33,12 @@ und ist die massgebliche Fassung. Was hier steht, ist die Begruendung dazu.
|
|||||||
| Kategorie | Kanal | Wer schreibt | Wofuer |
|
| Kategorie | Kanal | Wer schreibt | Wofuer |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 📌 Info | `👋│willkommen` | niemand | Die drei Regeln, einmal gepostet |
|
| 📌 Info | `👋│willkommen` | niemand | Die drei Regeln, einmal gepostet |
|
||||||
|
| 📌 Info | `🙋│begrüßung` | alle | Discords Beitrittsmeldungen |
|
||||||
| 📌 Info | `📢│ankündigungen` | nur Webhooks | Stream-Start und Recap; Diskussion in Threads |
|
| 📌 Info | `📢│ankündigungen` | nur Webhooks | Stream-Start und Recap; Diskussion in Threads |
|
||||||
| 🎮 Community | `💬│allgemein` | alle | Der Hauptkanal. Im Zweifel hier |
|
| 🎮 Community | `💬│allgemein` | alle | Der Hauptkanal. Im Zweifel hier |
|
||||||
| 🎮 Community | `🎬│clips` | alle + Clip-Knopf | Gute Momente |
|
| 🎮 Community | `🎬│clips` | alle + Clip-Knopf | Gute Momente |
|
||||||
|
| 🎮 Community | `🎮│spielvorschläge` | alle (Forum) | Ein Beitrag je Spiel, abgestimmt per 👍 |
|
||||||
|
| 🎮 Community | `👀│reaction-einsendungen` | alle (Forum) | Ein Beitrag je Clip, auf den Abdu reagieren soll |
|
||||||
| 🎮 Community | `🔊 Zusammen zocken` | — | Sprachkanal |
|
| 🎮 Community | `🔊 Zusammen zocken` | — | Sprachkanal |
|
||||||
| 🔒 Intern | `🔨│mod-chat` | Mods | Fuer alle anderen unsichtbar |
|
| 🔒 Intern | `🔨│mod-chat` | Mods | Fuer alle anderen unsichtbar |
|
||||||
| 🔒 Intern | `🤖│technik-log` | Toolkit | Probelaeufe, Fehler, AutoMod-Treffer |
|
| 🔒 Intern | `🤖│technik-log` | Toolkit | Probelaeufe, Fehler, AutoMod-Treffer |
|
||||||
@@ -54,6 +57,23 @@ Server ist der erste Eindruck, den jemand davon bekommt.
|
|||||||
So bleibt die Kanalansicht eine saubere Liste von Streams, und die Diskussion
|
So bleibt die Kanalansicht eine saubere Liste von Streams, und die Diskussion
|
||||||
ueber das Recap haengt trotzdem direkt daran.
|
ueber das Recap haengt trotzdem direkt daran.
|
||||||
|
|
||||||
|
`🙋│begrüßung` gibt es, weil Discord die Beitrittsmeldungen sonst in den
|
||||||
|
ersten Kanal des Servers schreibt, und das war `💬│allgemein`. Dort gingen die
|
||||||
|
Meldungen im Gespräch unter und das Gespräch in den Meldungen. Der Kanal ist
|
||||||
|
beschreibbar, sonst tut der Winke-Knopf unter der Meldung nichts. Welcher
|
||||||
|
Kanal es ist, steht in `SYSTEM_CHANNEL`.
|
||||||
|
|
||||||
|
Die beiden Foren hat Abdu als Textkanäle angelegt, seit 2026-10-17 stehen sie
|
||||||
|
als Foren im Bauplan. In einem Textkanal schlägt nach drei Wochen jemand
|
||||||
|
dasselbe Spiel noch einmal vor, weil der erste Vorschlag weggescrollt ist. Im
|
||||||
|
Forum gibt es einen Beitrag je Spiel, darunter wird per 👍 abgestimmt. Die
|
||||||
|
Tags für den Stand („Wird gespielt", „Gespielt", „Eher nicht" bzw. „Kommt
|
||||||
|
dran", „Angeschaut") setzen nur Mods. Sonst markiert jeder sein eigenes Spiel
|
||||||
|
als „wird gespielt". Discord wandelt einen Textkanal nicht in ein Forum um:
|
||||||
|
die leeren Textkanäle wurden einmalig von Hand gelöscht, das Skript selbst
|
||||||
|
löscht weiterhin nichts. Foren gehen auch ohne Community-Server, am echten
|
||||||
|
Server nachgeprüft.
|
||||||
|
|
||||||
Der Sprachkanal ist bewusst dabei, obwohl er anfangs leer sein wird. Ein
|
Der Sprachkanal ist bewusst dabei, obwohl er anfangs leer sein wird. Ein
|
||||||
Sprachkanal, den zwei Leute nutzen, ist der Anfang; einer, den es nicht gibt,
|
Sprachkanal, den zwei Leute nutzen, ist der Anfang; einer, den es nicht gibt,
|
||||||
wird nie einer.
|
wird nie einer.
|
||||||
|
|||||||
@@ -161,7 +161,14 @@ export type RestFull = Rest & {
|
|||||||
|
|
||||||
export type ApiRole = { id: string; name: string; permissions: string; position: number; color: number; hoist: boolean; mentionable: boolean; managed: boolean };
|
export type ApiRole = { id: string; name: string; permissions: string; position: number; color: number; hoist: boolean; mentionable: boolean; managed: boolean };
|
||||||
export type ApiOverwrite = { id: string; type: 0 | 1; allow: string; deny: string };
|
export type ApiOverwrite = { id: string; type: 0 | 1; allow: string; deny: string };
|
||||||
export type ApiChannel = { id: string; name: string; type: number; position: number; parent_id: string | null; topic?: string | null; permission_overwrites?: ApiOverwrite[] };
|
export type ApiTag = { id?: string; name: string; emoji_id?: string | null; emoji_name: string | null; moderated: boolean };
|
||||||
|
export type ApiChannel = {
|
||||||
|
id: string; name: string; type: number; position: number; parent_id: string | null;
|
||||||
|
topic?: string | null; permission_overwrites?: ApiOverwrite[];
|
||||||
|
// Nur Foren.
|
||||||
|
available_tags?: ApiTag[];
|
||||||
|
default_reaction_emoji?: { emoji_id: string | null; emoji_name: string | null } | null;
|
||||||
|
};
|
||||||
export type ApiGuild = {
|
export type ApiGuild = {
|
||||||
id: string; name: string; owner_id: string; roles: ApiRole[]; features: string[];
|
id: string; name: string; owner_id: string; roles: ApiRole[]; features: string[];
|
||||||
// Die Felder aus GUILD_SETTINGS, damit sich vergleichen laesst, statt bei
|
// Die Felder aus GUILD_SETTINGS, damit sich vergleichen laesst, statt bei
|
||||||
@@ -170,6 +177,7 @@ export type ApiGuild = {
|
|||||||
explicit_content_filter?: number;
|
explicit_content_filter?: number;
|
||||||
default_message_notifications?: number;
|
default_message_notifications?: number;
|
||||||
system_channel_flags?: number;
|
system_channel_flags?: number;
|
||||||
|
system_channel_id?: string | null;
|
||||||
};
|
};
|
||||||
export type ApiWebhook = { id: string; name: string; channel_id: string; url?: string; token?: string };
|
export type ApiWebhook = { id: string; name: string; channel_id: string; url?: string; token?: string };
|
||||||
export type ApiUser = { id: string; username: string; bot?: boolean };
|
export type ApiUser = { id: string; username: string; bot?: boolean };
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { CH, type ApiChannel, type ApiGuild, type ApiOverwrite, type ApiRole, type ApiWebhook, type Rest } from './api.ts';
|
import { CH, type ApiChannel, type ApiGuild, type ApiOverwrite, type ApiRole, type ApiWebhook, type Rest } from './api.ts';
|
||||||
import { applyPlan, type Step } from './apply.ts';
|
import { applyPlan, mergeTags, type Step } from './apply.ts';
|
||||||
import { CATEGORIES, CHANNELS, EVERYONE, ROLES } from './plan.ts';
|
import { CATEGORIES, CHANNELS, EVERYONE, ROLES } from './plan.ts';
|
||||||
import { str } from './permissions.ts';
|
import { str } from './permissions.ts';
|
||||||
|
|
||||||
@@ -504,3 +504,41 @@ test('eine Nachricht ohne Knopfleiste bekommt sie nachgereicht', async () => {
|
|||||||
assert.equal((api.messages.get(kanal)![0]!.components as unknown[]).length, 1);
|
assert.equal((api.messages.get(kanal)![0]!.components as unknown[]).length, 1);
|
||||||
assert.equal(api.messages.get(kanal)!.length, 1, 'nicht neu posten');
|
assert.equal(api.messages.get(kanal)!.length, 1, 'nicht neu posten');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Foren
|
||||||
|
|
||||||
|
test('ein Forum bekommt Tags und Standard-Reaktion aus dem Bauplan', async () => {
|
||||||
|
const api = new FakeDiscord();
|
||||||
|
await run(api);
|
||||||
|
const plan = CHANNELS.find((c) => c.key === 'spielvorschlaege')!;
|
||||||
|
const forum = live(api, 'spielvorschlaege')!;
|
||||||
|
assert.equal(forum.type, CH.FORUM);
|
||||||
|
assert.deepEqual(forum.available_tags?.map((t) => t.name), plan.tags?.map((t) => t.name));
|
||||||
|
assert.equal(forum.default_reaction_emoji?.emoji_name, plan.reaction);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ein Textkanal gleichen Namens wird nicht fuer das Forum gehalten', async () => {
|
||||||
|
// Discord kann einen Textkanal nicht in ein Forum umwandeln. Der alte steht
|
||||||
|
// also daneben, bis ihn jemand loescht - angepasst werden darf er nicht.
|
||||||
|
const api = new FakeDiscord();
|
||||||
|
api.channels.push({ id: '1', name: '🎮│spielvorschläge', type: CH.TEXT, position: 0, parent_id: null });
|
||||||
|
await run(api);
|
||||||
|
assert.equal(api.channels.filter((c) => c.name === '🎮│spielvorschläge').length, 2);
|
||||||
|
assert.equal(api.channels.find((c) => c.id === '1')?.available_tags, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('von Hand angelegte Tags bleiben mit ihrer ID stehen', async () => {
|
||||||
|
const fremd = { id: '77', name: 'Horror', emoji_name: '👻', moderated: false };
|
||||||
|
const eigen = { id: '78', name: 'gespielt', emoji_name: '✔️', moderated: false };
|
||||||
|
const out = mergeTags([fremd, eigen], [
|
||||||
|
{ name: 'Gespielt', emoji: '✅', moderated: true },
|
||||||
|
{ name: 'Eher nicht', emoji: '❌', moderated: true },
|
||||||
|
]);
|
||||||
|
assert.ok(out);
|
||||||
|
assert.deepEqual(out.find((t) => t.name === 'Horror'), fremd);
|
||||||
|
const g = out.find((t) => t.name === 'Gespielt')!;
|
||||||
|
assert.equal(g.id, '78', 'ohne ID verlieren die Beitraege ihre Markierung');
|
||||||
|
assert.equal(g.emoji_name, '✅');
|
||||||
|
assert.ok(out.find((t) => t.name === 'Eher nicht'));
|
||||||
|
assert.equal(mergeTags(out, [{ name: 'Gespielt', emoji: '✅', moderated: true }]), null);
|
||||||
|
});
|
||||||
@@ -11,14 +11,14 @@
|
|||||||
import { logger } from '#core/logger.ts';
|
import { logger } from '#core/logger.ts';
|
||||||
import { updateSecrets } from '#twitch/secrets.ts';
|
import { updateSecrets } from '#twitch/secrets.ts';
|
||||||
import {
|
import {
|
||||||
CH, type ApiChannel, type ApiGuild, type ApiOverwrite, type ApiRole, type ApiUser,
|
CH, type ApiChannel, type ApiGuild, type ApiOverwrite, type ApiRole, type ApiTag, type ApiUser,
|
||||||
type ApiWebhook, type Rest,
|
type ApiWebhook, type Rest,
|
||||||
} from './api.ts';
|
} from './api.ts';
|
||||||
import { str } from './permissions.ts';
|
import { str } from './permissions.ts';
|
||||||
import { selfRoleComponents } from './selfroles.ts';
|
import { selfRoleComponents } from './selfroles.ts';
|
||||||
import {
|
import {
|
||||||
AUTOMOD, AUTOMOD_BLOCK, CATEGORIES, CHANNELS, EVERYONE, GUILD_SETTINGS, ROLES, WELCOME,
|
AUTOMOD, AUTOMOD_BLOCK, CATEGORIES, CHANNELS, EVERYONE, GUILD_SETTINGS, ROLES, SYSTEM_CHANNEL, WELCOME,
|
||||||
type CategoryPlan, type ChannelPlan, type OverwritePlan, type WebhookSlot,
|
type CategoryPlan, type ChannelPlan, type OverwritePlan, type TagPlan, type WebhookSlot,
|
||||||
} from './plan.ts';
|
} from './plan.ts';
|
||||||
|
|
||||||
const log = logger('discord:setup');
|
const log = logger('discord:setup');
|
||||||
@@ -241,7 +241,30 @@ function kindOf(type: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Kategorie und Kanal unterscheiden sich fuers Auftragen nur im Typ. */
|
/** Kategorie und Kanal unterscheiden sich fuers Auftragen nur im Typ. */
|
||||||
type AnyPlan = CategoryPlan & { type: number; topic?: string };
|
type AnyPlan = CategoryPlan & { type: number; topic?: string; tags?: TagPlan[]; reaction?: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tags eines Forums: die aus dem Plan nachziehen, fremde stehen lassen.
|
||||||
|
* Discord ersetzt beim PATCH die ganze Liste, deshalb gehen die vorhandenen
|
||||||
|
* mit ihrer ID wieder mit - ohne ID waere es ein neuer Tag, und Beitraege
|
||||||
|
* verloeren ihre Markierung. Gibt null zurueck, wenn alles passt.
|
||||||
|
*/
|
||||||
|
export function mergeTags(existing: ApiTag[], plan: TagPlan[]): ApiTag[] | null {
|
||||||
|
let changed = false;
|
||||||
|
const out = existing.map((t) => ({ ...t }));
|
||||||
|
for (const p of plan) {
|
||||||
|
const want = { name: p.name, emoji_name: p.emoji, moderated: p.moderated ?? false };
|
||||||
|
const hit = out.find((t) => eq(t.name, p.name));
|
||||||
|
if (!hit) {
|
||||||
|
out.push(want);
|
||||||
|
changed = true;
|
||||||
|
} else if (hit.name !== want.name || hit.emoji_name !== want.emoji_name || hit.moderated !== want.moderated) {
|
||||||
|
Object.assign(hit, want, { emoji_id: null });
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed ? out : null;
|
||||||
|
}
|
||||||
|
|
||||||
async function applyChannel(
|
async function applyChannel(
|
||||||
api: Rest, guild: ApiGuild, existing: ApiChannel[], steps: Step[], dry: boolean,
|
api: Rest, guild: ApiGuild, existing: ApiChannel[], steps: Step[], dry: boolean,
|
||||||
@@ -268,6 +291,8 @@ async function applyChannel(
|
|||||||
...(parentId ? { parent_id: parentId } : {}),
|
...(parentId ? { parent_id: parentId } : {}),
|
||||||
...(plan.topic ? { topic: plan.topic } : {}),
|
...(plan.topic ? { topic: plan.topic } : {}),
|
||||||
...(overwrites.length ? { permission_overwrites: overwrites } : {}),
|
...(overwrites.length ? { permission_overwrites: overwrites } : {}),
|
||||||
|
...(plan.tags?.length ? { available_tags: mergeTags([], plan.tags) } : {}),
|
||||||
|
...(plan.reaction ? { default_reaction_emoji: { emoji_id: null, emoji_name: plan.reaction } } : {}),
|
||||||
}, 'Kanal aus dem Bauplan');
|
}, 'Kanal aus dem Bauplan');
|
||||||
steps.push({ kind, name: plan.name, change: 'erstellt' });
|
steps.push({ kind, name: plan.name, change: 'erstellt' });
|
||||||
return made;
|
return made;
|
||||||
@@ -280,6 +305,11 @@ async function applyChannel(
|
|||||||
if (parentId && found.parent_id !== parentId) patch['parent_id'] = parentId;
|
if (parentId && found.parent_id !== parentId) patch['parent_id'] = parentId;
|
||||||
const merged = mergeOverwrites(found.permission_overwrites ?? [], plan.overwrites ?? [], roleId);
|
const merged = mergeOverwrites(found.permission_overwrites ?? [], plan.overwrites ?? [], roleId);
|
||||||
if (merged) patch['permission_overwrites'] = merged;
|
if (merged) patch['permission_overwrites'] = merged;
|
||||||
|
const tags = plan.tags?.length ? mergeTags(found.available_tags ?? [], plan.tags) : null;
|
||||||
|
if (tags) patch['available_tags'] = tags;
|
||||||
|
if (plan.reaction && found.default_reaction_emoji?.emoji_name !== plan.reaction) {
|
||||||
|
patch['default_reaction_emoji'] = { emoji_id: null, emoji_name: plan.reaction };
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(patch).length === 0) {
|
if (Object.keys(patch).length === 0) {
|
||||||
steps.push({ kind, name: plan.name, change: 'unveraendert' });
|
steps.push({ kind, name: plan.name, change: 'unveraendert' });
|
||||||
@@ -525,7 +555,13 @@ export async function applyPlan(
|
|||||||
await applyWebhooks(api, channel, plan, steps, dry, urls);
|
await applyWebhooks(api, channel, plan, steps, dry, urls);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. AutoMod und Begruessung - beides braucht Kanaele, die es geben muss.
|
// 4. Beitrittsmeldungen, AutoMod und Begruessung - alles braucht Kanaele,
|
||||||
|
// die es geben muss.
|
||||||
|
const system = made.get(SYSTEM_CHANNEL);
|
||||||
|
if (system && guild.system_channel_id !== system.id) {
|
||||||
|
if (!dry) await api.patch(`/guilds/${guild.id}`, { system_channel_id: system.id }, 'Beitrittsmeldungen aus dem Bauplan');
|
||||||
|
steps.push({ kind: 'Server', name: `Beitrittsmeldungen nach ${system.name}`, change: dry ? 'erstellt' : 'angepasst' });
|
||||||
|
}
|
||||||
await applyAutomod(api, guild, made.get('technik-log')?.id ?? null, steps, dry);
|
await applyAutomod(api, guild, made.get('technik-log')?.id ?? null, steps, dry);
|
||||||
const welcome = made.get('willkommen');
|
const welcome = made.get('willkommen');
|
||||||
if (welcome) {
|
if (welcome) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { P, bits, names, str } from './permissions.ts';
|
import { P, bits, names, str } from './permissions.ts';
|
||||||
import { AUTOMOD, AUTOMOD_BLOCK, CATEGORIES, CHANNELS, EVERYONE, ROLES, WELCOME } from './plan.ts';
|
import { AUTOMOD, AUTOMOD_BLOCK, CATEGORIES, CHANNELS, EVERYONE, ROLES, SYSTEM_CHANNEL, WELCOME } from './plan.ts';
|
||||||
import { findChannel, mergeOverwrites, normalizeName, rolePositions } from './apply.ts';
|
import { findChannel, mergeOverwrites, normalizeName, rolePositions } from './apply.ts';
|
||||||
import { CH } from './api.ts';
|
import { CH } from './api.ts';
|
||||||
|
|
||||||
@@ -180,6 +180,16 @@ test('der Begruessungstext verweist nur auf Kanaele, die angelegt werden', () =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Beitritte landen in einem eigenen, beschreibbaren Kanal', () => {
|
||||||
|
// Ohne Angabe nimmt Discord den ersten Kanal, und das war #allgemein.
|
||||||
|
const c = CHANNELS.find((x) => x.key === SYSTEM_CHANNEL);
|
||||||
|
assert.ok(c, `${SYSTEM_CHANNEL} gibt es nicht`);
|
||||||
|
assert.notEqual(c.key, 'allgemein');
|
||||||
|
// Der Winke-Knopf unter der Meldung schreibt im Namen dessen, der klickt.
|
||||||
|
const deny = c.overwrites?.find((o) => o.role === '@everyone')?.deny ?? 0n;
|
||||||
|
assert.equal(deny & P.SEND_MESSAGES, 0n);
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Merge
|
// ---------------------------------------------------------------- Merge
|
||||||
|
|
||||||
const ID = (name: string): string | undefined =>
|
const ID = (name: string): string | undefined =>
|
||||||
@@ -334,3 +344,16 @@ test('die AutoMod-Regel kennt ihren frueheren Namen', () => {
|
|||||||
assert.ok(regel?.aliases?.includes('Erwaehnungs-Spam'),
|
assert.ok(regel?.aliases?.includes('Erwaehnungs-Spam'),
|
||||||
'sonst steht die Regel nach der Umbenennung zweimal da');
|
'sonst steht die Regel nach der Umbenennung zweimal da');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Tags und Standard-Reaktion stehen nur an Foren und passen in Discords Grenzen', () => {
|
||||||
|
for (const c of CHANNELS) {
|
||||||
|
if (c.type !== CH.FORUM) {
|
||||||
|
assert.equal(c.tags, undefined, `${c.name}: Tags gibt es nur in Foren`);
|
||||||
|
assert.equal(c.reaction, undefined, `${c.name}: Standard-Reaktion gibt es nur in Foren`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert.ok((c.tags?.length ?? 0) <= 20, `${c.name}: hoechstens 20 Tags`);
|
||||||
|
for (const t of c.tags ?? []) assert.ok(t.name.length <= 20, `Tag ${t.name} ist laenger als 20 Zeichen`);
|
||||||
|
assert.ok((c.topic?.length ?? 0) <= 4096, `${c.name}: Richtlinien zu lang`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -49,16 +49,30 @@ export type CategoryPlan = {
|
|||||||
overwrites?: OverwritePlan[];
|
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 = {
|
export type ChannelPlan = {
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
type: number;
|
type: number;
|
||||||
|
/** Bei Foren sind das die Richtlinien, die ueber jedem neuen Beitrag stehen. */
|
||||||
topic?: string;
|
topic?: string;
|
||||||
/** `key` einer Kategorie aus CATEGORIES. */
|
/** `key` einer Kategorie aus CATEGORIES. */
|
||||||
category?: string;
|
category?: string;
|
||||||
overwrites?: OverwritePlan[];
|
overwrites?: OverwritePlan[];
|
||||||
webhooks?: Array<{ name: string; slot: WebhookSlot }>;
|
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
|
// ---------------------------------------------------------------- Rechte
|
||||||
@@ -184,6 +198,17 @@ export const CHANNELS: ChannelPlan[] = [
|
|||||||
topic: 'Was das hier ist und die drei Regeln.',
|
topic: 'Was das hier ist und die drei Regeln.',
|
||||||
overwrites: READONLY,
|
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',
|
key: 'ankuendigungen',
|
||||||
name: '📢│ankündigungen',
|
name: '📢│ankündigungen',
|
||||||
@@ -214,6 +239,44 @@ export const CHANNELS: ChannelPlan[] = [
|
|||||||
topic: 'Gute Momente aus dem Stream. Der Clip-Knopf postet hier hin.',
|
topic: 'Gute Momente aus dem Stream. Der Clip-Knopf postet hier hin.',
|
||||||
webhooks: [{ name: 'Clips', slot: 'clipsWebhook' }],
|
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',
|
key: 'voice',
|
||||||
name: '🔊 Zusammen zocken',
|
name: '🔊 Zusammen zocken',
|
||||||
@@ -260,6 +323,14 @@ export const GUILD_SETTINGS = {
|
|||||||
system_channel_flags: 4, // Einrichtungstipps unterdruecken, Beitritte zeigen
|
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.
|
* Wird einmalig in #willkommen gepostet, wenn der Kanal noch leer ist.
|
||||||
* {key} wird durch die Erwaehnung des Kanals mit diesem `key` ersetzt.
|
* {key} wird durch die Erwaehnung des Kanals mit diesem `key` ersetzt.
|
||||||
@@ -276,6 +347,8 @@ Das hier ist der Ort zwischen den Streams. Wer mitliest, gehört dazu.
|
|||||||
**Wo was passiert**
|
**Wo was passiert**
|
||||||
> {allgemein} - alles andere
|
> {allgemein} - alles andere
|
||||||
> {clips} - die guten Momente
|
> {clips} - die guten Momente
|
||||||
|
> {spielvorschlaege} - was Abdu mal spielen sollte
|
||||||
|
> {reaction} - Clips, auf die Abdu reagieren soll
|
||||||
> {ankuendigungen} - Stream-Start und die Zusammenfassung danach
|
> {ankuendigungen} - Stream-Start und die Zusammenfassung danach
|
||||||
|
|
||||||
**Bescheid wissen, wenn der Stream startet**
|
**Bescheid wissen, wenn der Stream startet**
|
||||||
|
|||||||
Reference in new issue
Block a user