AutoMod sichtbar machen: Treffer ins Protokoll, Regeln ins Panel
Discords eigener Filter arbeitet serverseitig und ohne Latenz — daran ist nichts zu verbessern. Was fehlte, war die Sicht darauf: Treffer landeten nur in Discords eigenem Kanal, und die Regeln pflegte man in einer ganz anderen Oberflaeche als alles andere. Jetzt spiegelt der Bot jeden Treffer als Embed ins Mod-Log — mit Nutzer, Regel, Ausloeser, Massnahme, getroffenem Wort und gekuerztem Wortlaut, ohne Erwaehnungen, die den Kanal anpingen. Im Panel steht unter Support jede Regel mit Ausloeser, Massnahmen und der Zahl ausgenommener Rollen und Kanaele — letzteres oft der Grund, warum eine Regel scheinbar nicht greift. Schalten geht dort direkt. Das Bearbeiten der Wortlisten bleibt bewusst in Discord; dort ist es gut geloest, und ein zweiter Ort dafuer waere nur eine weitere Stelle, an der etwas auseinanderlaeuft. Dazu noetig: der Gateway-Intent AutoModerationExecution. Nicht privilegiert, aber Discord schickt die Ereignisse nur an Bots mit "Server verwalten" — fehlt das Recht, sagt das Panel es statt still leer zu bleiben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -302,6 +302,8 @@ export default {
|
|||||||
'feat.moderation.desc': 'Warnings, timeouts, cleanup — with history and a log.',
|
'feat.moderation.desc': 'Warnings, timeouts, cleanup — with history and a log.',
|
||||||
'feat.anti_raid': 'Raid protection',
|
'feat.anti_raid': 'Raid protection',
|
||||||
'feat.anti_raid.desc': 'Spots waves of joins and locks the server down for a while.',
|
'feat.anti_raid.desc': 'Spots waves of joins and locks the server down for a while.',
|
||||||
|
'feat.automod': 'AutoMod log',
|
||||||
|
'feat.automod.desc': "Hits from Discord's own filter land in the log; rules can be switched here.",
|
||||||
'feat.modlog': 'Audit log',
|
'feat.modlog': 'Audit log',
|
||||||
'feat.modlog.desc': 'Deleted and edited messages, joins, name and role changes.',
|
'feat.modlog.desc': 'Deleted and edited messages, joins, name and role changes.',
|
||||||
'feat.modmail': 'Modmail',
|
'feat.modmail': 'Modmail',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
IconArchive, IconMail, IconSupport, IconMagic, IconTeam, IconRefresh,
|
IconArchive, IconMail, IconSupport, IconMagic, IconTeam, IconRefresh,
|
||||||
IconWarning, IconServer, IconMonitor, IconVoice, IconZap, IconClock,
|
IconWarning, IconServer, IconMonitor, IconVoice, IconZap, IconClock,
|
||||||
IconSend, IconTag, IconDatabase, IconPackage, IconLock, IconIdea, IconLink,
|
IconSend, IconTag, IconDatabase, IconPackage, IconLock, IconIdea, IconLink,
|
||||||
|
IconBot,
|
||||||
} from './icons.jsx';
|
} from './icons.jsx';
|
||||||
|
|
||||||
export const MODULE_ICONS = {
|
export const MODULE_ICONS = {
|
||||||
@@ -19,7 +20,8 @@ export const MODULE_ICONS = {
|
|||||||
voting: IconThumbsUp, giveaways: IconGiveaway, birthdays: IconBirthday,
|
voting: IconThumbsUp, giveaways: IconGiveaway, birthdays: IconBirthday,
|
||||||
events: IconEvents, playtester: IconGame, alpha_keys: IconKey,
|
events: IconEvents, playtester: IconGame, alpha_keys: IconKey,
|
||||||
role_menus: IconRoles, applications: IconApplications,
|
role_menus: IconRoles, applications: IconApplications,
|
||||||
moderation: IconShield, anti_raid: IconLock, modlog: IconArchive, modmail: IconMail,
|
moderation: IconShield, anti_raid: IconLock, automod: IconBot,
|
||||||
|
modlog: IconArchive, modmail: IconMail,
|
||||||
linked_roles: IconLink, polls: IconThumbsUp,
|
linked_roles: IconLink, polls: IconThumbsUp,
|
||||||
tickets: IconSupport, welcome: IconMagic, autorole: IconTeam,
|
tickets: IconSupport, welcome: IconMagic, autorole: IconTeam,
|
||||||
sticky_roles: IconRefresh, bug_reports: IconWarning,
|
sticky_roles: IconRefresh, bug_reports: IconWarning,
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ export default function Settings({ me }) {
|
|||||||
const [audit, setAudit] = useState([]);
|
const [audit, setAudit] = useState([]);
|
||||||
const [serverStats, setServerStats] = useState(null);
|
const [serverStats, setServerStats] = useState(null);
|
||||||
const [watchdog, setWatchdog] = useState(null);
|
const [watchdog, setWatchdog] = useState(null);
|
||||||
|
const [automod, setAutomod] = useState(null);
|
||||||
const [dienstDraft, setDienstDraft] = useState({ id: null, name: '', url: '', gruppe: 'Dienste', oeffentlich: false });
|
const [dienstDraft, setDienstDraft] = useState({ id: null, name: '', url: '', gruppe: 'Dienste', oeffentlich: false });
|
||||||
const [owner, setOwner] = useState(null);
|
const [owner, setOwner] = useState(null);
|
||||||
const [auditAlle, setAuditAlle] = useState(false);
|
const [auditAlle, setAuditAlle] = useState(false);
|
||||||
@@ -471,6 +472,7 @@ export default function Settings({ me }) {
|
|||||||
// Fuers Dashboard: Mitgliederzahl und Nachrichten pro Tag
|
// Fuers Dashboard: Mitgliederzahl und Nachrichten pro Tag
|
||||||
apiGet('/api/serverstats').then(setServerStats).catch(() => {});
|
apiGet('/api/serverstats').then(setServerStats).catch(() => {});
|
||||||
apiGet('/api/watchdog').then(setWatchdog).catch(() => {});
|
apiGet('/api/watchdog').then(setWatchdog).catch(() => {});
|
||||||
|
apiGet('/api/automod').then(setAutomod).catch(() => {});
|
||||||
apiGet('/api/ssoapps').then((d) => setSsoApps(d.apps)).catch(() => {});
|
apiGet('/api/ssoapps').then((d) => setSsoApps(d.apps)).catch(() => {});
|
||||||
apiGet('/api/services').then((d) => setServices(d.services)).catch(() => {});
|
apiGet('/api/services').then((d) => setServices(d.services)).catch(() => {});
|
||||||
apiGet('/api/pages/all/list').then((d) => setPages(d.pages)).catch(() => {});
|
apiGet('/api/pages/all/list').then((d) => setPages(d.pages)).catch(() => {});
|
||||||
@@ -1818,8 +1820,76 @@ export default function Settings({ me }) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** AutoMod-Regel an- oder abschalten */
|
||||||
|
async function automodSchalten(id, aktiv) {
|
||||||
|
try {
|
||||||
|
await apiPut(`/api/automod/${id}`, { aktiv });
|
||||||
|
const frisch = await apiGet('/api/automod');
|
||||||
|
setAutomod(frisch);
|
||||||
|
} catch (e) {
|
||||||
|
setFeedback(e.body?.error ?? 'Umschalten fehlgeschlagen.');
|
||||||
|
setTimeout(() => setFeedback(null), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tabSupport = (
|
const tabSupport = (
|
||||||
<>
|
<>
|
||||||
|
{automod && (
|
||||||
|
<div className="settings-section">
|
||||||
|
<h2 className="settings-title">// AutoMod</h2>
|
||||||
|
<p className="section-intro">
|
||||||
|
Discords eigener Filter — er läuft serverseitig und ohne Umweg über den Bot.
|
||||||
|
Hier siehst du, was eingerichtet ist, und kannst Regeln schalten.
|
||||||
|
Wortlisten bearbeitest du in Discord unter <strong>Servereinstellungen → AutoMod</strong>.
|
||||||
|
{!automod.aktiv && ' Das Protokollieren der Treffer ist gerade ausgeschaltet.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{!automod.ok && (
|
||||||
|
<p className="notice">
|
||||||
|
Regeln nicht abrufbar: {automod.grund}
|
||||||
|
{' '}— fehlt dem Bot das Recht „Server verwalten"?
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{automod.ok && automod.regeln.length === 0 && (
|
||||||
|
<p className="notice">
|
||||||
|
Noch keine AutoMod-Regel eingerichtet. Anlegen geht in Discord unter
|
||||||
|
{' '}<strong>Servereinstellungen → AutoMod</strong>.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{automod.regeln.map((r) => (
|
||||||
|
<div className="am-row card" key={r.id}>
|
||||||
|
<div className="am-head">
|
||||||
|
<span className={`wd-dot ${r.aktiv ? 'an' : 'wartet'}`} />
|
||||||
|
<span className="st-name">{r.name}</span>
|
||||||
|
<span className="tm-chip">{r.ausloeser}</span>
|
||||||
|
<label className="toggle am-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={r.aktiv}
|
||||||
|
onChange={(e) => automodSchalten(r.id, e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>{r.aktiv ? 'aktiv' : 'aus'}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="am-meta">
|
||||||
|
{r.details.length > 0 && <span>{r.details.join(' · ')}</span>}
|
||||||
|
<span className="am-actions">
|
||||||
|
{r.aktionen.map((a) => <span className="tm-chip" key={a}>{a}</span>)}
|
||||||
|
</span>
|
||||||
|
{(r.ausnahmen.rollen > 0 || r.ausnahmen.kanaele > 0) && (
|
||||||
|
<span className="am-exempt">
|
||||||
|
ausgenommen: {[
|
||||||
|
r.ausnahmen.rollen > 0 && `${r.ausnahmen.rollen} ${r.ausnahmen.rollen === 1 ? 'Rolle' : 'Rollen'}`,
|
||||||
|
r.ausnahmen.kanaele > 0 && `${r.ausnahmen.kanaele} ${r.ausnahmen.kanaele === 1 ? 'Kanal' : 'Kanäle'}`,
|
||||||
|
].filter(Boolean).join(', ')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Moderation & Kontakt</h2>
|
<h2 className="settings-title">// Moderation & Kontakt</h2>
|
||||||
<div className="field" data-setting="modmail_channel_id">
|
<div className="field" data-setting="modmail_channel_id">
|
||||||
|
|||||||
@@ -1873,6 +1873,17 @@ label.toggle {
|
|||||||
font-size: .62rem; letter-spacing: .06em; color: var(--muted2); white-space: nowrap;
|
font-size: .62rem; letter-spacing: .06em; color: var(--muted2); white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── AUTOMOD-REGELN ────────────────────────────────── */
|
||||||
|
.am-row { padding: .9rem 1.1rem; margin-bottom: .7rem; min-width: 0; }
|
||||||
|
.am-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
|
||||||
|
.am-toggle { margin-left: auto; }
|
||||||
|
.am-meta {
|
||||||
|
display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
|
||||||
|
margin-top: .55rem; font-size: .78rem; color: var(--muted);
|
||||||
|
}
|
||||||
|
.am-actions { display: flex; gap: .35rem; flex-wrap: wrap; }
|
||||||
|
.am-exempt { font-family: var(--mono); font-size: .64rem; color: var(--muted2); }
|
||||||
|
|
||||||
/* ── ERREICHBARKEITS-BALKEN ────────────────────────── */
|
/* ── ERREICHBARKEITS-BALKEN ────────────────────────── */
|
||||||
/* Ein Strich pro Stunde, 168 Stück. Auf schmalen Schirmen werden die Striche
|
/* Ein Strich pro Stunde, 168 Stück. Auf schmalen Schirmen werden die Striche
|
||||||
dünner als ein Pixel — deshalb scrollt der Balken dort lieber, statt zu
|
dünner als ein Pixel — deshalb scrollt der Balken dort lieber, statt zu
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
// AutoMod: Discords eigener Filter, sichtbar gemacht.
|
||||||
|
//
|
||||||
|
// Bewusst kein eigener Wortfilter. Discord filtert serverseitig, ohne Latenz
|
||||||
|
// und ohne dass eine Nachricht erst bei uns ankommen muss — das kann kein Bot
|
||||||
|
// besser. Was fehlt, ist die Sicht darauf: Treffer landen nur in Discords
|
||||||
|
// eigenem Kanal, und die Regeln pflegt man in einer anderen Oberfläche als
|
||||||
|
// alles andere.
|
||||||
|
//
|
||||||
|
// Deshalb hier zweierlei: Treffer ins Mod-Log spiegeln, und die Regeln im
|
||||||
|
// Panel anzeigen und an-/abschalten. Das Bearbeiten der Wortlisten bleibt in
|
||||||
|
// Discord — dort ist es gut gelöst.
|
||||||
|
import { Events } from 'discord.js';
|
||||||
|
import {
|
||||||
|
AutoModerationRuleTriggerType as Trigger,
|
||||||
|
AutoModerationActionType as Aktion,
|
||||||
|
AutoModerationRuleKeywordPresetType as Preset,
|
||||||
|
} from 'discord.js';
|
||||||
|
import { moduleEnabled } from '../modules.js';
|
||||||
|
import { modlogChannelId } from '../runtime-settings.js';
|
||||||
|
import { brandEmbed } from '../embeds.js';
|
||||||
|
|
||||||
|
const AUSLOESER = {
|
||||||
|
[Trigger.Keyword]: 'Eigene Wörter',
|
||||||
|
[Trigger.Spam]: 'Spam-Erkennung',
|
||||||
|
[Trigger.KeywordPreset]: 'Discords Wortlisten',
|
||||||
|
[Trigger.MentionSpam]: 'Erwähnungs-Spam',
|
||||||
|
[Trigger.MemberProfile]: 'Profil-Inhalte',
|
||||||
|
};
|
||||||
|
|
||||||
|
const AKTIONEN = {
|
||||||
|
[Aktion.BlockMessage]: 'Nachricht blockiert',
|
||||||
|
[Aktion.SendAlertMessage]: 'Meldung an Team',
|
||||||
|
[Aktion.Timeout]: 'Auszeit',
|
||||||
|
[Aktion.BlockMemberInteraction]: 'Interaktion blockiert',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PRESETS = {
|
||||||
|
[Preset.Profanity]: 'Schimpfwörter',
|
||||||
|
[Preset.SexualContent]: 'Sexuelle Inhalte',
|
||||||
|
[Preset.Slurs]: 'Beleidigungen',
|
||||||
|
};
|
||||||
|
|
||||||
|
const zaehl = (n, eins, viele) => `${n} ${n === 1 ? eins : viele}`;
|
||||||
|
|
||||||
|
/** Eine Regel so beschreiben, dass man sie ohne Discord-Vorwissen versteht */
|
||||||
|
export function regelBeschreiben(regel) {
|
||||||
|
const md = regel.triggerMetadata ?? {};
|
||||||
|
const details = [];
|
||||||
|
if (md.keywordFilter?.length) details.push(zaehl(md.keywordFilter.length, 'Wort', 'Wörter'));
|
||||||
|
if (md.regexPatterns?.length) details.push(zaehl(md.regexPatterns.length, 'Muster', 'Muster'));
|
||||||
|
if (md.presets?.length) details.push(md.presets.map((p) => PRESETS[p] ?? p).join(', '));
|
||||||
|
if (md.mentionTotalLimit) details.push(`ab ${md.mentionTotalLimit} Erwähnungen`);
|
||||||
|
if (md.allowList?.length) details.push(zaehl(md.allowList.length, 'erlaubtes Wort', 'erlaubte Wörter'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: regel.id,
|
||||||
|
name: regel.name,
|
||||||
|
aktiv: regel.enabled,
|
||||||
|
ausloeser: AUSLOESER[regel.triggerType] ?? `Typ ${regel.triggerType}`,
|
||||||
|
details,
|
||||||
|
aktionen: (regel.actions ?? []).map((a) => AKTIONEN[a.type] ?? `Typ ${a.type}`),
|
||||||
|
// Rollen und Kanäle, die ausgenommen sind — oft der Grund, warum eine
|
||||||
|
// Regel scheinbar nicht greift
|
||||||
|
ausnahmen: {
|
||||||
|
rollen: regel.exemptRoles?.size ?? 0,
|
||||||
|
kanaele: regel.exemptChannels?.size ?? 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alle Regeln der Guild holen — leer, wenn keine Guild oder keine Rechte */
|
||||||
|
export async function listRules(client, guildId) {
|
||||||
|
const guild = guildId ? client.guilds.cache.get(guildId) : client.guilds.cache.first();
|
||||||
|
if (!guild) return { ok: false, grund: 'Kein Server verbunden', regeln: [] };
|
||||||
|
try {
|
||||||
|
const regeln = await guild.autoModerationRules.fetch();
|
||||||
|
return { ok: true, regeln: [...regeln.values()].map(regelBeschreiben) };
|
||||||
|
} catch (error) {
|
||||||
|
// Häufigster Fall: dem Bot fehlt „Server verwalten"
|
||||||
|
return { ok: false, grund: error.message, regeln: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Regel an- oder abschalten */
|
||||||
|
export async function setRuleEnabled(client, guildId, ruleId, aktiv, durch) {
|
||||||
|
const guild = guildId ? client.guilds.cache.get(guildId) : client.guilds.cache.first();
|
||||||
|
if (!guild) throw new Error('Kein Server verbunden');
|
||||||
|
const regel = await guild.autoModerationRules.fetch(ruleId);
|
||||||
|
await regel.edit({ enabled: Boolean(aktiv), reason: `Über das Panel durch ${durch}` });
|
||||||
|
return regelBeschreiben(await guild.autoModerationRules.fetch(ruleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerAutoMod(client) {
|
||||||
|
client.on(Events.AutoModerationActionExecution, async (treffer) => {
|
||||||
|
try {
|
||||||
|
if (!moduleEnabled('automod')) return;
|
||||||
|
const channelId = modlogChannelId();
|
||||||
|
if (!channelId) return;
|
||||||
|
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||||
|
if (!channel?.isTextBased()) return;
|
||||||
|
|
||||||
|
const regelName = treffer.autoModerationRule?.name ?? `Regel ${treffer.ruleId}`;
|
||||||
|
const embed = brandEmbed(client, 'AUTOMOD', { secondary: true })
|
||||||
|
.setTitle('🛑 AutoMod hat eingegriffen')
|
||||||
|
.setDescription(
|
||||||
|
`**${treffer.user?.username ?? treffer.userId}** `
|
||||||
|
+ `(<@${treffer.userId}>)${treffer.channelId ? ` in <#${treffer.channelId}>` : ''}`
|
||||||
|
)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Regel', value: regelName, inline: true },
|
||||||
|
{ name: 'Auslöser', value: AUSLOESER[treffer.ruleTriggerType] ?? '—', inline: true },
|
||||||
|
{ name: 'Maßnahme', value: AKTIONEN[treffer.action?.type] ?? '—', inline: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (treffer.matchedKeyword) {
|
||||||
|
embed.addFields({ name: 'Treffer', value: `\`${treffer.matchedKeyword}\``, inline: true });
|
||||||
|
}
|
||||||
|
if (treffer.content) {
|
||||||
|
// Der Wortlaut gehört ins Protokoll, aber gekürzt und ohne
|
||||||
|
// Erwähnungen, die den Kanal anpingen
|
||||||
|
embed.addFields({
|
||||||
|
name: 'Nachricht',
|
||||||
|
value: treffer.content.slice(0, 500),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await channel.send({ embeds: [embed], allowedMentions: { parse: [] } }).catch(() => {});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[automod]', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
|
|||||||
import { registerCommunityListeners } from './community.js';
|
import { registerCommunityListeners } from './community.js';
|
||||||
import { registerModTools } from './mod-tools.js';
|
import { registerModTools } from './mod-tools.js';
|
||||||
import { registerAntiRaid } from './anti-raid.js';
|
import { registerAntiRaid } from './anti-raid.js';
|
||||||
|
import { registerAutoMod } from './automod.js';
|
||||||
import { registerMetadata, startLinkedRoles } from './linked-roles.js';
|
import { registerMetadata, startLinkedRoles } from './linked-roles.js';
|
||||||
import { registerLevels } from './levels.js';
|
import { registerLevels } from './levels.js';
|
||||||
import { registerPresence } from './presence.js';
|
import { registerPresence } from './presence.js';
|
||||||
@@ -66,6 +67,9 @@ export async function startBot() {
|
|||||||
// Temp-Voice (Join to Create) + Event-Ankündigungen
|
// Temp-Voice (Join to Create) + Event-Ankündigungen
|
||||||
GatewayIntentBits.GuildVoiceStates,
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
GatewayIntentBits.GuildScheduledEvents,
|
GatewayIntentBits.GuildScheduledEvents,
|
||||||
|
// AutoMod-Treffer mitbekommen. Nicht privilegiert, aber Discord
|
||||||
|
// schickt die Ereignisse nur an Bots mit „Server verwalten".
|
||||||
|
GatewayIntentBits.AutoModerationExecution,
|
||||||
],
|
],
|
||||||
// Partials: Delete/Reaction-Events für ungecachte Nachrichten + DM-Kanäle
|
// Partials: Delete/Reaction-Events für ungecachte Nachrichten + DM-Kanäle
|
||||||
partials: [Partials.Message, Partials.Reaction, Partials.Channel],
|
partials: [Partials.Message, Partials.Reaction, Partials.Channel],
|
||||||
@@ -376,6 +380,7 @@ export async function startBot() {
|
|||||||
registerModTools(client);
|
registerModTools(client);
|
||||||
// Vor den anderen Beitritts-Handlern egal — die Erkennung zaehlt nur mit
|
// Vor den anderen Beitritts-Handlern egal — die Erkennung zaehlt nur mit
|
||||||
registerAntiRaid(client);
|
registerAntiRaid(client);
|
||||||
|
registerAutoMod(client);
|
||||||
registerLevels(client);
|
registerLevels(client);
|
||||||
registerPresence(client);
|
registerPresence(client);
|
||||||
registerExtras(client);
|
registerExtras(client);
|
||||||
|
|||||||
@@ -156,6 +156,12 @@ export const MODULES = [
|
|||||||
tab: 'support', default: true,
|
tab: 'support', default: true,
|
||||||
tool: { tab: 'support', label: 'Moderation' },
|
tool: { tab: 'support', label: 'Moderation' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'automod', group: 'moderation', name: 'AutoMod-Protokoll',
|
||||||
|
desc: 'Treffer von Discords eigenem Filter landen im Protokoll, Regeln sind hier schaltbar.',
|
||||||
|
tab: 'support', setting: 'automod_enabled', defaultOn: false,
|
||||||
|
requires: [{ setting: 'modlog_channel_id', label: 'Protokoll-Kanal', create: { name: 'protokoll', privat: true, topic: 'Bearbeitet, geloescht, beigetreten' } }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'anti_raid', group: 'moderation', name: 'Raid-Schutz',
|
id: 'anti_raid', group: 'moderation', name: 'Raid-Schutz',
|
||||||
desc: 'Erkennt Beitritts-Wellen und macht den Server vorübergehend dicht.',
|
desc: 'Erkennt Beitritts-Wellen und macht den Server vorübergehend dicht.',
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js';
|
|||||||
import { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js';
|
import { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js';
|
||||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||||
import { watchdogState, watchedServices } from '../bot/watchdog.js';
|
import { watchdogState, watchedServices } from '../bot/watchdog.js';
|
||||||
|
import { listRules, setRuleEnabled } from '../bot/automod.js';
|
||||||
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
|
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
|
||||||
import { sanitizeEmbed } from './api-v1.js';
|
import { sanitizeEmbed } from './api-v1.js';
|
||||||
import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-menus.js';
|
import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-menus.js';
|
||||||
@@ -293,6 +294,29 @@ export function registerApiRoutes(app, client) {
|
|||||||
return { deleted: deleteMonitored(Number(request.params.id)), dienste: listMonitored() };
|
return { deleted: deleteMonitored(Number(request.params.id)), dienste: listMonitored() };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// AutoMod: Discords eigene Regeln lesen und schalten. Die Wortlisten
|
||||||
|
// bleiben in Discord — dort ist das Bearbeiten besser geloest.
|
||||||
|
app.get('/api/automod', async (request, reply) => {
|
||||||
|
if (requireAnyScope(request, reply)) return;
|
||||||
|
const res = await listRules(client, discordGuildId());
|
||||||
|
return { aktiv: moduleEnabled('automod'), ...res };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/automod/:id', async (request, reply) => {
|
||||||
|
if (requireScope(request, reply, 'settings')) return;
|
||||||
|
const user = getSessionUser(request);
|
||||||
|
try {
|
||||||
|
const regel = await setRuleEnabled(
|
||||||
|
client, discordGuildId(), String(request.params.id),
|
||||||
|
Boolean(request.body?.aktiv), user?.username ?? 'Panel'
|
||||||
|
);
|
||||||
|
logAudit(user, `automod-regel ${regel.aktiv ? 'an' : 'aus'}`, regel.name);
|
||||||
|
return { ok: true, regel };
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(400).send({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- Module: zentrale An/Aus-Schalter aller Funktionen ---
|
// --- Module: zentrale An/Aus-Schalter aller Funktionen ---
|
||||||
|
|
||||||
app.get('/api/modules', async (request, reply) => {
|
app.get('/api/modules', async (request, reply) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user