mirror of
https://github.com/ahmadk953/poixpixel-discord-bot.git
synced 2025-05-10 10:43:06 +00:00
feat: add kick, mute, and unmute commands
This commit is contained in:
parent
49d274f2be
commit
20af09b279
8 changed files with 395 additions and 7 deletions
88
src/commands/moderation/kick.ts
Normal file
88
src/commands/moderation/kick.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
|
||||||
|
|
||||||
|
import { updateMemberModerationHistory } from '@/db/db.js';
|
||||||
|
import { OptionsCommand } from '@/types/CommandTypes.js';
|
||||||
|
import logAction from '@/util/logging/logAction.js';
|
||||||
|
|
||||||
|
const command: OptionsCommand = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('kick')
|
||||||
|
.setDescription('Kick a member from the server')
|
||||||
|
.addUserOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('member')
|
||||||
|
.setDescription('The member to kick')
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('reason')
|
||||||
|
.setDescription('The reason for the kick')
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
execute: async (interaction) => {
|
||||||
|
const moderator = await interaction.guild?.members.fetch(
|
||||||
|
interaction.user.id,
|
||||||
|
);
|
||||||
|
const member = await interaction.guild?.members.fetch(
|
||||||
|
interaction.options.get('member')!.value as string,
|
||||||
|
);
|
||||||
|
const reason = interaction.options.get('reason')?.value as string;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!interaction.memberPermissions?.has(
|
||||||
|
PermissionsBitField.Flags.KickMembers,
|
||||||
|
) ||
|
||||||
|
moderator!.roles.highest.position <= member!.roles.highest.position ||
|
||||||
|
!member?.kickable
|
||||||
|
) {
|
||||||
|
await interaction.reply({
|
||||||
|
content:
|
||||||
|
'You do not have permission to kick members or this member cannot be kicked.',
|
||||||
|
flags: ['Ephemeral'],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await member.user.send(
|
||||||
|
`You have been kicked from ${interaction.guild!.name}. Reason: ${reason}. You can join back at: \nhttps://discord.gg/KRTGjxx7gY`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send DM to kicked user:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await member.kick(reason);
|
||||||
|
|
||||||
|
await updateMemberModerationHistory({
|
||||||
|
discordId: member.id,
|
||||||
|
moderatorDiscordId: interaction.user.id,
|
||||||
|
action: 'kick',
|
||||||
|
reason,
|
||||||
|
duration: '',
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await logAction({
|
||||||
|
guild: interaction.guild!,
|
||||||
|
action: 'kick',
|
||||||
|
target: member,
|
||||||
|
moderator: moderator!,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: `<@${member.id}> has been kicked. Reason: ${reason}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Kick command error:', error);
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Unable to kick member.',
|
||||||
|
flags: ['Ephemeral'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default command;
|
115
src/commands/moderation/mute.ts
Normal file
115
src/commands/moderation/mute.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
|
||||||
|
|
||||||
|
import { updateMember, updateMemberModerationHistory } from '@/db/db.js';
|
||||||
|
import { parseDuration } from '@/util/helpers.js';
|
||||||
|
import { OptionsCommand } from '@/types/CommandTypes.js';
|
||||||
|
import logAction from '@/util/logging/logAction.js';
|
||||||
|
|
||||||
|
const command: OptionsCommand = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('mute')
|
||||||
|
.setDescription('Timeout a member in the server')
|
||||||
|
.addUserOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('member')
|
||||||
|
.setDescription('The member to timeout')
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('reason')
|
||||||
|
.setDescription('The reason for the timeout')
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('duration')
|
||||||
|
.setDescription(
|
||||||
|
'The duration of the timeout (ex. 5m, 1h, 1d, 1w). Max 28 days.',
|
||||||
|
)
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
execute: async (interaction) => {
|
||||||
|
const moderator = await interaction.guild?.members.fetch(
|
||||||
|
interaction.user.id,
|
||||||
|
);
|
||||||
|
const member = await interaction.guild?.members.fetch(
|
||||||
|
interaction.options.get('member')!.value as string,
|
||||||
|
);
|
||||||
|
const reason = interaction.options.get('reason')?.value as string;
|
||||||
|
const muteDuration = interaction.options.get('duration')?.value as string;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!interaction.memberPermissions?.has(
|
||||||
|
PermissionsBitField.Flags.ModerateMembers,
|
||||||
|
) ||
|
||||||
|
moderator!.roles.highest.position <= member!.roles.highest.position ||
|
||||||
|
!member?.moderatable
|
||||||
|
) {
|
||||||
|
await interaction.reply({
|
||||||
|
content:
|
||||||
|
'You do not have permission to timeout members or this member cannot be timed out.',
|
||||||
|
flags: ['Ephemeral'],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const durationMs = parseDuration(muteDuration);
|
||||||
|
const maxTimeout = 28 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
if (durationMs > maxTimeout) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Timeout duration cannot exceed 28 days.',
|
||||||
|
flags: ['Ephemeral'],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await member.user.send(
|
||||||
|
`You have been timed out in ${interaction.guild!.name} for ${muteDuration}. Reason: ${reason}.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await member.timeout(durationMs, reason);
|
||||||
|
|
||||||
|
const expiresAt = new Date(Date.now() + durationMs);
|
||||||
|
|
||||||
|
await updateMemberModerationHistory({
|
||||||
|
discordId: member.id,
|
||||||
|
moderatorDiscordId: interaction.user.id,
|
||||||
|
action: 'mute',
|
||||||
|
reason,
|
||||||
|
duration: muteDuration,
|
||||||
|
createdAt: new Date(),
|
||||||
|
expiresAt,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateMember({
|
||||||
|
discordId: member.id,
|
||||||
|
currentlyMuted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await logAction({
|
||||||
|
guild: interaction.guild!,
|
||||||
|
action: 'mute',
|
||||||
|
target: member,
|
||||||
|
moderator: moderator!,
|
||||||
|
reason,
|
||||||
|
duration: muteDuration,
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: `<@${member.id}> has been timed out for ${muteDuration}. Reason: ${reason}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Mute command error:', error);
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Unable to timeout member.',
|
||||||
|
flags: ['Ephemeral'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default command;
|
65
src/commands/moderation/unmute.ts
Normal file
65
src/commands/moderation/unmute.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
|
||||||
|
|
||||||
|
import { executeUnmute } from '@/util/helpers.js';
|
||||||
|
import { OptionsCommand } from '@/types/CommandTypes.js';
|
||||||
|
|
||||||
|
const command: OptionsCommand = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('unmute')
|
||||||
|
.setDescription('Remove a timeout from a member')
|
||||||
|
.addUserOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('member')
|
||||||
|
.setDescription('The member to unmute')
|
||||||
|
.setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('reason')
|
||||||
|
.setDescription('The reason for removing the timeout')
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
execute: async (interaction) => {
|
||||||
|
const moderator = await interaction.guild?.members.fetch(
|
||||||
|
interaction.user.id,
|
||||||
|
);
|
||||||
|
const member = await interaction.guild?.members.fetch(
|
||||||
|
interaction.options.get('member')!.value as string,
|
||||||
|
);
|
||||||
|
const reason = interaction.options.get('reason')?.value as string;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!interaction.memberPermissions?.has(
|
||||||
|
PermissionsBitField.Flags.ModerateMembers,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'You do not have permission to unmute members.',
|
||||||
|
flags: ['Ephemeral'],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await executeUnmute(
|
||||||
|
interaction.client,
|
||||||
|
interaction.guild!.id,
|
||||||
|
member!.id,
|
||||||
|
reason,
|
||||||
|
moderator,
|
||||||
|
);
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: `<@${member!.id}>'s timeout has been removed. Reason: ${reason}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unmute command error:', error);
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'Unable to unmute member.',
|
||||||
|
flags: ['Ephemeral'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default command;
|
|
@ -54,9 +54,6 @@ const command: OptionsCommand = {
|
||||||
await member!.user.send(
|
await member!.user.send(
|
||||||
`You have been warned in **${interaction?.guild?.name}**. Reason: **${reason}**.`,
|
`You have been warned in **${interaction?.guild?.name}**. Reason: **${reason}**.`,
|
||||||
);
|
);
|
||||||
await interaction.reply(
|
|
||||||
`<@${member!.user.id}> has been warned. Reason: ${reason}`,
|
|
||||||
);
|
|
||||||
await logAction({
|
await logAction({
|
||||||
guild: interaction.guild!,
|
guild: interaction.guild!,
|
||||||
action: 'warn',
|
action: 'warn',
|
||||||
|
@ -64,6 +61,9 @@ const command: OptionsCommand = {
|
||||||
moderator: moderator!,
|
moderator: moderator!,
|
||||||
reason: reason,
|
reason: reason,
|
||||||
});
|
});
|
||||||
|
await interaction.reply(
|
||||||
|
`<@${member!.user.id}> has been warned. Reason: ${reason}`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
|
|
|
@ -133,6 +133,7 @@ export async function updateMember({
|
||||||
discordUsername,
|
discordUsername,
|
||||||
currentlyInServer,
|
currentlyInServer,
|
||||||
currentlyBanned,
|
currentlyBanned,
|
||||||
|
currentlyMuted,
|
||||||
}: schema.memberTableTypes): Promise<void> {
|
}: schema.memberTableTypes): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await ensureDbInitialized();
|
await ensureDbInitialized();
|
||||||
|
@ -147,6 +148,7 @@ export async function updateMember({
|
||||||
discordUsername,
|
discordUsername,
|
||||||
currentlyInServer,
|
currentlyInServer,
|
||||||
currentlyBanned,
|
currentlyBanned,
|
||||||
|
currentlyMuted,
|
||||||
})
|
})
|
||||||
.where(eq(schema.memberTable.discordId, discordId));
|
.where(eq(schema.memberTable.discordId, discordId));
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
|
|
||||||
import { updateMember, setMembers } from '@/db/db.js';
|
import { updateMember, setMembers } from '@/db/db.js';
|
||||||
import { generateMemberBanner } from '@/util/helpers.js';
|
import { executeUnmute, generateMemberBanner } from '@/util/helpers.js';
|
||||||
import { loadConfig } from '@/util/configLoader.js';
|
import { loadConfig } from '@/util/configLoader.js';
|
||||||
import { Event } from '@/types/EventTypes.js';
|
import { Event } from '@/types/EventTypes.js';
|
||||||
import logAction from '@/util/logging/logAction.js';
|
import logAction from '@/util/logging/logAction.js';
|
||||||
|
@ -144,6 +144,21 @@ export const memberUpdate: Event<typeof Events.GuildMemberUpdate> = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
oldMember.communicationDisabledUntil !==
|
||||||
|
newMember.communicationDisabledUntil &&
|
||||||
|
newMember.communicationDisabledUntil === null
|
||||||
|
) {
|
||||||
|
executeUnmute(
|
||||||
|
newMember.client,
|
||||||
|
guild.id,
|
||||||
|
newMember.user.id,
|
||||||
|
undefined,
|
||||||
|
guild.members.me!,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling member update:', error);
|
console.error('Error handling member update:', error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
setDiscordClient as setRedisDiscordClient,
|
setDiscordClient as setRedisDiscordClient,
|
||||||
} from '@/db/redis.js';
|
} from '@/db/redis.js';
|
||||||
import { setDiscordClient as setDbDiscordClient } from '@/db/db.js';
|
import { setDiscordClient as setDbDiscordClient } from '@/db/db.js';
|
||||||
import { loadActiveBans } from '@/util/helpers.js';
|
import { loadActiveBans, loadActiveMutes } from '@/util/helpers.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.ClientReady,
|
name: Events.ClientReady,
|
||||||
|
@ -38,6 +38,7 @@ export default {
|
||||||
await setMembers(nonBotMembers);
|
await setMembers(nonBotMembers);
|
||||||
|
|
||||||
await loadActiveBans(client, guild);
|
await loadActiveBans(client, guild);
|
||||||
|
await loadActiveMutes(client, guild);
|
||||||
|
|
||||||
await scheduleFactOfTheDay(client);
|
await scheduleFactOfTheDay(client);
|
||||||
await scheduleGiveaways(client);
|
await scheduleGiveaways(client);
|
||||||
|
|
|
@ -10,11 +10,12 @@ import {
|
||||||
ButtonStyle,
|
ButtonStyle,
|
||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
|
DiscordAPIError,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { and, eq } from 'drizzle-orm';
|
import { and, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
import { moderationTable } from '@/db/schema.js';
|
import { moderationTable } from '@/db/schema.js';
|
||||||
import { db, handleDbError, updateMember } from '@/db/db.js';
|
import { db, getMember, handleDbError, updateMember } from '@/db/db.js';
|
||||||
import logAction from './logging/logAction.js';
|
import logAction from './logging/logAction.js';
|
||||||
|
|
||||||
const __dirname = path.resolve();
|
const __dirname = path.resolve();
|
||||||
|
@ -116,6 +117,107 @@ export async function generateMemberBanner({
|
||||||
return attachment;
|
return attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes an unmute for a user
|
||||||
|
* @param client - The client to use
|
||||||
|
* @param guildId - The guild ID to unmute the user in
|
||||||
|
* @param userId - The user ID to unmute
|
||||||
|
* @param reason - The reason for the unmute
|
||||||
|
* @param moderator - The moderator who is unmuting the user
|
||||||
|
* @param alreadyUnmuted - Whether the user is already unmuted
|
||||||
|
*/
|
||||||
|
export async function executeUnmute(
|
||||||
|
client: Client,
|
||||||
|
guildId: string,
|
||||||
|
userId: string,
|
||||||
|
reason?: string,
|
||||||
|
moderator?: GuildMember,
|
||||||
|
alreadyUnmuted: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const guild = await client.guilds.fetch(guildId);
|
||||||
|
let member;
|
||||||
|
|
||||||
|
try {
|
||||||
|
member = await guild.members.fetch(userId);
|
||||||
|
if (!alreadyUnmuted) {
|
||||||
|
await member.timeout(null, reason ?? 'Temporary mute expired');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
`Member ${userId} not found in server, just updating database`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await getMember(userId))?.currentlyMuted) return;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(moderationTable)
|
||||||
|
.set({ active: false })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(moderationTable.discordId, userId),
|
||||||
|
eq(moderationTable.action, 'mute'),
|
||||||
|
eq(moderationTable.active, true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateMember({
|
||||||
|
discordId: userId,
|
||||||
|
currentlyMuted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (member) {
|
||||||
|
await logAction({
|
||||||
|
guild,
|
||||||
|
action: 'unmute',
|
||||||
|
target: member,
|
||||||
|
reason: reason ?? 'Temporary mute expired',
|
||||||
|
moderator: moderator ? moderator : guild.members.me!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error executing unmute:', error);
|
||||||
|
|
||||||
|
if (!(error instanceof DiscordAPIError && error.code === 10007)) {
|
||||||
|
handleDbError('Failed to execute unmute', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all active mutes and schedules unmute events
|
||||||
|
* @param client - The client to use
|
||||||
|
* @param guild - The guild to load mutes for
|
||||||
|
*/
|
||||||
|
export async function loadActiveMutes(
|
||||||
|
client: Client,
|
||||||
|
guild: Guild,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const activeMutes = await db
|
||||||
|
.select()
|
||||||
|
.from(moderationTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(moderationTable.action, 'mute'),
|
||||||
|
eq(moderationTable.active, true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const mute of activeMutes) {
|
||||||
|
if (!mute.expiresAt) continue;
|
||||||
|
|
||||||
|
const timeUntilUnmute = mute.expiresAt.getTime() - Date.now();
|
||||||
|
if (timeUntilUnmute <= 0) {
|
||||||
|
await executeUnmute(client, guild.id, mute.discordId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleDbError('Failed to load active mutes', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules an unban for a user
|
* Schedules an unban for a user
|
||||||
* @param client - The client to use
|
* @param client - The client to use
|
||||||
|
@ -174,7 +276,7 @@ export async function executeUnban(
|
||||||
guild,
|
guild,
|
||||||
action: 'unban',
|
action: 'unban',
|
||||||
target: guild.members.cache.get(userId)!,
|
target: guild.members.cache.get(userId)!,
|
||||||
moderator: guild.members.cache.get(client.user!.id)!,
|
moderator: guild.members.me!,
|
||||||
reason: reason ?? 'Temporary ban expired',
|
reason: reason ?? 'Temporary ban expired',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue