Carl-Bot-Paket: Moderation, Auto-/Sticky-Roles, Level-System, Tags, geplante Posts
- Moderation: /warn (DM + Mod-Log), /warns (Historie + löschen), /timeout (max 28d), /purge (bulk, 14-Tage-Limit sauber gemeldet); Mod-Log erweitert um Join/Leave und Nick-/Rollen-Änderungen - Auto-Rolle bei Join + Sticky-Roles (Rollen bei Leave gesichert, bei Rejoin wiederhergestellt; managed/@everyone ausgenommen) — Rollen-Tab - Level-System (opt-in): 15-25 XP/Nachricht mit 60s-Cooldown, MEE6-Formel, Level-Up-Announce, Rollen-Belohnungen (Level=RolleID), /rank mit Fortschritt, öffentliche Bestenliste /level; Community-Tab - Tags: /tag mit Autocomplete, Verwaltung im Composer-Tab (/api/tags) - Geplante Posts: einmalig/täglich/wöchentlich, Minuten-Scheduler, DST-sicher über Neuberechnung aus Zeitfeldern; Composer-Tab (/api/scheduled) - Autocomplete-Dispatch im InteractionCreate; alles smoke-getestet Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+21
-1
@@ -5,6 +5,7 @@ import { devlogChannelId, devlogPingRoleId, playtesterRoleId, ticketChannelId }
|
||||
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
|
||||
import { registerCommunityListeners } from './community.js';
|
||||
import { registerModTools } from './mod-tools.js';
|
||||
import { registerLevels } from './levels.js';
|
||||
import { handleRoleMenuButton } from './role-menus.js';
|
||||
import {
|
||||
addPlaytester, removePlaytester, isWish, bumpWish,
|
||||
@@ -20,9 +21,18 @@ import * as galerieBackfill from './commands/galerie-backfill.js';
|
||||
import * as wunsch from './commands/wunsch.js';
|
||||
import * as giveaway from './commands/giveaway.js';
|
||||
import * as ticketSetup from './commands/ticket-setup.js';
|
||||
import * as warn from './commands/warn.js';
|
||||
import * as warns from './commands/warns.js';
|
||||
import * as timeout from './commands/timeout.js';
|
||||
import * as purge from './commands/purge.js';
|
||||
import * as rank from './commands/rank.js';
|
||||
import * as tag from './commands/tag.js';
|
||||
|
||||
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
|
||||
const commandModules = [ping, devlogBackfill, bug, playtesterSetup, galerieBackfill, wunsch, giveaway, ticketSetup];
|
||||
const commandModules = [
|
||||
ping, devlogBackfill, bug, playtesterSetup, galerieBackfill,
|
||||
wunsch, giveaway, ticketSetup, warn, warns, timeout, purge, rank, tag,
|
||||
];
|
||||
|
||||
export async function startBot() {
|
||||
const client = new Client({
|
||||
@@ -92,6 +102,15 @@ export async function startBot() {
|
||||
});
|
||||
|
||||
client.on(Events.InteractionCreate, async (interaction) => {
|
||||
// Autocomplete (z. B. /tag)
|
||||
if (interaction.isAutocomplete()) {
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
await command?.autocomplete?.(interaction).catch((e) =>
|
||||
console.error(`[bot] Autocomplete /${interaction.commandName}:`, e)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔔-Button unterm Devlog: Ping-Rolle selbst an-/abmelden
|
||||
if (interaction.isButton() && interaction.customId === 'devlog_ping_toggle') {
|
||||
const roleId = devlogPingRoleId();
|
||||
@@ -276,6 +295,7 @@ export async function startBot() {
|
||||
|
||||
registerCommunityListeners(client);
|
||||
registerModTools(client);
|
||||
registerLevels(client);
|
||||
|
||||
await client.login(config.discordToken);
|
||||
return client;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// /purge — Nachrichten im aktuellen Kanal massenlöschen (max 100, jünger als 14 Tage)
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js';
|
||||
import { modlogChannelId } from '../../runtime-settings.js';
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('purge')
|
||||
.setDescription('Löscht die letzten N Nachrichten in diesem Kanal')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
|
||||
.addIntegerOption((o) =>
|
||||
o.setName('anzahl').setDescription('Wie viele? (1–100)').setRequired(true).setMinValue(1).setMaxValue(100)
|
||||
);
|
||||
|
||||
export async function execute(interaction) {
|
||||
const count = interaction.options.getInteger('anzahl');
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
let deleted;
|
||||
try {
|
||||
// true = Nachrichten >14 Tage still überspringen (Discord-API-Limit)
|
||||
deleted = await interaction.channel.bulkDelete(count, true);
|
||||
} catch {
|
||||
await interaction.editReply('❌ Löschen fehlgeschlagen — Bot braucht „Nachrichten verwalten" hier.');
|
||||
return;
|
||||
}
|
||||
|
||||
const logChannelId = modlogChannelId();
|
||||
if (logChannelId && deleted.size > 0) {
|
||||
const channel = await interaction.client.channels.fetch(logChannelId).catch(() => null);
|
||||
await channel?.send({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff4d00)
|
||||
.setTitle('🧹 Purge')
|
||||
.setDescription(
|
||||
`**${deleted.size}** Nachrichten in <#${interaction.channelId}> gelöscht von **${interaction.user.username}**`
|
||||
)
|
||||
.setTimestamp(),
|
||||
],
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
await interaction.editReply(
|
||||
`🧹 **${deleted.size}** Nachrichten gelöscht.${deleted.size < count ? ' (ältere als 14 Tage kann Discord nicht massenlöschen)' : ''}`
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// /rank — eigenes Level, XP-Fortschritt und Platzierung anzeigen
|
||||
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||
import { getLevelRow, rankOf } from '../../db.js';
|
||||
import { xpForLevel } from '../levels.js';
|
||||
import { levelsEnabled, publicUrl } from '../../runtime-settings.js';
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('rank')
|
||||
.setDescription('Dein Level, XP und Platz in der Bestenliste')
|
||||
.addUserOption((o) => o.setName('member').setDescription('Optional: anderen Member anschauen'));
|
||||
|
||||
export async function execute(interaction) {
|
||||
if (!levelsEnabled()) {
|
||||
await interaction.reply({ content: '❌ Das Level-System ist nicht aktiviert.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
const user = interaction.options.getUser('member') ?? interaction.user;
|
||||
const row = getLevelRow(user.id);
|
||||
if (!row) {
|
||||
await interaction.reply({ content: `**${user.username}** hat noch keine XP — einfach mitschreiben! ✍️`, ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const currentBase = xpForLevel(row.level);
|
||||
const nextAt = xpForLevel(row.level + 1);
|
||||
const progress = row.xp - currentBase;
|
||||
const needed = nextAt - currentBase;
|
||||
const pct = Math.floor((progress / needed) * 100);
|
||||
const barLen = 14;
|
||||
const bar = '█'.repeat(Math.round((pct / 100) * barLen)).padEnd(barLen, '·');
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xf5c518)
|
||||
.setAuthor({ name: user.username, iconURL: user.displayAvatarURL({ size: 64 }) })
|
||||
.setTitle(`Level ${row.level} · Platz #${rankOf(user.id)}`)
|
||||
.setDescription(`\`${bar}\` ${pct}%\n**${progress} / ${needed} XP** bis Level ${row.level + 1} · gesamt ${row.xp} XP`)
|
||||
.setURL(`${publicUrl()}/level`)
|
||||
.setFooter({ text: 'D4RKST3R // LEVEL' }),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// /tag — gespeicherte Text-Bausteine abrufen (verwaltet über die Setup-Seite)
|
||||
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
||||
import { getTag, listTags } from '../../db.js';
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('tag')
|
||||
.setDescription('Gespeicherten Text-Baustein posten (z. B. /tag name:server-ip)')
|
||||
.addStringOption((o) =>
|
||||
o.setName('name').setDescription('Name des Tags').setRequired(true).setAutocomplete(true)
|
||||
);
|
||||
|
||||
export async function autocomplete(interaction) {
|
||||
const input = interaction.options.getFocused().toLowerCase();
|
||||
const choices = listTags()
|
||||
.filter((t) => t.name.toLowerCase().includes(input))
|
||||
.slice(0, 25)
|
||||
.map((t) => ({ name: t.name, value: t.name }));
|
||||
await interaction.respond(choices).catch(() => {});
|
||||
}
|
||||
|
||||
export async function execute(interaction) {
|
||||
const name = interaction.options.getString('name').trim().toLowerCase();
|
||||
const tag = getTag(name);
|
||||
if (!tag) {
|
||||
const available = listTags().map((t) => `\`${t.name}\``).slice(0, 25).join(', ');
|
||||
await interaction.reply({
|
||||
content: `❌ Tag \`${name}\` gibt es nicht.${available ? `\nVerfügbar: ${available}` : ''}`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await interaction.reply({ content: tag.content.slice(0, 2000), allowedMentions: { parse: [] } });
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// /timeout — Member stummschalten (Discord-Timeout) mit Mod-Log
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js';
|
||||
import { parseDuration } from './giveaway.js';
|
||||
import { modlogChannelId } from '../../runtime-settings.js';
|
||||
|
||||
const MAX_TIMEOUT_MS = 28 * 86_400_000; // Discord-Limit
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('timeout')
|
||||
.setDescription('Member vorübergehend stummschalten')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||
.addUserOption((o) => o.setName('member').setDescription('Wen?').setRequired(true))
|
||||
.addStringOption((o) => o.setName('dauer').setDescription('z. B. 10m, 2h, 1d (max 28d)').setRequired(true))
|
||||
.addStringOption((o) => o.setName('grund').setDescription('Grund').setMaxLength(400));
|
||||
|
||||
export async function execute(interaction) {
|
||||
const duration = parseDuration(interaction.options.getString('dauer'));
|
||||
if (!duration || duration > MAX_TIMEOUT_MS) {
|
||||
await interaction.reply({ content: '❌ Dauer bitte als `10m`, `2h` oder `1d` (max 28d).', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
const user = interaction.options.getUser('member');
|
||||
const reason = interaction.options.getString('grund') ?? 'Kein Grund angegeben';
|
||||
|
||||
const member = await interaction.guild.members.fetch(user.id).catch(() => null);
|
||||
if (!member) {
|
||||
await interaction.reply({ content: '❌ Member nicht gefunden.', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await member.timeout(duration, reason);
|
||||
} catch {
|
||||
await interaction.reply({ content: '❌ Timeout fehlgeschlagen — Rechte/Rollen-Reihenfolge prüfen.', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
|
||||
const until = Math.floor((Date.now() + duration) / 1000);
|
||||
const logChannelId = modlogChannelId();
|
||||
if (logChannelId) {
|
||||
const channel = await interaction.client.channels.fetch(logChannelId).catch(() => null);
|
||||
await channel?.send({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff4d00)
|
||||
.setTitle('🔇 Timeout')
|
||||
.addFields(
|
||||
{ name: 'Member', value: `${user.username} (<@${user.id}>)`, inline: true },
|
||||
{ name: 'Moderator', value: interaction.user.username, inline: true },
|
||||
{ name: 'Bis', value: `<t:${until}:R>`, inline: true },
|
||||
{ name: 'Grund', value: reason }
|
||||
)
|
||||
.setTimestamp(),
|
||||
],
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
await interaction.reply({
|
||||
content: `🔇 **${user.username}** ist stumm bis <t:${until}:R>.`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// /warn — Member verwarnen: DB-Eintrag, DM an den User, Mod-Log (Moderatoren)
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js';
|
||||
import { addWarn, warnsOf } from '../../db.js';
|
||||
import { modlogChannelId } from '../../runtime-settings.js';
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('warn')
|
||||
.setDescription('Member verwarnen (mit DM und Mod-Log)')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||
.addUserOption((o) => o.setName('member').setDescription('Wen verwarnen?').setRequired(true))
|
||||
.addStringOption((o) => o.setName('grund').setDescription('Grund der Verwarnung').setRequired(true).setMaxLength(500));
|
||||
|
||||
export async function execute(interaction) {
|
||||
const user = interaction.options.getUser('member');
|
||||
const reason = interaction.options.getString('grund');
|
||||
if (user.bot) {
|
||||
await interaction.reply({ content: '❌ Bots kann man nicht verwarnen.', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
|
||||
addWarn(user.id, interaction.user.username, reason);
|
||||
const count = warnsOf(user.id).length;
|
||||
|
||||
// DM — best effort, User kann DMs blocken
|
||||
let dmOk = true;
|
||||
await user
|
||||
.send(`⚠️ Du wurdest auf **${interaction.guild.name}** verwarnt (Verwarnung #${count}):\n> ${reason}`)
|
||||
.catch(() => { dmOk = false; });
|
||||
|
||||
// Mod-Log
|
||||
const logChannelId = modlogChannelId();
|
||||
if (logChannelId) {
|
||||
const channel = await interaction.client.channels.fetch(logChannelId).catch(() => null);
|
||||
await channel?.send({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff4d00)
|
||||
.setTitle(`⚠️ Verwarnung #${count}`)
|
||||
.addFields(
|
||||
{ name: 'Member', value: `${user.username} (<@${user.id}>)`, inline: true },
|
||||
{ name: 'Moderator', value: interaction.user.username, inline: true },
|
||||
{ name: 'Grund', value: reason }
|
||||
)
|
||||
.setTimestamp(),
|
||||
],
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
await interaction.reply({
|
||||
content: `⚠️ **${user.username}** verwarnt (#${count}).${dmOk ? '' : ' (DM nicht zustellbar)'}`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// /warns — Verwarnungs-Historie eines Members einsehen / Eintrag löschen (Moderatoren)
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js';
|
||||
import { warnsOf, deleteWarn } from '../../db.js';
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('warns')
|
||||
.setDescription('Verwarnungen eines Members anzeigen (oder eine löschen)')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||
.addUserOption((o) => o.setName('member').setDescription('Wessen Verwarnungen?').setRequired(true))
|
||||
.addIntegerOption((o) => o.setName('loeschen').setDescription('Optional: Verwarnungs-ID entfernen'));
|
||||
|
||||
export async function execute(interaction) {
|
||||
const user = interaction.options.getUser('member');
|
||||
const removeId = interaction.options.getInteger('loeschen');
|
||||
|
||||
if (removeId) {
|
||||
const removed = deleteWarn(removeId);
|
||||
await interaction.reply({
|
||||
content: removed ? `🗑️ Verwarnung #${removeId} entfernt.` : `❌ Verwarnung #${removeId} nicht gefunden.`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const warns = warnsOf(user.id);
|
||||
if (warns.length === 0) {
|
||||
await interaction.reply({ content: `✅ **${user.username}** hat keine Verwarnungen.`, flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = warns
|
||||
.slice(-15)
|
||||
.map((w) => `\`#${w.id}\` ${w.created_at.slice(0, 10)} — ${w.reason} *(${w.moderator ?? '—'})*`);
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xf5c518)
|
||||
.setTitle(`⚠️ Verwarnungen: ${user.username} (${warns.length})`)
|
||||
.setDescription(lines.join('\n'))
|
||||
.setFooter({ text: 'Löschen: /warns member:<user> loeschen:<ID>' }),
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Level-System (Carl-/MEE6-Stil): XP pro Nachricht mit Cooldown, Level-Up-Announce,
|
||||
// Rollen-Belohnungen; Bestenliste öffentlich auf der Webseite (/level).
|
||||
import { Events } from 'discord.js';
|
||||
import { getLevelRow, setLevelRow } from '../db.js';
|
||||
import { levelsEnabled, levelsAnnounce, levelRewards } from '../runtime-settings.js';
|
||||
|
||||
const XP_COOLDOWN_MS = 60_000;
|
||||
const XP_MIN = 15;
|
||||
const XP_SPREAD = 11; // 15–25 XP pro gewerteter Nachricht
|
||||
|
||||
/** Kumulierte XP-Schwelle, ab der ein Level erreicht ist (MEE6-Formel pro Stufe) */
|
||||
export function xpForLevel(level) {
|
||||
let total = 0;
|
||||
for (let n = 0; n < level; n++) total += 5 * n * n + 50 * n + 100;
|
||||
return total;
|
||||
}
|
||||
|
||||
export function levelFromXp(xp) {
|
||||
let level = 0;
|
||||
while (xp >= xpForLevel(level + 1)) level++;
|
||||
return level;
|
||||
}
|
||||
|
||||
/**
|
||||
* XP für eine Nachricht gutschreiben (Cooldown-gesteuert).
|
||||
* @returns {{ xp: number, level: number, leveledUp: boolean } | null} null = Cooldown aktiv
|
||||
*/
|
||||
export function awardXp(userId, username, now = Date.now()) {
|
||||
const row = getLevelRow(userId) ?? { user_id: userId, username, xp: 0, level: 0, last_xp_ms: 0 };
|
||||
if (now - row.last_xp_ms < XP_COOLDOWN_MS) return null;
|
||||
|
||||
const xp = row.xp + XP_MIN + Math.floor(Math.random() * XP_SPREAD);
|
||||
const level = levelFromXp(xp);
|
||||
const leveledUp = level > row.level;
|
||||
setLevelRow({ user_id: userId, username, xp, level, last_xp_ms: now });
|
||||
return { xp, level, leveledUp };
|
||||
}
|
||||
|
||||
/** Beim Level-Up fällige Belohnungs-Rollen (alle Schwellen ≤ Level) vergeben */
|
||||
async function grantRewards(member, level) {
|
||||
for (const reward of levelRewards()) {
|
||||
if (reward.level > level) break;
|
||||
if (!member.roles.cache.has(reward.roleId)) {
|
||||
await member.roles.add(reward.roleId).catch((e) =>
|
||||
console.error(`[level] Belohnungs-Rolle ${reward.roleId} fehlgeschlagen: ${e.message}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerLevels(client) {
|
||||
client.on(Events.MessageCreate, async (message) => {
|
||||
try {
|
||||
if (!levelsEnabled()) return;
|
||||
if (!message.guildId || message.author?.bot || message.webhookId) return;
|
||||
|
||||
const result = awardXp(
|
||||
message.author.id,
|
||||
message.member?.displayName ?? message.author.username
|
||||
);
|
||||
if (!result?.leveledUp) return;
|
||||
|
||||
if (message.member) await grantRewards(message.member, result.level);
|
||||
if (levelsAnnounce()) {
|
||||
await message.channel
|
||||
.send({
|
||||
content: `🎉 **${message.member?.displayName ?? message.author.username}** ist jetzt **Level ${result.level}**!`,
|
||||
allowedMentions: { parse: [] },
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[level] XP-Verarbeitung fehlgeschlagen:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
+79
-3
@@ -1,7 +1,10 @@
|
||||
// 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';
|
||||
import { saveModmail, modmailByUser, modmailByThread, saveStickyRoles, stickyRolesOf } from '../db.js';
|
||||
import {
|
||||
modmailChannelId, welcomeChannelId, modlogChannelId, publicUrl,
|
||||
autoroleId, stickyRolesEnabled,
|
||||
} from '../runtime-settings.js';
|
||||
|
||||
const BRAND_YELLOW = 0xf5c518;
|
||||
const BRAND_ORANGE = 0xff4d00;
|
||||
@@ -101,6 +104,26 @@ async function handleMemberAdd(member) {
|
||||
await channel.send({ content: `<@${member.id}>`, embeds: [embed] });
|
||||
}
|
||||
|
||||
/* ── Auto- & Sticky-Roles ──────────────────────────── */
|
||||
|
||||
/** Beim Join: gesicherte Rollen wiederherstellen + Auto-Rolle vergeben */
|
||||
async function restoreRoles(member) {
|
||||
const roleIds = new Set();
|
||||
if (stickyRolesEnabled()) {
|
||||
for (const id of stickyRolesOf(member.id)) roleIds.add(id);
|
||||
}
|
||||
const auto = autoroleId();
|
||||
if (auto) roleIds.add(auto);
|
||||
|
||||
for (const id of roleIds) {
|
||||
const role = member.guild.roles.cache.get(id);
|
||||
if (!role || role.managed || role.id === member.guild.id) continue;
|
||||
await member.roles.add(id).catch((e) =>
|
||||
console.error(`[roles] Rolle ${id} bei Join fehlgeschlagen: ${e.message}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mod-Log ───────────────────────────────────────── */
|
||||
|
||||
async function logToModlog(client, embed) {
|
||||
@@ -136,15 +159,68 @@ export function registerModTools(client) {
|
||||
}
|
||||
});
|
||||
|
||||
// Willkommens-Embed
|
||||
// Willkommens-Embed + Auto-/Sticky-Roles + Join-Log
|
||||
client.on(Events.GuildMemberAdd, async (member) => {
|
||||
try {
|
||||
await handleMemberAdd(member);
|
||||
await restoreRoles(member);
|
||||
await logToModlog(client, new EmbedBuilder()
|
||||
.setColor(0x00ff88)
|
||||
.setTitle('📥 Member beigetreten')
|
||||
.setDescription(`**${member.user.username}** (<@${member.id}>) — Mitglied #${member.guild.memberCount}`)
|
||||
.setTimestamp());
|
||||
} catch (error) {
|
||||
console.error('[welcome] Fehler:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Leave: Rollen für Sticky-Restore sichern + Log
|
||||
client.on(Events.GuildMemberRemove, async (member) => {
|
||||
try {
|
||||
const roleIds = [...(member.roles?.cache?.values() ?? [])]
|
||||
.filter((r) => r.id !== member.guild.id && !r.managed)
|
||||
.map((r) => r.id);
|
||||
if (roleIds.length > 0) saveStickyRoles(member.id, roleIds);
|
||||
|
||||
await logToModlog(client, new EmbedBuilder()
|
||||
.setColor(BRAND_ORANGE)
|
||||
.setTitle('📤 Member gegangen')
|
||||
.setDescription(`**${member.user?.username ?? member.id}**${roleIds.length ? ` — Rollen gesichert (${roleIds.length})` : ''}`)
|
||||
.setTimestamp());
|
||||
} catch (error) {
|
||||
console.error('[modlog] Leave-Handling fehlgeschlagen:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Nick-/Rollen-Änderungen loggen
|
||||
client.on(Events.GuildMemberUpdate, async (oldMember, newMember) => {
|
||||
try {
|
||||
const changes = [];
|
||||
if (oldMember.nickname !== newMember.nickname) {
|
||||
changes.push({
|
||||
name: 'Nickname',
|
||||
value: `\`${oldMember.nickname ?? oldMember.user?.username ?? '—'}\` → \`${newMember.nickname ?? newMember.user?.username ?? '—'}\``,
|
||||
});
|
||||
}
|
||||
const oldIds = new Set(oldMember.roles?.cache?.keys?.() ?? []);
|
||||
const newIds = new Set(newMember.roles?.cache?.keys?.() ?? []);
|
||||
const added = [...newIds].filter((id) => !oldIds.has(id) && id !== newMember.guild.id);
|
||||
const removed = [...oldIds].filter((id) => !newIds.has(id) && id !== newMember.guild.id);
|
||||
if (added.length) changes.push({ name: 'Rollen +', value: added.map((id) => `<@&${id}>`).join(' ') });
|
||||
if (removed.length) changes.push({ name: 'Rollen −', value: removed.map((id) => `<@&${id}>`).join(' ') });
|
||||
if (changes.length === 0) return;
|
||||
|
||||
await logToModlog(client, new EmbedBuilder()
|
||||
.setColor(BRAND_YELLOW)
|
||||
.setTitle('👤 Member geändert')
|
||||
.setDescription(`**${newMember.user?.username ?? newMember.id}**`)
|
||||
.addFields(changes)
|
||||
.setTimestamp());
|
||||
} catch (error) {
|
||||
console.error('[modlog] Member-Update fehlgeschlagen:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Mod-Log: gelöschte + bearbeitete Nachrichten (User-Nachrichten in Guild-Kanälen)
|
||||
client.on(Events.MessageDelete, async (message) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user