Implement leave announcement for banned members and enhance moderation notifications

- Added a `leaveAnnounceBan` field to the `WelcomeConfig` model, allowing for optional announcements in the leave channel when a member is banned.
- Updated the leave handling logic to check for bans and send a notice in the leave channel if the `leaveAnnounceBan` toggle is enabled.
- Enhanced moderation actions (kick and ban) to notify the affected user via DM with the reason for the action.
- Introduced a new `notifyModerationTargetDm` function to streamline DM notifications for moderation actions.
- Updated the WebUI to include a toggle for the `leaveAnnounceBan` feature, improving user configurability.
- Improved localization for new messages related to moderation notifications and leave announcements in both English and German.
- Documented changes in phase tracking to reflect the new leave announcement feature and moderation notification enhancements.
This commit is contained in:
TheOnlyMace
2026-07-25 17:50:18 +02:00
parent 03bb406d82
commit e2bab9cceb
16 changed files with 198 additions and 5 deletions

View File

@@ -2,7 +2,7 @@ import { TextChannel, type ButtonInteraction } from 'discord.js';
import { type Locale, t, tf } from '@nexumi/shared';
import type { BotContext } from '../../types.js';
import { assertBannableForConfirm } from '../../hierarchy.js';
import { createCase, scheduleTempBanExpire } from './service.js';
import { createCase, notifyModerationTargetDm, scheduleTempBanExpire } from './service.js';
import { tryParseDuration } from './duration.js';
type BanPayload = {
@@ -55,6 +55,18 @@ export async function executeBan(
await interaction.deferUpdate();
const targetUser = await context.client.users.fetch(payload.userId).catch(() => null);
if (targetUser) {
await notifyModerationTargetDm({
user: targetUser,
guildName: guild.name,
reason: payload.reason,
locale,
action: 'ban',
duration: payload.duration
});
}
await guild.members.ban(payload.userId, { reason: payload.reason });
const modCase = await createCase(context, {
guildId: guild.id,

View File

@@ -8,7 +8,7 @@ import { t, tf } from '@nexumi/shared';
import type { SlashCommand } from '../../types.js';
import { requirePermission } from '../../permissions.js';
import { assertModerableMember } from '../../hierarchy.js';
import { applyWarnEscalation, createCase, createWarning } from './service.js';
import { applyWarnEscalation, createCase, createWarning, notifyModerationTargetDm } from './service.js';
import { parseDuration, parseTimeoutDuration, tryParseDuration } from './duration.js';
import { getGuildLocale } from '../../i18n.js';
import { promptDestructiveConfirmation } from './confirmations.js';
@@ -133,6 +133,13 @@ const kickCommand: SlashCommand = {
const reason = reasonFrom(interaction, locale);
const hierarchy = await assertModerableMember(interaction, user.id, locale, 'kick');
if (!hierarchy.ok || !hierarchy.member) return;
await notifyModerationTargetDm({
user,
guildName: interaction.guild!.name,
reason,
locale,
action: 'kick'
});
await hierarchy.member.kick(reason);
const modCase = await createCase(context, {
guildId: interaction.guildId!,

View File

@@ -64,6 +64,37 @@ async function notifyWarnedUser(params: {
}
}
export async function notifyModerationTargetDm(params: {
user: User;
guildName: string;
reason: string;
locale: Locale;
action: 'ban' | 'kick';
duration?: string | null;
}): Promise<void> {
const reason = params.reason || t(params.locale, 'moderation.reason.none');
const key =
params.action === 'kick'
? 'moderation.kick.dm'
: params.duration
? 'moderation.ban.dm.temporary'
: 'moderation.ban.dm';
try {
await params.user.send(
tf(params.locale, key, {
guild: params.guildName,
reason,
...(params.duration ? { duration: params.duration } : {})
})
);
} catch (error) {
logger.warn(
{ error, userId: params.user.id, action: params.action },
'Failed to DM moderation target'
);
}
}
export async function createWarning(
context: BotContext,
input: CreateWarningInput
@@ -298,6 +329,13 @@ export async function applyWarnEscalation(
String(warningCount)
);
}
await notifyModerationTargetDm({
user: member.user,
guildName: interaction.guild.name,
reason,
locale,
action: 'kick'
});
await member.kick(reason);
await createCase(context, {
guildId,
@@ -318,6 +356,13 @@ export async function applyWarnEscalation(
String(warningCount)
);
}
await notifyModerationTargetDm({
user: member.user,
guildName: interaction.guild.name,
reason,
locale,
action: 'ban'
});
await interaction.guild.members.ban(userId, { reason });
await createCase(context, {
guildId,