Moderation & Kontakt + Game-Server-Monitor
- Modmail: DMs an den Bot landen als Threads im privaten Staff-Kanal, Thread-Antworten gehen als DM zurück (Zustell-Feedback per Reaktion) - Willkommens-Embed für neue Member (GuildMemberAdd; Server-Members-Intent nötig) - Mod-Log: gelöschte/bearbeitete User-Nachrichten in privaten Log-Kanal - Server-Monitor: FiveM-kompatible Endpoints (/dynamic.json) alle 2 min, persistentes Status-Embed (edit in place) + Spielerzahl als Bot-Presence - Neue Intents: DirectMessages, GuildMembers; Partials.Channel - Setup-Seite: Sektionen // Moderation & Kontakt und // Game-Server Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,10 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St
|
||||
| 🧪 **Playtester-Programm** | `/playtester-setup` postet den Bewerbungs-Button; Rolle + Liste (Setup-Seite); Alpha-Keys später per DM über die API |
|
||||
| ⭐ **Starboard** | Nachrichten mit genug ⭐-Reaktionen landen automatisch im Best-of-Kanal |
|
||||
| 📸 **Screenshot-Galerie** | Bilder aus dem Screenshot-Kanal → öffentliche `/galerie` (lokal gespeichert); Historie via `/galerie-backfill` |
|
||||
| 📬 **Modmail** | DM an den Bot → Thread im privaten Staff-Kanal; Antworten im Thread gehen als DM zurück |
|
||||
| 👋 **Willkommens-Embed** | Begrüßung neuer Member im Brand-Look (braucht Server-Members-Intent) |
|
||||
| 📋 **Mod-Log** | Gelöschte/bearbeitete Nachrichten in einen privaten Log-Kanal |
|
||||
| 🎮 **Server-Monitor** | FiveM-kompatible Game-Server (/dynamic.json): Live-Status-Embed + Spielerzahl in der Bot-Presence |
|
||||
| 🏓 `/ping`, 🗄 `/devlog-backfill` | Lebenszeichen · Kanal-Historie nacharchivieren (Admin) |
|
||||
|
||||
### Webinterface (`bot.d4rkst3r.de`)
|
||||
@@ -62,7 +66,8 @@ kein Redeploy nötig:
|
||||
1. **New Application** → Name vergeben
|
||||
2. **General Information** → Application ID = `DISCORD_CLIENT_ID`
|
||||
3. **Bot** → Reset Token = `DISCORD_TOKEN` (wird nur einmal angezeigt!)
|
||||
und **Message Content Intent** aktivieren (Devlog-Listener)
|
||||
und **Message Content Intent** + **Server Members Intent** aktivieren
|
||||
(Devlog-Listener bzw. Willkommens-Embeds)
|
||||
4. **OAuth2** → Client Secret = `DISCORD_CLIENT_SECRET`;
|
||||
unter **Redirects** eintragen:
|
||||
- `https://bot.d4rkst3r.de/auth/callback`
|
||||
|
||||
@@ -357,6 +357,65 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Moderation & Kontakt */}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Moderation & Kontakt</h2>
|
||||
<div className="field">
|
||||
<label>Modmail-Kanal <span className="field-hint">PRIVATER Staff-Kanal — DMs an den Bot landen hier als Threads</span></label>
|
||||
<select
|
||||
value={form.modmail_channel_id ?? ''}
|
||||
onChange={(e) => setForm({ ...form, modmail_channel_id: e.target.value })}
|
||||
>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Willkommens-Kanal <span className="field-hint">Begrüßungs-Embed für neue Member (braucht Server-Members-Intent im Dev-Portal!)</span></label>
|
||||
<select
|
||||
value={form.welcome_channel_id ?? ''}
|
||||
onChange={(e) => setForm({ ...form, welcome_channel_id: e.target.value })}
|
||||
>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Mod-Log-Kanal <span className="field-hint">PRIVAT — gelöschte/bearbeitete Nachrichten</span></label>
|
||||
<select
|
||||
value={form.modlog_channel_id ?? ''}
|
||||
onChange={(e) => setForm({ ...form, modlog_channel_id: e.target.value })}
|
||||
>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Game-Server */}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Game-Server</h2>
|
||||
<div className="field">
|
||||
<label>Status-Kanal <span className="field-hint">persistentes Status-Embed, alle 2 min aktualisiert</span></label>
|
||||
<select
|
||||
value={form.status_channel_id ?? ''}
|
||||
onChange={(e) => setForm({ ...form, status_channel_id: e.target.value })}
|
||||
>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Server <span className="field-hint">kommagetrennt Name=URL — FiveM-kompatibel (/dynamic.json)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="z. B. FiveM=http://1.2.3.4:30120, EcoGame=http://…"
|
||||
value={form.gameservers}
|
||||
onChange={(e) => setForm({ ...form, gameservers: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backups */}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Backups</h2>
|
||||
|
||||
+8
-2
@@ -4,6 +4,7 @@ import { config } from '../config.js';
|
||||
import { devlogChannelId, devlogPingRoleId, playtesterRoleId } from '../runtime-settings.js';
|
||||
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
|
||||
import { registerCommunityListeners } from './community.js';
|
||||
import { registerModTools } from './mod-tools.js';
|
||||
import { addPlaytester, removePlaytester } from '../db.js';
|
||||
import * as ping from './commands/ping.js';
|
||||
import * as devlogBackfill from './commands/devlog-backfill.js';
|
||||
@@ -24,9 +25,13 @@ export async function startBot() {
|
||||
GatewayIntentBits.MessageContent,
|
||||
// Für das Starboard (⭐-Reaktionen)
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
// Für Modmail (DMs) — Willkommens-Embeds brauchen zusätzlich den
|
||||
// privilegierten "Server Members Intent" im Developer Portal!
|
||||
GatewayIntentBits.DirectMessages,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
],
|
||||
// Partials, damit Delete/Reaction-Events auch für ungecachte Nachrichten feuern
|
||||
partials: [Partials.Message, Partials.Reaction],
|
||||
// Partials: Delete/Reaction-Events für ungecachte Nachrichten + DM-Kanäle
|
||||
partials: [Partials.Message, Partials.Reaction, Partials.Channel],
|
||||
});
|
||||
|
||||
// Commands in Collection ablegen für schnellen Zugriff im Interaction-Handler
|
||||
@@ -145,6 +150,7 @@ export async function startBot() {
|
||||
});
|
||||
|
||||
registerCommunityListeners(client);
|
||||
registerModTools(client);
|
||||
|
||||
await client.login(config.discordToken);
|
||||
return client;
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// Moderation & Kontakt: Modmail (DM ↔ Staff-Thread), Willkommens-Embed, Mod-Log
|
||||
import { ChannelType, EmbedBuilder, Events } from 'discord.js';
|
||||
import { saveModmail, modmailByUser, modmailByThread } from '../db.js';
|
||||
import { modmailChannelId, welcomeChannelId, modlogChannelId, publicUrl } from '../runtime-settings.js';
|
||||
|
||||
const BRAND_YELLOW = 0xf5c518;
|
||||
const BRAND_ORANGE = 0xff4d00;
|
||||
|
||||
/* ── Modmail ───────────────────────────────────────── */
|
||||
|
||||
/** Staff-Thread für einen User finden oder anlegen */
|
||||
async function ensureModmailThread(client, user) {
|
||||
const channelId = modmailChannelId();
|
||||
if (!channelId) return null;
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
if (!channel?.threads) return null;
|
||||
|
||||
const existing = modmailByUser(user.id);
|
||||
if (existing) {
|
||||
const thread = await client.channels.fetch(existing.thread_id).catch(() => null);
|
||||
if (thread) {
|
||||
if (thread.archived) await thread.setArchived(false).catch(() => {});
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
|
||||
const thread = await channel.threads.create({
|
||||
name: `📬 ${user.username}`,
|
||||
autoArchiveDuration: 10080, // 7 Tage
|
||||
type: ChannelType.PublicThread,
|
||||
});
|
||||
saveModmail(user.id, thread.id, user.username);
|
||||
await thread.send({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(BRAND_YELLOW)
|
||||
.setDescription(
|
||||
`Neue Modmail-Konversation mit **${user.username}** (<@${user.id}>).\n` +
|
||||
'Antworten in diesem Thread gehen als DM an den User.'
|
||||
)
|
||||
.setFooter({ text: 'D4RKST3R // MODMAIL' }),
|
||||
],
|
||||
});
|
||||
return thread;
|
||||
}
|
||||
|
||||
/** DM vom User → in den Staff-Thread spiegeln */
|
||||
async function handleIncomingDm(client, message) {
|
||||
const thread = await ensureModmailThread(client, message.author);
|
||||
if (!thread) return; // Feature aus → DM ignorieren
|
||||
|
||||
const files = [...message.attachments.values()].map((a) => a.url).slice(0, 5);
|
||||
await thread.send({
|
||||
content: `**${message.author.username}:** ${message.content || ''}${
|
||||
files.length ? `\n${files.join('\n')}` : ''
|
||||
}`.slice(0, 2000),
|
||||
allowedMentions: { parse: [] },
|
||||
});
|
||||
await message.react('📬').catch(() => {});
|
||||
}
|
||||
|
||||
/** Staff-Antwort im Thread → als DM an den User */
|
||||
async function handleThreadReply(client, message) {
|
||||
const row = modmailByThread(message.channelId);
|
||||
if (!row) return;
|
||||
|
||||
const user = await client.users.fetch(row.user_id).catch(() => null);
|
||||
if (!user) return;
|
||||
try {
|
||||
await user.send(
|
||||
`**${message.member?.displayName ?? message.author.username}:** ${message.content}`.slice(0, 2000)
|
||||
);
|
||||
await message.react('✅').catch(() => {});
|
||||
} catch {
|
||||
await message.react('⚠️').catch(() => {});
|
||||
await message.reply('DM nicht zustellbar (User blockt DMs).').catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Willkommens-Embed ─────────────────────────────── */
|
||||
|
||||
async function handleMemberAdd(member) {
|
||||
const channelId = welcomeChannelId();
|
||||
if (!channelId) return;
|
||||
const channel = await member.client.channels.fetch(channelId).catch(() => null);
|
||||
if (!channel?.isTextBased()) return;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(BRAND_YELLOW)
|
||||
.setTitle(`👋 Willkommen, ${member.displayName}!`)
|
||||
.setThumbnail(member.user.displayAvatarURL({ size: 128 }))
|
||||
.setDescription(
|
||||
`Schön, dass du da bist — du bist Mitglied **#${member.guild.memberCount}**.\n\n` +
|
||||
`📔 Devlogs & Roadmap: ${publicUrl()}\n` +
|
||||
'🧪 Playtester werden? Schau nach dem Bewerbungs-Post!\n' +
|
||||
'🐛 Bug gefunden? Einfach `/bug` benutzen.'
|
||||
)
|
||||
.setFooter({ text: 'D4RKST3R // COMMUNITY' })
|
||||
.setTimestamp();
|
||||
|
||||
await channel.send({ content: `<@${member.id}>`, embeds: [embed] });
|
||||
}
|
||||
|
||||
/* ── Mod-Log ───────────────────────────────────────── */
|
||||
|
||||
async function logToModlog(client, embed) {
|
||||
const channelId = modlogChannelId();
|
||||
if (!channelId) return;
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
if (!channel?.isTextBased()) return;
|
||||
await channel.send({ embeds: [embed] });
|
||||
}
|
||||
|
||||
function trimmed(text) {
|
||||
const t = (text ?? '').trim();
|
||||
return t.length > 900 ? `${t.slice(0, 900)}…` : t || '*[kein Text / nicht im Cache]*';
|
||||
}
|
||||
|
||||
/* ── Registrierung ─────────────────────────────────── */
|
||||
|
||||
export function registerModTools(client) {
|
||||
// Modmail: eingehende DMs + Staff-Antworten in Threads
|
||||
client.on(Events.MessageCreate, async (message) => {
|
||||
try {
|
||||
if (message.author?.bot) return;
|
||||
if (message.channel?.type === ChannelType.DM) {
|
||||
await handleIncomingDm(client, message);
|
||||
} else if (
|
||||
message.channel?.isThread?.() &&
|
||||
message.channel.parentId === modmailChannelId()
|
||||
) {
|
||||
await handleThreadReply(client, message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[modmail] Fehler:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Willkommens-Embed
|
||||
client.on(Events.GuildMemberAdd, async (member) => {
|
||||
try {
|
||||
await handleMemberAdd(member);
|
||||
} catch (error) {
|
||||
console.error('[welcome] Fehler:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Mod-Log: gelöschte + bearbeitete Nachrichten (User-Nachrichten in Guild-Kanälen)
|
||||
client.on(Events.MessageDelete, async (message) => {
|
||||
try {
|
||||
if (!message.guildId || message.author?.bot) return;
|
||||
if (message.channelId === modlogChannelId()) return;
|
||||
await logToModlog(client, new EmbedBuilder()
|
||||
.setColor(BRAND_ORANGE)
|
||||
.setTitle('🗑️ Nachricht gelöscht')
|
||||
.addFields(
|
||||
{ name: 'User', value: message.author ? `${message.author.username}` : 'unbekannt', inline: true },
|
||||
{ name: 'Kanal', value: `<#${message.channelId}>`, inline: true },
|
||||
{ name: 'Inhalt', value: trimmed(message.content) }
|
||||
)
|
||||
.setTimestamp());
|
||||
} catch (error) {
|
||||
console.error('[modlog] Fehler:', error);
|
||||
}
|
||||
});
|
||||
|
||||
client.on(Events.MessageUpdate, async (oldMessage, newMessage) => {
|
||||
try {
|
||||
if (!newMessage.guildId || newMessage.author?.bot) return;
|
||||
if (oldMessage.content === newMessage.content) return; // z. B. nur Embed-Update
|
||||
await logToModlog(client, new EmbedBuilder()
|
||||
.setColor(BRAND_YELLOW)
|
||||
.setTitle('✏️ Nachricht bearbeitet')
|
||||
.addFields(
|
||||
{ name: 'User', value: newMessage.author?.username ?? 'unbekannt', inline: true },
|
||||
{ name: 'Kanal', value: `<#${newMessage.channelId}>`, inline: true },
|
||||
{ name: 'Vorher', value: trimmed(oldMessage.content) },
|
||||
{ name: 'Nachher', value: trimmed(newMessage.content) }
|
||||
)
|
||||
.setTimestamp());
|
||||
} catch (error) {
|
||||
console.error('[modlog] Fehler:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Game-Server-Monitor: pollt FiveM-kompatible HTTP-Endpoints (/dynamic.json)
|
||||
// und pflegt ein persistentes Status-Embed im Status-Kanal + Bot-Presence.
|
||||
// Setting "gameservers": kommagetrennt "Name=http://ip:30120, Name2=…"
|
||||
import { ActivityType, EmbedBuilder, Events } from 'discord.js';
|
||||
import { getSetting, setSetting } from '../db.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
||||
const TIMEOUT_MS = 8000;
|
||||
const BRAND_YELLOW = 0xf5c518;
|
||||
|
||||
function serverList() {
|
||||
return (getSetting('gameservers') ?? '')
|
||||
.split(',')
|
||||
.map((entry) => {
|
||||
const [name, ...rest] = entry.split('=');
|
||||
const url = rest.join('=').trim().replace(/\/$/, '');
|
||||
return name?.trim() && /^https?:\/\//.test(url) ? { name: name.trim(), url } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** Einen Server abfragen (FiveM-Format: clients, sv_maxclients, hostname) */
|
||||
async function queryServer(server) {
|
||||
try {
|
||||
const res = await fetch(`${server.url}/dynamic.json`, {
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/1.0' },
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
return {
|
||||
...server,
|
||||
online: true,
|
||||
players: Number(data.clients) || 0,
|
||||
max: Number(data.sv_maxclients) || 0,
|
||||
map: data.mapname ?? null,
|
||||
};
|
||||
} catch {
|
||||
return { ...server, online: false, players: 0, max: 0, map: null };
|
||||
}
|
||||
}
|
||||
|
||||
/** Status-Embed bauen */
|
||||
function buildEmbed(results) {
|
||||
const lines = results.map((r) =>
|
||||
r.online
|
||||
? `🟢 **${r.name}** — ${r.players}/${r.max} Spieler${r.map ? ` · ${r.map}` : ''}`
|
||||
: `🔴 **${r.name}** — offline`
|
||||
);
|
||||
return new EmbedBuilder()
|
||||
.setColor(BRAND_YELLOW)
|
||||
.setTitle('🎮 Server-Status')
|
||||
.setDescription(lines.join('\n'))
|
||||
.setFooter({ text: 'D4RKST3R // STATUS • aktualisiert alle 2 min' })
|
||||
.setTimestamp();
|
||||
}
|
||||
|
||||
/** Ein Poll-Durchlauf — exportiert für Tests und den Timer */
|
||||
export async function monitorTick(client) {
|
||||
const servers = serverList();
|
||||
const channelId = getSetting('status_channel_id');
|
||||
if (servers.length === 0) return;
|
||||
|
||||
const results = await Promise.all(servers.map(queryServer));
|
||||
|
||||
// Bot-Presence: Gesamtspielerzahl (nur wenn mindestens ein Server online)
|
||||
const online = results.filter((r) => r.online);
|
||||
const totalPlayers = online.reduce((sum, r) => sum + r.players, 0);
|
||||
try {
|
||||
client.user?.setActivity?.(
|
||||
online.length > 0 ? `${totalPlayers} Spieler online` : 'Server offline',
|
||||
{ type: ActivityType.Watching }
|
||||
);
|
||||
} catch { /* Presence ist nice-to-have */ }
|
||||
|
||||
// Persistentes Embed im Status-Kanal pflegen (einmal posten, danach editieren)
|
||||
if (!channelId) return;
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
if (!channel?.isTextBased()) return;
|
||||
|
||||
const embed = buildEmbed(results);
|
||||
const messageId = getSetting('status_message_id');
|
||||
if (messageId) {
|
||||
const existing = await channel.messages.fetch(messageId).catch(() => null);
|
||||
if (existing) {
|
||||
await existing.edit({ embeds: [embed] });
|
||||
return;
|
||||
}
|
||||
}
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
setSetting('status_message_id', message.id);
|
||||
}
|
||||
|
||||
export function startServerMonitor(client) {
|
||||
setInterval(() => monitorTick(client).catch((e) => console.error('[monitor] Fehler:', e)), CHECK_INTERVAL_MS);
|
||||
client.once(Events.ClientReady, () => monitorTick(client).catch(() => {}));
|
||||
}
|
||||
@@ -178,6 +178,25 @@ export const listGallery = (limit, offset) => ({
|
||||
total: countGalleryStmt.get().n,
|
||||
});
|
||||
|
||||
// Modmail: DM-Konversationen ↔ Staff-Threads
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS modmail (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
thread_id TEXT NOT NULL UNIQUE,
|
||||
username TEXT,
|
||||
opened_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
const upsertModmail = db.prepare(`
|
||||
INSERT INTO modmail (user_id, thread_id, username) VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET thread_id = excluded.thread_id, username = excluded.username
|
||||
`);
|
||||
const modmailByUserStmt = db.prepare('SELECT * FROM modmail WHERE user_id = ?');
|
||||
const modmailByThreadStmt = db.prepare('SELECT * FROM modmail WHERE thread_id = ?');
|
||||
export const saveModmail = (userId, threadId, username) => upsertModmail.run(userId, threadId, username);
|
||||
export const modmailByUser = (userId) => modmailByUserStmt.get(userId) ?? null;
|
||||
export const modmailByThread = (threadId) => modmailByThreadStmt.get(threadId) ?? null;
|
||||
|
||||
// API-Keys für externe Skripte (/api/v1/*) — nur der SHA-256-Hash wird gespeichert
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { startWebServer } from './web/server.js';
|
||||
import { scheduleWeeklyRecap } from './bot/weekly-recap.js';
|
||||
import { startWatchdog } from './bot/watchdog.js';
|
||||
import { scheduleBackups } from './backup.js';
|
||||
import { startServerMonitor } from './bot/server-monitor.js';
|
||||
|
||||
process.on('unhandledRejection', (error) => {
|
||||
console.error('[main] Unhandled Rejection:', error);
|
||||
@@ -15,6 +16,7 @@ try {
|
||||
scheduleWeeklyRecap(client);
|
||||
startWatchdog(client);
|
||||
scheduleBackups(client);
|
||||
startServerMonitor(client);
|
||||
} catch (error) {
|
||||
console.error('[main] Start fehlgeschlagen:', error);
|
||||
process.exit(1);
|
||||
|
||||
@@ -47,6 +47,21 @@ export function starboardThreshold() {
|
||||
return Number.isInteger(n) && n >= 1 ? n : 3;
|
||||
}
|
||||
|
||||
/** Modmail-Staff-Kanal (privat!) — leer = Feature aus */
|
||||
export function modmailChannelId() {
|
||||
return getSetting('modmail_channel_id') || null;
|
||||
}
|
||||
|
||||
/** Willkommens-Kanal — leer = Feature aus */
|
||||
export function welcomeChannelId() {
|
||||
return getSetting('welcome_channel_id') || null;
|
||||
}
|
||||
|
||||
/** Mod-Log-Kanal (privat!) — leer = Feature aus */
|
||||
export function modlogChannelId() {
|
||||
return getSetting('modlog_channel_id') || null;
|
||||
}
|
||||
|
||||
/** Screenshot-Kanal für die Galerie — leer = Feature aus */
|
||||
export function screenshotChannelId() {
|
||||
return getSetting('screenshot_channel_id') || null;
|
||||
|
||||
+11
-2
@@ -203,6 +203,11 @@ ${rssItems}
|
||||
starboard_channel_id: getSetting('starboard_channel_id') ?? '',
|
||||
starboard_threshold: Number(getSetting('starboard_threshold')) || 3,
|
||||
screenshot_channel_id: getSetting('screenshot_channel_id') ?? '',
|
||||
modmail_channel_id: getSetting('modmail_channel_id') ?? '',
|
||||
welcome_channel_id: getSetting('welcome_channel_id') ?? '',
|
||||
modlog_channel_id: getSetting('modlog_channel_id') ?? '',
|
||||
status_channel_id: getSetting('status_channel_id') ?? '',
|
||||
gameservers: getSetting('gameservers') ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,7 +236,11 @@ ${rssItems}
|
||||
const body = request.body ?? {};
|
||||
|
||||
// Kanäle: müssen existierende Textkanäle sein (optionale dürfen leer sein = aus)
|
||||
const OPTIONAL_CHANNELS = ['release_channel_id', 'backup_channel_id', 'starboard_channel_id', 'screenshot_channel_id'];
|
||||
const OPTIONAL_CHANNELS = [
|
||||
'release_channel_id', 'backup_channel_id', 'starboard_channel_id',
|
||||
'screenshot_channel_id', 'modmail_channel_id', 'welcome_channel_id',
|
||||
'modlog_channel_id', 'status_channel_id',
|
||||
];
|
||||
for (const key of ['commit_channel_id', 'devlog_channel_id', ...OPTIONAL_CHANNELS]) {
|
||||
if (body[key] === undefined) continue;
|
||||
const value = String(body[key]);
|
||||
@@ -279,7 +288,7 @@ ${rssItems}
|
||||
if (body.backup_enabled !== undefined) {
|
||||
setSetting('backup_enabled', body.backup_enabled ? '1' : '0');
|
||||
}
|
||||
for (const key of ['commit_branch_filter', 'ignored_repos', 'bug_report_repo', 'watchdog_urls', 'roadmap_repo']) {
|
||||
for (const key of ['commit_branch_filter', 'ignored_repos', 'bug_report_repo', 'watchdog_urls', 'roadmap_repo', 'gameservers']) {
|
||||
if (body[key] !== undefined) {
|
||||
setSetting(key, String(body[key]).trim());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user