Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"ms": "^2.1.3",
"sharp": "^0.35.3",
"syllable-count-english": "^1.0.6",
"undici": "^8.7.0",
"undici": "^8.10.0",
"zod": "^4.4.3"
},
"devDependencies": {
Expand Down
89 changes: 89 additions & 0 deletions apps/bot/src/commands/moderation/lockdown/lockdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// QuestBot: A free and open-source Discord Bot.
// Copyright(C) 2026 Vantern
// SPDX-License-Identifier: AGPL-3.0-or-later

import { BucketScope, Command } from '@sapphire/framework';
import { EmbedBuilder, MessageFlags, PermissionFlagsBits } from 'discord.js';
import { isServerLocked, lockdownServer } from '#lib/lockdown.js';
import { logEmbed } from '#lib/logging.js';
import { errorEmbed, infoEmbed, successEmbed } from '#utils/embeds.js';
import { emojis } from '#utils/emoji.js';

export class LockdownCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
preconditions: ['devMode'],
cooldownDelay: 30_000,
cooldownLimit: 1,
cooldownScope: BucketScope.Guild,
});
}

public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName('lockdown')
.setDescription('Lock down all channels in the server, preventing everyone from speaking.')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
);
}

public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
if (!interaction.inCachedGuild()) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} This command can only be used in a server.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} You do not have permission to lock down the server.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

if (await isServerLocked(interaction.guildId)) {
await interaction.reply({
embeds: [infoEmbed(`${emojis.rightArrow2} The server is already locked down.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

// defer as we are about to change all channels perms
await interaction.deferReply({ flags: MessageFlags.Ephemeral });

try {
const { affected, skipped } = await lockdownServer(interaction.guild, `Lockdown by ${interaction.user.tag}`);

const logEntry = new EmbedBuilder()
.setTitle('Server Locked')
.setColor(0x000000)
.addFields(
{ name: 'Moderator', value: `<@${interaction.user.id}>`, inline: true },
{ name: 'Locked', value: String(affected), inline: true },
{ name: 'Skipped', value: String(skipped), inline: true },
)
.setTimestamp();

await logEmbed(interaction.guild, logEntry);

await interaction.editReply({
embeds: [
successEmbed(
`${emojis.rightArrow1} Locked ${affected} channel${affected === 1 ? '' : 's'}.${skipped ? `\nSkipped ${skipped} I could not lock.` : ''}`,
),
],
});
} catch (error) {
console.error(error);
await interaction.editReply({
embeds: [errorEmbed(`${emojis.rightArrow2} Failed to lock down this server.`)],
});
}
}
}
88 changes: 88 additions & 0 deletions apps/bot/src/commands/moderation/lockdown/unlockdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// QuestBot: A free and open-source Discord Bot.
// Copyright(C) 2026 Vantern
// SPDX-License-Identifier: AGPL-3.0-or-later

import { BucketScope, Command } from '@sapphire/framework';
import { EmbedBuilder, MessageFlags, PermissionFlagsBits } from 'discord.js';
import { isServerLocked, unLockdown } from '#lib/lockdown.js';
import { logEmbed } from '#lib/logging.js';
import { errorEmbed, infoEmbed, successEmbed } from '#utils/embeds.js';
import { emojis } from '#utils/emoji.js';

export class UnlockdownCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
preconditions: ['devMode'],
cooldownDelay: 30_000,
cooldownLimit: 1,
cooldownScope: BucketScope.Guild,
});
}

public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName('unlockdown')
.setDescription('Unlock the whole server.')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
);
}

public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
if (!interaction.inCachedGuild()) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} This command can only be used in a server.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} You do not have permission to unlock the server.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

if (!(await isServerLocked(interaction.guildId))) {
await interaction.reply({
embeds: [infoEmbed(`${emojis.rightArrow2} The server is not locked down.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

await interaction.deferReply({ flags: MessageFlags.Ephemeral });

try {
const { affected, skipped } = await unLockdown(interaction.guild, `Lockdown removed by ${interaction.user.tag}`);

const logEntry = new EmbedBuilder()
.setTitle('Server Unlocked')
.setColor(0x77dd76)
.addFields(
{ name: 'Moderator', value: `<@${interaction.user.id}>`, inline: true },
{ name: 'Unlocked', value: String(affected), inline: true },
{ name: 'Skipped', value: String(skipped), inline: true },
)
.setTimestamp();

await logEmbed(interaction.guild, logEntry);

await interaction.editReply({
embeds: [
successEmbed(
`${emojis.rightArrow1} Unlocked ${affected} channel${affected === 1 ? '' : 's'}.${skipped ? `\nSkipped ${skipped} I could not unlock.` : ''}`,
),
],
});
} catch (error) {
console.error(error);
await interaction.editReply({
embeds: [errorEmbed(`${emojis.rightArrow2} Failed to unlock the server.`)],
});
}
}
}
26 changes: 26 additions & 0 deletions apps/bot/src/commands/moderation/sticky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { Command } from '@sapphire/framework';
import {
EmbedBuilder,
type GuildTextBasedChannel,
MessageFlags,
PermissionFlagsBits,
Expand All @@ -12,6 +13,7 @@ import {
type SlashCommandSubcommandBuilder,
} from 'discord.js';
import { containsBlockedWord } from '#lib/automod.js';
import { logEmbed, truncate } from '#lib/logging.js';
import { getSticky, removeSticky, repostSticky, setSticky } from '#lib/sticky.js';
import { errorEmbed, infoEmbed, successEmbed } from '#utils/embeds.js';
import { emojis } from '#utils/emoji.js';
Expand Down Expand Up @@ -113,6 +115,18 @@ export class StickyCommand extends Command {
const sticky = await setSticky(interaction.guildId, interaction.guild.name, channel.id, content);
await repostSticky(channel, sticky, true);

const logEntry = new EmbedBuilder()
.setTitle('Sticky Message Set')
.setColor(0x77dd76)
.addFields(
{ name: 'Moderator', value: `<@${interaction.user.id}>`, inline: true },
{ name: 'Channel', value: channel.toString(), inline: true },
{ name: 'Message', value: truncate(content) || '-', inline: false }, // if this actually shows up as '-' then my security researcher would probably kill me and send me questbot's gh advisories
)
.setTimestamp();

await logEmbed(interaction.guild, logEntry);

await interaction.reply({
embeds: [successEmbed(`${emojis.rightArrow1} Sticky message set for this channel.`)],
flags: MessageFlags.Ephemeral,
Expand Down Expand Up @@ -143,6 +157,18 @@ export class StickyCommand extends Command {
await channel.messages.delete(sticky.stickyMessageId).catch(() => {});
}

const logEntry = new EmbedBuilder()
.setTitle('Sticky Message Removed')
.setColor(0xff6962)
.addFields(
{ name: 'Moderator', value: `<@${interaction.user.id}>`, inline: true },
{ name: 'Channel', value: channel.toString(), inline: true },
{ name: 'Message', value: truncate(sticky?.stickyContent) || '-', inline: false },
)
.setTimestamp();

await logEmbed(interaction.guild, logEntry);

await interaction.reply({
embeds: [successEmbed(`${emojis.rightArrow1} Sticky message removed from this channel.`)],
flags: MessageFlags.Ephemeral,
Expand Down
Loading