Implement lockdown management in automod system
- Added functionality to activate and deactivate lockdowns via the automod worker, responding to specific job names. - Enhanced the anti-raid join handler to apply a verification role when lockdown is active and the anti-raid action is set to VERIFY. - Updated the automod configuration to ensure default rules are created if missing, improving the setup process for new guilds. - Introduced new API endpoints for managing cases and warnings, allowing for soft deletion and reason updates. - Enhanced the moderation dashboard to include warnings management, improving user experience in moderation tasks. - Updated localization files to reflect new features and improve user guidance in both English and German.
This commit is contained in:
@@ -8,6 +8,7 @@ import { logger } from './logger.js';
|
||||
import type { BotContext } from './types.js';
|
||||
import { env } from './env.js';
|
||||
import { refreshPhishingDomains } from './modules/automod/filters.js';
|
||||
import { activateLockdown, deactivateLockdown } from './modules/automod/actions.js';
|
||||
import { completeVerification } from './modules/verification/handlers.js';
|
||||
import { closePoll } from './modules/utility/poll.js';
|
||||
import { sendReminder } from './modules/utility/reminders.js';
|
||||
@@ -212,11 +213,28 @@ export function startWorkers(context: BotContext): Worker[] {
|
||||
const automodWorker = new Worker(
|
||||
automodQueueName,
|
||||
async (job) => {
|
||||
if (job.name !== 'refreshPhishingList') {
|
||||
if (job.name === 'refreshPhishingList') {
|
||||
const count = await refreshPhishingDomains(redis);
|
||||
logger.info({ domainCount: count }, 'Phishing domain list refreshed');
|
||||
return;
|
||||
}
|
||||
const count = await refreshPhishingDomains(redis);
|
||||
logger.info({ domainCount: count }, 'Phishing domain list refreshed');
|
||||
if (job.name === 'deactivateLockdown' || job.name === 'activateLockdown') {
|
||||
const guildId = (job.data as { guildId?: string }).guildId;
|
||||
if (!guildId) {
|
||||
return;
|
||||
}
|
||||
const guild = await context.client.guilds.fetch(guildId).catch(() => null);
|
||||
if (!guild) {
|
||||
return;
|
||||
}
|
||||
if (job.name === 'deactivateLockdown') {
|
||||
await deactivateLockdown(guild);
|
||||
logger.info({ guildId }, 'Lockdown deactivated via dashboard');
|
||||
return;
|
||||
}
|
||||
await activateLockdown(guild);
|
||||
logger.info({ guildId }, 'Lockdown activated via dashboard');
|
||||
}
|
||||
},
|
||||
{ connection: redis }
|
||||
);
|
||||
|
||||
@@ -204,6 +204,16 @@ export async function handleAntiRaidJoin(
|
||||
return;
|
||||
}
|
||||
|
||||
// While VERIFY raid-response is active, keep gating new joins with the unverified role.
|
||||
if (config.lockdownActive && config.antiRaidAction === 'VERIFY') {
|
||||
await applyRaidVerifyRole(context, member);
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.lockdownActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `automod:raid:${member.guild.id}`;
|
||||
const count = await context.redis.incr(key);
|
||||
if (count === 1) {
|
||||
@@ -214,10 +224,6 @@ export async function handleAntiRaidJoin(
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.lockdownActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
await context.prisma.autoModConfig.update({
|
||||
where: { guildId: member.guild.id },
|
||||
data: { lockdownActive: true }
|
||||
@@ -225,16 +231,45 @@ export async function handleAntiRaidJoin(
|
||||
|
||||
if (config.antiRaidAction === 'LOCKDOWN') {
|
||||
await activateLockdown(member.guild);
|
||||
} else if (config.antiRaidAction === 'VERIFY') {
|
||||
await applyRaidVerifyRole(context, member);
|
||||
}
|
||||
|
||||
const alertChannel = member.guild.systemChannel ?? member.guild.publicUpdatesChannel;
|
||||
if (alertChannel && alertChannel.isTextBased()) {
|
||||
await (alertChannel as TextChannel).send(
|
||||
`Anti-raid triggered: ${count} joins in ${config.antiRaidWindowSeconds}s.`
|
||||
`Anti-raid triggered: ${count} joins in ${config.antiRaidWindowSeconds}s (${config.antiRaidAction}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyRaidVerifyRole(context: BotContext, member: GuildMember): Promise<void> {
|
||||
const verification = await context.prisma.verificationConfig.findUnique({
|
||||
where: { guildId: member.guild.id }
|
||||
});
|
||||
const roleId = verification?.unverifiedRoleId;
|
||||
if (!roleId) {
|
||||
logger.warn(
|
||||
{ guildId: member.guild.id },
|
||||
'Anti-raid VERIFY triggered but no unverified role is configured'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const me = member.guild.members.me;
|
||||
if (!me?.permissions.has(PermissionFlagsBits.ManageRoles)) {
|
||||
return;
|
||||
}
|
||||
const role = member.guild.roles.cache.get(roleId);
|
||||
if (!role || role.position >= me.roles.highest.position) {
|
||||
return;
|
||||
}
|
||||
if (!member.roles.cache.has(roleId)) {
|
||||
await member.roles.add(roleId).catch((error) => {
|
||||
logger.warn({ error, memberId: member.id }, 'Failed to apply unverified role during raid verify');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleAntiNukeAction(
|
||||
context: BotContext,
|
||||
guildId: string,
|
||||
|
||||
@@ -18,12 +18,17 @@ export async function getAutoModConfig(prisma: PrismaClient, guildId: string) {
|
||||
|
||||
export async function ensureDefaultAutoModRules(prisma: PrismaClient, guildId: string) {
|
||||
await ensureGuild(prisma, guildId);
|
||||
const existing = await prisma.autoModRule.count({ where: { guildId } });
|
||||
if (existing > 0) {
|
||||
const existing = await prisma.autoModRule.findMany({
|
||||
where: { guildId },
|
||||
select: { ruleType: true }
|
||||
});
|
||||
const have = new Set(existing.map((row) => row.ruleType));
|
||||
const missing = DEFAULT_AUTOMOD_RULES.filter((rule) => !have.has(rule.ruleType));
|
||||
if (missing.length === 0) {
|
||||
return;
|
||||
}
|
||||
await prisma.autoModRule.createMany({
|
||||
data: DEFAULT_AUTOMOD_RULES.map((rule) => ({
|
||||
data: missing.map((rule) => ({
|
||||
guildId,
|
||||
ruleType: rule.ruleType,
|
||||
action: rule.action,
|
||||
|
||||
@@ -164,14 +164,20 @@ export const slowmodeCommandData = applyCommandDescription(
|
||||
export const lockCommandData = applyCommandDescription(
|
||||
new SlashCommandBuilder()
|
||||
.setName('lock')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels),
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels)
|
||||
.addBooleanOption((o) =>
|
||||
applyOptionDescription(o.setName('server'), 'moderation.lock.options.server')
|
||||
),
|
||||
'moderation.lock.description'
|
||||
);
|
||||
|
||||
export const unlockCommandData = applyCommandDescription(
|
||||
new SlashCommandBuilder()
|
||||
.setName('unlock')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels),
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels)
|
||||
.addBooleanOption((o) =>
|
||||
applyOptionDescription(o.setName('server'), 'moderation.unlock.options.server')
|
||||
),
|
||||
'moderation.unlock.description'
|
||||
);
|
||||
|
||||
|
||||
@@ -456,18 +456,31 @@ const lockCommand: SlashCommand = {
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageChannels, locale))) return;
|
||||
if (interaction.channel?.type === ChannelType.GuildText) {
|
||||
await interaction.channel.permissionOverwrites.edit(interaction.guild!.roles.everyone, {
|
||||
const serverWide = interaction.options.getBoolean('server') ?? false;
|
||||
const guild = interaction.guild!;
|
||||
|
||||
if (serverWide) {
|
||||
for (const channel of guild.channels.cache.values()) {
|
||||
if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) {
|
||||
continue;
|
||||
}
|
||||
await channel.permissionOverwrites.edit(guild.roles.everyone, {
|
||||
SendMessages: false
|
||||
});
|
||||
}
|
||||
} else if (interaction.channel?.type === ChannelType.GuildText) {
|
||||
await interaction.channel.permissionOverwrites.edit(guild.roles.everyone, {
|
||||
SendMessages: false
|
||||
});
|
||||
}
|
||||
|
||||
const modCase = await createCase(context, {
|
||||
guildId: interaction.guildId!,
|
||||
targetUserId: interaction.user.id,
|
||||
moderatorId: interaction.user.id,
|
||||
...auditFrom(interaction),
|
||||
action: 'LOCK',
|
||||
reason: 'Channel locked'
|
||||
reason: serverWide ? 'Server locked' : 'Channel locked'
|
||||
});
|
||||
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
||||
}
|
||||
@@ -478,18 +491,31 @@ const unlockCommand: SlashCommand = {
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageChannels, locale))) return;
|
||||
if (interaction.channel?.type === ChannelType.GuildText) {
|
||||
await interaction.channel.permissionOverwrites.edit(interaction.guild!.roles.everyone, {
|
||||
const serverWide = interaction.options.getBoolean('server') ?? false;
|
||||
const guild = interaction.guild!;
|
||||
|
||||
if (serverWide) {
|
||||
for (const channel of guild.channels.cache.values()) {
|
||||
if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) {
|
||||
continue;
|
||||
}
|
||||
await channel.permissionOverwrites.edit(guild.roles.everyone, {
|
||||
SendMessages: null
|
||||
});
|
||||
}
|
||||
} else if (interaction.channel?.type === ChannelType.GuildText) {
|
||||
await interaction.channel.permissionOverwrites.edit(guild.roles.everyone, {
|
||||
SendMessages: null
|
||||
});
|
||||
}
|
||||
|
||||
const modCase = await createCase(context, {
|
||||
guildId: interaction.guildId!,
|
||||
targetUserId: interaction.user.id,
|
||||
moderatorId: interaction.user.id,
|
||||
...auditFrom(interaction),
|
||||
action: 'UNLOCK',
|
||||
reason: 'Channel unlocked'
|
||||
reason: serverWide ? 'Server unlocked' : 'Channel unlocked'
|
||||
});
|
||||
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user