Ban command

This commit is contained in:
Tarrgon
2025-06-03 14:16:37 -04:00
parent b2e1b4beee
commit 77a482ac0f
11 changed files with 197 additions and 15 deletions
+104
View File
@@ -0,0 +1,104 @@
import { ApplicationIntegrationType, BitFieldResolvable, ChatInputCommandInteraction, Client, GuildBasedChannel, GuildMember, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder, time, TimestampStyles } from 'discord.js';
import { channelIsInStaffCategory, deferInteraction, handleWhoIsInteraction } from '../utils';
import { getRecordMessageFromDiscordId } from '../utils/record-utils';
import { Database } from '../shared/Database';
export default {
name: 'ban',
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Bans a user.')
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
.setContexts(InteractionContextType.Guild)
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
.addUserOption(option =>
option
.setName('user')
.setDescription('The discord user to ban.')
.setRequired(true)
)
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason for the ban')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('hours')
.setDescription('The duration of the ban, added with other options (0 for permanent).')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('minutes')
.setDescription('The duration of the ban, added with other options (0 for permanent).')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('seconds')
.setDescription('The duration of the ban, added with other options (0 for permanent).')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('delete-message-days')
.setDescription('How far back to delete messages (in days, default: 0 days).')
.setRequired(false)
.setMinValue(0)
.setMaxValue(7)
),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
await deferInteraction(interaction);
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
if (!interaction.guild.members.me) return interaction.editReply('An error has occurred. Please try again later.');
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason') ?? '';
const hours = (interaction.options.getNumber('hours') ?? 0) * 3.6e+6;
const minutes = (interaction.options.getNumber('minutes') ?? 0) * 60000;
const seconds = (interaction.options.getNumber('seconds') ?? 0) * 1000;
const duration = hours + minutes + seconds;
const deleteMessageDays = (interaction.options.getNumber('delete-message-days') ?? 0) * 86400;
let banMember: GuildMember | null = null;
try {
banMember = await interaction.guild.members.fetch(user.id);
} catch (e) {
// Member not in server.
}
const member = await interaction.guild.members.fetch(interaction.user.id);
if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) {
return await interaction.editReply('You do not have permission to ban this user.');
}
if (banMember && !banMember.bannable) {
return await interaction.editReply('I do not have permission to ban this user.');
}
const expiresAt = new Date(Date.now() + duration);
if (duration > 0) await Database.putBan(user.id, expiresAt);
try {
await interaction.guild.bans.create(user, {
reason: (reason + ` Banned by ${interaction.user.username} (${interaction.user.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim(),
deleteMessageSeconds: deleteMessageDays
});
} catch (e) {
console.error(e);
return await interaction.editReply("Error banning user (couldn't ban).");
}
await interaction.editReply('Banned.');
}
};
+10 -2
View File
@@ -1,4 +1,4 @@
import { ApplicationIntegrationType, BitFieldResolvable, ChatInputCommandInteraction, Client, GuildBasedChannel, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; import { ApplicationIntegrationType, BitFieldResolvable, ChatInputCommandInteraction, Client, GuildBasedChannel, GuildMember, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { channelIsInStaffCategory, deferInteraction, handleWhoIsInteraction } from '../utils'; import { channelIsInStaffCategory, deferInteraction, handleWhoIsInteraction } from '../utils';
import { getRecordMessageFromDiscordId } from '../utils/record-utils'; import { getRecordMessageFromDiscordId } from '../utils/record-utils';
@@ -26,6 +26,7 @@ export default {
option option
.setName('days') .setName('days')
.setDescription('How far back to delete messages (in days, default: 7 days).') .setDescription('How far back to delete messages (in days, default: 7 days).')
.setRequired(false)
.setMinValue(0) .setMinValue(0)
.setMaxValue(7) .setMaxValue(7)
), ),
@@ -40,7 +41,14 @@ export default {
const reason = interaction.options.getString('reason') ?? ''; const reason = interaction.options.getString('reason') ?? '';
const seconds = (interaction.options.getNumber('days') ?? 7) * 86400; const seconds = (interaction.options.getNumber('days') ?? 7) * 86400;
const banMember = await interaction.guild.members.fetch(user.id); let banMember: GuildMember | null = null;
try {
banMember = await interaction.guild.members.fetch(user.id);
} catch (e) {
// Member not in server.
}
const member = await interaction.guild.members.fetch(interaction.user.id); const member = await interaction.guild.members.fetch(interaction.user.id);
if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) { if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) {
+6
View File
@@ -0,0 +1,6 @@
import { GuildBan } from 'discord.js';
import { Database } from '../shared/Database';
export async function handleBanRemove(ban: GuildBan) {
await Database.removeBan(ban.user.id);
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { Guild } from 'discord.js';
import { Database } from '../shared/Database'; import { Database } from '../shared/Database';
export async function handleGuildCreate(guild) { export async function handleGuildCreate(guild: Guild) {
try { try {
if (!await Database.getGuildSettings(guild.id)) await Database.putGuild(guild.id); if (!await Database.getGuildSettings(guild.id)) await Database.putGuild(guild.id);
} catch (e) { } catch (e) {
+1
View File
@@ -1,4 +1,5 @@
export * from './handle-audit-log-create'; export * from './handle-audit-log-create';
export * from './handle-ban-remove';
export * from './handle-guild-create'; export * from './handle-guild-create';
export * from './handle-member-join'; export * from './handle-member-join';
export * from './handle-message'; export * from './handle-message';
+9 -10
View File
@@ -2,11 +2,11 @@ import 'source-map-support/register';
import { Client as DiscordClient, GatewayIntentBits, Guild, MessageFlags, Partials } from 'discord.js'; import { Client as DiscordClient, GatewayIntentBits, Guild, MessageFlags, Partials } from 'discord.js';
import { config } from './config'; import { config } from './config';
import { Handler } from './types'; import { Handler } from './types';
import { initIfNecessary, loadHandlersFrom, refreshCommands } from './utils'; import { checkExpiredBans, initIfNecessary, loadHandlersFrom, refreshCommands } from './utils';
import { initializeDiscordJoiner } from './discord-joiner'; import { initializeDiscordJoiner } from './discord-joiner';
import { Database } from './shared/Database'; import { Database } from './shared/Database';
import { handleAuditLogCreate, handleBulkMessageDelete, handleGuildCreate, handleMemberJoin, handleMessageCreate, handleMessageDelete, handleMessageUpdate, handleThreadCreate, handleVoiceStateUpdate } from './events'; import { handleAuditLogCreate, handleBanRemove, handleBulkMessageDelete, handleGuildCreate, handleMemberJoin, handleMessageCreate, handleMessageDelete, handleMessageUpdate, handleThreadCreate, handleVoiceStateUpdate } from './events';
import { ticketCooldownMap } from './shared/ticket-cooldown'; import { pruneOldTickets, ticketCooldownMap } from './shared/ticket-cooldown';
import { openRedisClient } from './shared/RedisClient'; import { openRedisClient } from './shared/RedisClient';
let ready = false; let ready = false;
@@ -138,13 +138,11 @@ client.on('ready', async () => {
await initializeDiscordJoiner(); await initializeDiscordJoiner();
// Prune ticket cooldowns that are expired every day // Prune ticket cooldowns that are expired every day
setInterval(() => { setInterval(pruneOldTickets, 8.64e+7);
const keys = Array.from(ticketCooldownMap.keys());
for (const key of keys) { // Check for expired bans every 5 minutes
if (Date.now() >= ticketCooldownMap.get(key)!) checkExpiredBans(client);
ticketCooldownMap.delete(key); setInterval(checkExpiredBans.bind(null, client), 300000);
}
}, 8.64e+7);
ready = true; ready = true;
@@ -152,6 +150,7 @@ client.on('ready', async () => {
}); });
client.on('guildAuditLogEntryCreate', handleAuditLogCreate); client.on('guildAuditLogEntryCreate', handleAuditLogCreate);
client.on('guildBanRemove', handleBanRemove);
client.on('guildCreate', handleGuildCreate); client.on('guildCreate', handleGuildCreate);
client.on('guildMemberAdd', handleMemberJoin); client.on('guildMemberAdd', handleMemberJoin);
client.on('messageCreate', handleMessageCreate); client.on('messageCreate', handleMessageCreate);
+27 -1
View File
@@ -3,7 +3,7 @@ import { open, Database as SqliteDatabase } from 'sqlite';
import { config } from '../config'; import { config } from '../config';
import DiscordOAuth2 from 'discord-oauth2'; import DiscordOAuth2 from 'discord-oauth2';
import { serializeMessage, wait } from '../utils'; import { serializeMessage, wait } from '../utils';
import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note } from '../types'; import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban } from '../types';
import { Message } from '../events'; import { Message } from '../events';
const DB_SCHEMA = ` const DB_SCHEMA = `
@@ -60,6 +60,12 @@ const DB_SCHEMA = `
mod_id TEXT, mod_id TEXT,
timestamp datetime NOT NULL DEFAULT (datetime('now', 'localtime')) timestamp datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
); );
CREATE TABLE IF NOT EXISTS bans (
id INTEGER PRIMARY KEY,
user_id TEXT,
expires_at datetime
);
`; `;
export class Database { export class Database {
@@ -284,4 +290,24 @@ export class Database {
} }
// -- END NOTES -- // -- END NOTES --
// -- START BANS --
static async putBan(userId: string, expiresAt: Date) {
await Database.db.run('INSERT INTO bans(user_id, expires_at) VALUES (?, ?)', userId, expiresAt);
}
static async getExpiredBans(date: Date): Promise<Ban[]> {
return await Database.db.all<Ban[]>('SELECT * from bans WHERE expires_at <= ?', date);
}
static async pruneExpiredBans(date: Date) {
await Database.db.all<Ban[]>('DELETE from bans WHERE expires_at <= ?', date);
}
static async removeBan(userId: string) {
await Database.db.run('DELETE from bans WHERE user_id = ?', userId);
}
// -- END BANS --
} }
+8
View File
@@ -1 +1,9 @@
export const ticketCooldownMap = new Map<string, number>(); export const ticketCooldownMap = new Map<string, number>();
export function pruneOldTickets() {
const keys = Array.from(ticketCooldownMap.keys());
for (const key of keys) {
if (Date.now() >= ticketCooldownMap.get(key)!)
ticketCooldownMap.delete(key);
}
}
+6
View File
@@ -39,3 +39,9 @@ export type Note = {
mod_id: string mod_id: string
timestamp: string timestamp: string
} }
export type Ban = {
id: number
user_id: string
expires_at: string
}
+22
View File
@@ -0,0 +1,22 @@
import { Client } from 'discord.js';
import { Database } from '../shared/Database';
import { config } from '../config';
export async function checkExpiredBans(client: Client) {
const guild = await client.guilds.fetch(config.DISCORD_GUILD_ID!);
if (!guild) return;
const date = new Date();
for (const ban of await Database.getExpiredBans(date)) {
try {
await guild.bans.remove(ban.user_id);
} catch (e) {
console.error(`Error unbanning user: ${ban.user_id}`);
console.error(e);
}
}
await Database.pruneExpiredBans(date);
}
+1
View File
@@ -1,6 +1,7 @@
export * from './alt-utils'; export * from './alt-utils';
export * from './array-utils'; export * from './array-utils';
export * from './audit-log-utils'; export * from './audit-log-utils';
export * from './ban-utils';
export * from './channel-utils'; export * from './channel-utils';
export * from './commands'; export * from './commands';
export * from './e621-utils'; export * from './e621-utils';