feat(bot): merge pull request #343 from ahmadk953/utils

chore(bot): finish base bot
This commit is contained in:
Ahmad 2025-04-17 01:31:29 -04:00 committed by GitHub
commit 15cb83be7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 1276 additions and 172 deletions

View file

@ -11,7 +11,7 @@
> [!WARNING]
> Documentation is still under construction. Expect incomplete and undocumented features.
All documentation and setup instructions can be found at [https://docs.poixpixel.ahmadk953.org/](https://docs.poixpixel.ahmadk953.org/)
All documentation and setup instructions can be found at [https://docs.poixpixel.ahmadk953.org/](https://docs.poixpixel.ahmadk953.org/?utm_source=github&utm_medium=readme&utm_campaign=repository&utm_content=docs_link)
## Development Commands

View file

@ -2,6 +2,7 @@
"token": "DISCORD_BOT_TOKEN",
"clientId": "DISCORD_BOT_ID",
"guildId": "DISCORD_SERVER_ID",
"serverInvite": "DISCORD_SERVER_INVITE_LINK",
"database": {
"dbConnectionString": "POSTGRESQL_CONNECTION_STRING",
"maxRetryAttempts": "MAX_RETRY_ATTEMPTS",

View file

@ -10,8 +10,10 @@
"compile": "npx tsc",
"target": "node ./target/discord-bot.js",
"start:dev": "yarn run compile && yarn run target",
"start:dev:no-deploy": "cross-env SKIP_COMMAND_DEPLOY=true yarn run start:dev",
"start:prod": "yarn compile && pm2 start ./target/discord-bot.js --name poixpixel-discord-bot",
"restart": "pm2 restart poixpixel-discord-bot",
"undeploy-commands": "yarn compile && node --experimental-specifier-resolution=node ./target/util/undeployCommands.js",
"lint": "npx eslint ./src && npx tsc --noEmit",
"format": "prettier --check --ignore-path .prettierignore .",
"format:fix": "prettier --write --ignore-path .prettierignore .",
@ -34,6 +36,7 @@
"@types/pg": "^8.11.13",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^8.30.1",
"cross-env": "^7.0.3",
"drizzle-kit": "^0.31.0",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",

View file

@ -148,8 +148,9 @@ const command = {
),
async execute(interaction: ChatInputCommandInteraction) {
await interaction.deferReply();
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply();
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {

View file

@ -33,8 +33,9 @@ const command: SubcommandCommand = {
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply();
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'status') {
@ -82,33 +83,40 @@ const command: SubcommandCommand = {
});
}
await interaction.reply({ embeds: [embed] });
await interaction.editReply({ embeds: [embed] });
} else if (subcommand === 'setcount') {
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
await interaction.editReply({
content: 'You need administrator permissions to use this command.',
flags: ['Ephemeral'],
});
return;
}
const count = interaction.options.getInteger('count');
if (count === null) {
await interaction.reply({
await interaction.editReply({
content: 'Invalid count specified.',
flags: ['Ephemeral'],
});
return;
}
try {
await setCount(count);
await interaction.reply({
await interaction.editReply({
content: `Count has been set to **${count}**. The next number should be **${count + 1}**.`,
});
} catch (error) {
await interaction.editReply({
content: `Failed to set the count: ${error}`,
});
}
await interaction.editReply({
content: `Count has been set to **${count}**. The next number should be **${count + 1}**.`,
flags: ['Ephemeral'],
});
}
},

View file

@ -74,12 +74,11 @@ const command: SubcommandCommand = {
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({
flags: ['Ephemeral'],
});
await interaction.editReply('Processing...');
const config = loadConfig();
const subcommand = interaction.options.getSubcommand();
@ -100,7 +99,7 @@ const command: SubcommandCommand = {
});
if (!isAdmin) {
const approvalChannel = interaction.guild?.channels.cache.get(
const approvalChannel = interaction.guild.channels.cache.get(
config.channels.factApproval,
);

View file

@ -18,6 +18,7 @@ import {
builder,
} from '@/util/giveaways/giveawayManager.js';
import { createPaginationButtons } from '@/util/helpers.js';
import { loadConfig } from '@/util/configLoader';
const command: SubcommandCommand = {
data: new SlashCommandBuilder()
@ -53,16 +54,30 @@ const command: SubcommandCommand = {
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (!interaction.isChatInputCommand() || !interaction.guild) return;
const config = loadConfig();
const communityManagerRoleId = config.roles.staffRoles.find(
(role) => role.name === 'Community Manager',
)?.roleId;
if (!communityManagerRoleId) {
await interaction.reply({
content:
'Community Manager role not found in the configuration. Please contact a server admin.',
flags: ['Ephemeral'],
});
return;
}
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.ModerateMembers,
)
!interaction.guild.members.cache
.find((member) => member.id === interaction.user.id)
?.roles.cache.has(communityManagerRoleId)
) {
await interaction.reply({
content: 'You do not have permission to manage giveaways.',
ephemeral: true,
flags: ['Ephemeral'],
});
return;
}
@ -152,7 +167,7 @@ async function handleListGiveaways(interaction: ChatInputCommandInteraction) {
if (i.user.id !== interaction.user.id) {
await i.reply({
content: 'You cannot use these buttons.',
ephemeral: true,
flags: ['Ephemeral'],
});
return;
}

View file

@ -22,13 +22,12 @@ const command: OptionsCommand = {
.setRequired(false),
),
execute: async (interaction) => {
if (!interaction.guild) return;
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply();
try {
const usersPerPage =
(interaction.options.get('limit')?.value as number) || 10;
const usersPerPage = interaction.options.getInteger('limit') || 10;
const allUsers = await getLevelLeaderboard(100);

View file

@ -15,18 +15,16 @@ const command: OptionsCommand = {
.setRequired(false),
),
execute: async (interaction) => {
const member = await interaction.guild?.members.fetch(
(interaction.options.get('user')?.value as string) || interaction.user.id,
);
if (!member) {
await interaction.reply('User not found in this server.');
return;
}
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply();
try {
const member = await interaction.guild.members.fetch(
(interaction.options.get('user')?.value as string) ||
interaction.user.id,
);
const userData = await getUserLevel(member.id);
const rankCard = await generateRankCard(member, userData);

View file

@ -3,6 +3,7 @@ import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
import { updateMember, updateMemberModerationHistory } from '@/db/db.js';
import { parseDuration, scheduleUnban } from '@/util/helpers.js';
import { OptionsCommand } from '@/types/CommandTypes.js';
import { loadConfig } from '@/util/configLoader.js';
import logAction from '@/util/logging/logAction.js';
const command: OptionsCommand = {
@ -30,10 +31,15 @@ const command: OptionsCommand = {
.setRequired(false),
),
execute: async (interaction) => {
const moderator = await interaction.guild?.members.fetch(
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
try {
const moderator = await interaction.guild.members.fetch(
interaction.user.id,
);
const member = await interaction.guild?.members.fetch(
const member = await interaction.guild.members.fetch(
interaction.options.get('member')!.value as string,
);
const reason = interaction.options.get('reason')?.value as string;
@ -44,26 +50,44 @@ const command: OptionsCommand = {
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.BanMembers,
) ||
moderator!.roles.highest.position <= member!.roles.highest.position ||
!member?.bannable
)
) {
await interaction.reply({
content:
'You do not have permission to ban members or this member cannot be banned.',
flags: ['Ephemeral'],
await interaction.editReply({
content: 'You do not have permission to ban members.',
});
return;
}
if (moderator.roles.highest.position <= member.roles.highest.position) {
await interaction.editReply({
content:
'You cannot ban a member with equal or higher role than yours.',
});
return;
}
if (!member.bannable) {
await interaction.editReply({
content: 'I do not have permission to ban this member.',
});
return;
}
const config = loadConfig();
const invite = interaction.guild.vanityURLCode ?? config.serverInvite;
const until = banDuration
? new Date(Date.now() + parseDuration(banDuration)).toUTCString()
: 'indefinitely';
try {
await member.user.send(
banDuration
? `You have been banned from ${interaction.guild!.name} for ${banDuration}. Reason: ${reason}. You can join back at ${new Date(
Date.now() + parseDuration(banDuration),
).toUTCString()} using the link below:\nhttps://discord.gg/KRTGjxx7gY`
: `You been indefinitely banned from ${interaction.guild!.name}. Reason: ${reason}.`,
? `You have been banned from ${interaction.guild.name} for ${banDuration}. Reason: ${reason}. You can join back at ${until} using the link below:\n${invite}`
: `You been indefinitely banned from ${interaction.guild.name}. Reason: ${reason}.`,
);
} catch (error) {
console.error('Failed to send DM:', error);
}
await member.ban({ reason });
if (banDuration) {
@ -72,7 +96,7 @@ const command: OptionsCommand = {
await scheduleUnban(
interaction.client,
interaction.guild!.id,
interaction.guild.id,
member.id,
expiresAt,
);
@ -94,23 +118,22 @@ const command: OptionsCommand = {
});
await logAction({
guild: interaction.guild!,
guild: interaction.guild,
action: 'ban',
target: member,
moderator: moderator!,
moderator,
reason,
});
await interaction.reply({
await interaction.editReply({
content: banDuration
? `<@${member.id}> has been banned for ${banDuration}. Reason: ${reason}`
: `<@${member.id}> has been indefinitely banned. Reason: ${reason}`,
});
} catch (error) {
console.error('Ban command error:', error);
await interaction.reply({
await interaction.editReply({
content: 'Unable to ban member.',
flags: ['Ephemeral'],
});
}
},

View file

@ -0,0 +1,103 @@
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
import { updateMemberModerationHistory } from '@/db/db.js';
import { OptionsCommand } from '@/types/CommandTypes.js';
import { loadConfig } from '@/util/configLoader.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) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
try {
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,
)
) {
await interaction.editReply({
content: 'You do not have permission to kick members.',
});
return;
}
if (moderator!.roles.highest.position <= member.roles.highest.position) {
await interaction.editReply({
content:
'You cannot kick a member with equal or higher role than yours.',
});
return;
}
if (!member.kickable) {
await interaction.editReply({
content: 'I do not have permission to kick this member.',
});
return;
}
try {
await member.user.send(
`You have been kicked from ${interaction.guild!.name}. Reason: ${reason}. You can join back at: \n${interaction.guild.vanityURLCode ?? loadConfig().serverInvite}`,
);
} 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,
reason,
});
await interaction.editReply({
content: `<@${member.id}> has been kicked. Reason: ${reason}`,
});
} catch (error) {
console.error('Kick command error:', error);
await interaction.editReply({
content: 'Unable to kick member.',
});
}
},
};
export default command;

View file

@ -0,0 +1,128 @@
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) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
try {
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.KickMembers,
)
) {
await interaction.editReply({
content: 'You do not have permission to mute members.',
});
return;
}
if (moderator.roles.highest.position <= member.roles.highest.position) {
await interaction.editReply({
content:
'You cannot mute a member with equal or higher role than yours.',
});
return;
}
if (!member.moderatable) {
await interaction.editReply({
content: 'I do not have permission to mute this member.',
});
return;
}
const durationMs = parseDuration(muteDuration);
const maxTimeout = 28 * 24 * 60 * 60 * 1000;
if (durationMs > maxTimeout) {
await interaction.editReply({
content: 'Timeout duration cannot exceed 28 days.',
});
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,
reason,
duration: muteDuration,
});
await interaction.editReply({
content: `<@${member.id}> has been muted for ${muteDuration}. Reason: ${reason}`,
});
} catch (error) {
console.error('Mute command error:', error);
await interaction.editReply({
content: 'Unable to timeout member.',
});
}
},
};
export default command;

View file

@ -20,52 +20,54 @@ const command: OptionsCommand = {
.setRequired(true),
),
execute: async (interaction) => {
const userId = interaction.options.get('userid')!.value as string;
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
try {
const userId = interaction.options.get('userid')?.value as string;
const reason = interaction.options.get('reason')?.value as string;
if (
!interaction.memberPermissions?.has(PermissionsBitField.Flags.BanMembers)
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.BanMembers,
)
) {
await interaction.reply({
await interaction.editReply({
content: 'You do not have permission to unban users.',
flags: ['Ephemeral'],
});
return;
}
try {
try {
const ban = await interaction.guild?.bans.fetch(userId);
const ban = await interaction.guild.bans.fetch(userId);
if (!ban) {
await interaction.reply({
await interaction.editReply({
content: 'This user is not banned.',
flags: ['Ephemeral'],
});
return;
}
} catch {
await interaction.reply({
await interaction.editReply({
content: 'Error getting ban. Is this user banned?',
flags: ['Ephemeral'],
});
return;
}
await executeUnban(
interaction.client,
interaction.guildId!,
interaction.guild.id,
userId,
reason,
);
await interaction.reply({
await interaction.editReply({
content: `<@${userId}> has been unbanned. Reason: ${reason}`,
});
} catch (error) {
console.error(error);
await interaction.reply({
console.error(`Unable to unban user: ${error}`);
await interaction.editReply({
content: 'Unable to unban user.',
flags: ['Ephemeral'],
});
}
},

View file

@ -0,0 +1,66 @@
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) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
try {
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.editReply({
content: 'You do not have permission to unmute members.',
});
return;
}
await executeUnmute(
interaction.client,
interaction.guild.id,
member.id,
reason,
moderator,
);
await interaction.editReply({
content: `<@${member.id}>'s timeout has been removed. Reason: ${reason}`,
});
} catch (error) {
console.error('Unmute command error:', error);
await interaction.editReply({
content: 'Unable to unmute member.',
});
}
},
};
export default command;

View file

@ -21,29 +21,38 @@ const command: OptionsCommand = {
.setRequired(true),
),
execute: async (interaction) => {
const moderator = await interaction.guild?.members.fetch(
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
try {
const moderator = await interaction.guild.members.fetch(
interaction.user.id,
);
const member = await interaction.guild?.members.fetch(
const member = await interaction.guild.members.fetch(
interaction.options.get('member')!.value as unknown as string,
);
const reason = interaction.options.get('reason')
?.value as unknown as string;
const reason = interaction.options.getString('reason')!;
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.ModerateMembers,
) ||
moderator!.roles.highest.position <= member!.roles.highest.position
)
) {
await interaction.reply({
content: 'You do not have permission to warn this member.',
flags: ['Ephemeral'],
await interaction.editReply({
content: 'You do not have permission to warn members.',
});
return;
}
if (moderator.roles.highest.position <= member.roles.highest.position) {
await interaction.editReply({
content:
'You cannot warn a member with equal or higher role than yours.',
});
return;
}
try {
await updateMemberModerationHistory({
discordId: member!.user.id,
moderatorDiscordId: interaction.user.id,
@ -54,9 +63,6 @@ const command: OptionsCommand = {
await member!.user.send(
`You have been warned in **${interaction?.guild?.name}**. Reason: **${reason}**.`,
);
await interaction.reply(
`<@${member!.user.id}> has been warned. Reason: ${reason}`,
);
await logAction({
guild: interaction.guild!,
action: 'warn',
@ -64,11 +70,13 @@ const command: OptionsCommand = {
moderator: moderator!,
reason: reason,
});
await interaction.editReply(
`<@${member!.user.id}> has been warned. Reason: ${reason}`,
);
} catch (error) {
console.error(error);
await interaction.reply({
await interaction.editReply({
content: 'There was an error trying to warn the member.',
flags: ['Ephemeral'],
});
}
},

View file

@ -8,25 +8,27 @@ const command: Command = {
.setDescription('Simulates a new member joining'),
execute: async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
const guild = interaction.guild;
await interaction.deferReply({ flags: ['Ephemeral'] });
if (
!interaction.memberPermissions!.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
await interaction.editReply({
content: 'You do not have permission to use this command.',
flags: ['Ephemeral'],
});
return;
}
const fakeMember = await guild!.members.fetch(interaction.user.id);
guild!.client.emit('guildMemberAdd', fakeMember);
const fakeMember = await guild.members.fetch(interaction.user.id);
guild.client.emit('guildMemberAdd', fakeMember);
await interaction.reply({
await interaction.editReply({
content: 'Triggered the join event!',
flags: ['Ephemeral'],
});
},
};

View file

@ -9,25 +9,26 @@ const command: Command = {
.setDescription('Simulates a member leaving'),
execute: async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
const guild = interaction.guild;
await interaction.deferReply({ flags: ['Ephemeral'] });
if (
!interaction.memberPermissions!.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
await interaction.editReply({
content: 'You do not have permission to use this command.',
flags: ['Ephemeral'],
});
}
const fakeMember = await guild!.members.fetch(interaction.user.id);
guild!.client.emit('guildMemberRemove', fakeMember);
const fakeMember = await guild.members.fetch(interaction.user.id);
guild.client.emit('guildMemberRemove', fakeMember);
await interaction.reply({
await interaction.editReply({
content: 'Triggered the leave event!',
flags: ['Ephemeral'],
});
await updateMember({

237
src/commands/util/config.ts Normal file
View file

@ -0,0 +1,237 @@
import {
SlashCommandBuilder,
EmbedBuilder,
PermissionFlagsBits,
} from 'discord.js';
import { Command } from '@/types/CommandTypes.js';
import { loadConfig } from '@/util/configLoader.js';
import { createPaginationButtons } from '@/util/helpers.js';
const command: Command = {
data: new SlashCommandBuilder()
.setName('config')
.setDescription('(Admin Only) Display the current configuration')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
if (
!interaction.memberPermissions?.has(PermissionFlagsBits.Administrator)
) {
await interaction.editReply({
content: 'You do not have permission to use this command.',
});
return;
}
const config = loadConfig();
const displayConfig = JSON.parse(JSON.stringify(config));
if (displayConfig.token) displayConfig.token = '••••••••••••••••••••••••••';
if (displayConfig.database?.dbConnectionString) {
displayConfig.database.dbConnectionString = '••••••••••••••••••••••••••';
}
if (displayConfig.redis?.redisConnectionString) {
displayConfig.redis.redisConnectionString = '••••••••••••••••••••••••••';
}
const pages: EmbedBuilder[] = [];
const basicConfigEmbed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle('Bot Configuration')
.setDescription(
'Current configuration settings (sensitive data redacted)',
)
.addFields(
{
name: 'Client ID',
value: displayConfig.clientId || 'Not set',
inline: true,
},
{
name: 'Guild ID',
value: displayConfig.guildId || 'Not set',
inline: true,
},
{
name: 'Token',
value: displayConfig.token || 'Not set',
inline: true,
},
);
pages.push(basicConfigEmbed);
if (displayConfig.database || displayConfig.redis) {
const dbRedisEmbed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle('Database and Redis Configuration')
.setDescription('Database and cache settings');
if (displayConfig.database) {
dbRedisEmbed.addFields({
name: 'Database',
value: `Connection: ${displayConfig.database.dbConnectionString}\nMax Retry: ${displayConfig.database.maxRetryAttempts}\nRetry Delay: ${displayConfig.database.retryDelay}ms`,
});
}
if (displayConfig.redis) {
dbRedisEmbed.addFields({
name: 'Redis',
value: `Connection: ${displayConfig.redis.redisConnectionString}\nRetry Attempts: ${displayConfig.redis.retryAttempts}\nInitial Retry Delay: ${displayConfig.redis.initialRetryDelay}ms`,
});
}
pages.push(dbRedisEmbed);
}
if (displayConfig.channels || displayConfig.roles) {
const channelsRolesEmbed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle('Channels and Roles Configuration')
.setDescription('Server channel and role settings');
if (displayConfig.channels) {
const channelsText = Object.entries(displayConfig.channels)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
channelsRolesEmbed.addFields({
name: 'Channels',
value: channelsText || 'None configured',
});
}
if (displayConfig.roles) {
let rolesText = '';
if (displayConfig.roles.joinRoles?.length) {
rolesText += `Join Roles: ${displayConfig.roles.joinRoles.join(', ')}\n`;
}
if (displayConfig.roles.levelRoles?.length) {
rolesText += `Level Roles: ${displayConfig.roles.levelRoles.length} configured\n`;
}
if (displayConfig.roles.staffRoles?.length) {
rolesText += `Staff Roles: ${displayConfig.roles.staffRoles.length} configured\n`;
}
if (displayConfig.roles.factPingRole) {
rolesText += `Fact Ping Role: ${displayConfig.roles.factPingRole}`;
}
channelsRolesEmbed.addFields({
name: 'Roles',
value: rolesText || 'None configured',
});
}
pages.push(channelsRolesEmbed);
}
if (
displayConfig.leveling ||
displayConfig.counting ||
displayConfig.giveaways
) {
const featuresEmbed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle('Feature Configurations')
.setDescription('Settings for bot features');
if (displayConfig.leveling) {
featuresEmbed.addFields({
name: 'Leveling',
value: `XP Cooldown: ${displayConfig.leveling.xpCooldown}s\nMin XP: ${displayConfig.leveling.minXpAwarded}\nMax XP: ${displayConfig.leveling.maxXpAwarded}`,
});
}
if (displayConfig.counting) {
const countingText = Object.entries(displayConfig.counting)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
featuresEmbed.addFields({
name: 'Counting',
value: countingText || 'Default settings',
});
}
if (displayConfig.giveaways) {
const giveawaysText = Object.entries(displayConfig.giveaways)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
featuresEmbed.addFields({
name: 'Giveaways',
value: giveawaysText || 'Default settings',
});
}
pages.push(featuresEmbed);
}
let currentPage = 0;
const components =
pages.length > 1
? [createPaginationButtons(pages.length, currentPage)]
: [];
const reply = await interaction.editReply({
embeds: [pages[currentPage]],
components,
});
if (pages.length <= 1) return;
const collector = reply.createMessageComponentCollector({
time: 300000,
});
collector.on('collect', async (i) => {
if (i.user.id !== interaction.user.id) {
await i.reply({
content: 'You cannot use these buttons.',
flags: ['Ephemeral'],
});
return;
}
switch (i.customId) {
case 'first':
currentPage = 0;
break;
case 'prev':
if (currentPage > 0) currentPage--;
break;
case 'next':
if (currentPage < pages.length - 1) currentPage++;
break;
case 'last':
currentPage = pages.length - 1;
break;
}
await i.update({
embeds: [pages[currentPage]],
components: [createPaginationButtons(pages.length, currentPage)],
});
});
collector.on('end', async () => {
try {
await interaction.editReply({ components: [] });
} catch (error) {
console.error('Failed to remove pagination buttons:', error);
}
});
},
};
export default command;

273
src/commands/util/help.ts Normal file
View file

@ -0,0 +1,273 @@
import {
SlashCommandBuilder,
EmbedBuilder,
ActionRowBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
ComponentType,
} from 'discord.js';
import { OptionsCommand } from '@/types/CommandTypes.js';
import { ExtendedClient } from '@/structures/ExtendedClient.js';
const DOC_BASE_URL = 'https://docs.poixpixel.ahmadk953.org/';
const getDocUrl = (location: string) =>
`${DOC_BASE_URL}?utm_source=discord&utm_medium=bot&utm_campaign=help_command&utm_content=${location}`;
const command: OptionsCommand = {
data: new SlashCommandBuilder()
.setName('help')
.setDescription('Shows a list of all available commands')
.addStringOption((option) =>
option
.setName('command')
.setDescription('Get detailed help for a specific command')
.setRequired(false),
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply();
try {
const client = interaction.client as ExtendedClient;
const commandName = interaction.options.getString('command');
if (commandName) {
return handleSpecificCommand(interaction, client, commandName);
}
const categories = new Map();
for (const [name, cmd] of client.commands) {
const category = getCategoryFromCommand(name);
if (!categories.has(category)) {
categories.set(category, []);
}
categories.get(category).push({
name,
description: cmd.data.toJSON().description,
});
}
const embed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle('Poixpixel Bot Commands')
.setDescription(
'**Welcome to Poixpixel Discord Bot!**\n\n' +
'Select a category from the dropdown menu below to see available commands.\n\n' +
`📚 **Documentation:** [Visit Our Documentation](${getDocUrl('main_description')})`,
)
.setThumbnail(client.user!.displayAvatarURL())
.setFooter({
text: 'Use /help [command] for detailed info about a command',
});
const categoryEmojis: Record<string, string> = {
fun: '🎮',
moderation: '🛡️',
util: '🔧',
testing: '🧪',
};
Array.from(categories.keys()).forEach((category) => {
const emoji = categoryEmojis[category] || '📁';
embed.addFields({
name: `${emoji} ${category.charAt(0).toUpperCase() + category.slice(1)}`,
value: `Use the dropdown to see ${category} commands`,
inline: true,
});
});
embed.addFields({
name: '📚 Documentation',
value: `[Click here to access our full documentation](${getDocUrl('main_footer_field')})`,
inline: false,
});
const selectMenu =
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
new StringSelectMenuBuilder()
.setCustomId('help_category_select')
.setPlaceholder('Select a command category')
.addOptions(
Array.from(categories.keys()).map((category) => {
const emoji = categoryEmojis[category] || '📁';
return new StringSelectMenuOptionBuilder()
.setLabel(
category.charAt(0).toUpperCase() + category.slice(1),
)
.setDescription(`View ${category} commands`)
.setValue(category)
.setEmoji(emoji);
}),
),
);
const message = await interaction.editReply({
embeds: [embed],
components: [selectMenu],
});
const collector = message.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
time: 60000,
});
collector.on('collect', async (i) => {
if (i.user.id !== interaction.user.id) {
await i.reply({
content: 'You cannot use this menu.',
ephemeral: true,
});
return;
}
const selectedCategory = i.values[0];
const commands = categories.get(selectedCategory);
const emoji = categoryEmojis[selectedCategory] || '📁';
const categoryEmbed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(
`${emoji} ${selectedCategory.charAt(0).toUpperCase() + selectedCategory.slice(1)} Commands`,
)
.setDescription('Here are all the commands in this category:')
.setFooter({
text: 'Use /help [command] for detailed info about a command',
});
commands.forEach((cmd: any) => {
categoryEmbed.addFields({
name: `/${cmd.name}`,
value: cmd.description || 'No description available',
inline: false,
});
});
categoryEmbed.addFields({
name: '📚 Documentation',
value: `[Click here to access our full documentation](${getDocUrl(`category_${selectedCategory}`)})`,
inline: false,
});
await i.update({ embeds: [categoryEmbed], components: [selectMenu] });
});
collector.on('end', () => {
interaction.editReply({ components: [] }).catch(console.error);
});
} catch (error) {
console.error('Error in help command:', error);
await interaction.editReply({
content: 'An error occurred while processing your request.',
});
}
},
};
/**
* Handle showing help for a specific command
*/
async function handleSpecificCommand(
interaction: any,
client: ExtendedClient,
commandName: string,
) {
const cmd = client.commands.get(commandName);
if (!cmd) {
return interaction.editReply({
content: `Command \`${commandName}\` not found.`,
ephemeral: true,
});
}
const embed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(`Help: /${commandName}`)
.setDescription(cmd.data.toJSON().description || 'No description available')
.addFields({
name: 'Category',
value: getCategoryFromCommand(commandName),
inline: true,
})
.setFooter({
text: `Poixpixel Discord Bot • Documentation: ${getDocUrl(`cmd_footer_${commandName}`)}`,
});
const options = cmd.data.toJSON().options;
if (options && options.length > 0) {
if (options[0].type === 1) {
embed.addFields({
name: 'Subcommands',
value: options
.map((opt: any) => `\`${opt.name}\`: ${opt.description}`)
.join('\n'),
inline: false,
});
} else {
embed.addFields({
name: 'Options',
value: options
.map(
(opt: any) =>
`\`${opt.name}\`: ${opt.description} ${opt.required ? '(Required)' : '(Optional)'}`,
)
.join('\n'),
inline: false,
});
}
}
embed.addFields({
name: '📚 Documentation',
value: `[Click here to access our full documentation](${getDocUrl(`cmd_field_${commandName}`)})`,
inline: false,
});
return interaction.editReply({ embeds: [embed] });
}
/**
* Get the category of a command based on its name
*/
function getCategoryFromCommand(commandName: string): string {
const commandCategories: Record<string, string> = {
achievement: 'fun',
fact: 'fun',
rank: 'fun',
counting: 'fun',
giveaway: 'fun',
leaderboard: 'fun',
ban: 'moderation',
kick: 'moderation',
mute: 'moderation',
unmute: 'moderation',
warn: 'moderation',
unban: 'moderation',
ping: 'util',
server: 'util',
userinfo: 'util',
members: 'util',
rules: 'util',
restart: 'util',
reconnect: 'util',
xp: 'util',
recalculatelevels: 'util',
help: 'util',
config: 'util',
testjoin: 'testing',
testleave: 'testing',
};
return commandCategories[commandName.toLowerCase()] || 'other';
}
export default command;

View file

@ -16,6 +16,10 @@ const command: Command = {
.setName('members')
.setDescription('Lists all non-bot members of the server'),
execute: async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply();
let members = await getAllMembers();
members = members.sort((a, b) =>
(a.discordUsername ?? '').localeCompare(b.discordUsername ?? ''),
@ -63,7 +67,7 @@ const command: Command = {
const components =
pages.length > 1 ? [getButtonActionRow(), getSelectMenuRow()] : [];
await interaction.reply({
await interaction.editReply({
embeds: [pages[currentPage]],
components,
});

View file

@ -8,7 +8,7 @@ const command: Command = {
.setDescription('Check the latency from you to the bot'),
execute: async (interaction) => {
await interaction.reply(
`Pong! Latency: ${Date.now() - interaction.createdTimestamp}ms`,
`🏓 Pong! Latency: ${Date.now() - interaction.createdTimestamp}ms`,
);
},
};

View file

@ -8,21 +8,22 @@ const command: Command = {
.setName('recalculatelevels')
.setDescription('(Admin Only) Recalculate all user levels'),
execute: async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
await interaction.editReply('Recalculating levels...');
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
await interaction.editReply({
content: 'You do not have permission to use this command.',
flags: ['Ephemeral'],
});
return;
}
await interaction.deferReply();
await interaction.editReply('Recalculating levels...');
try {
await recalculateUserLevels();
await interaction.editReply('Levels recalculated successfully!');

View file

@ -36,7 +36,9 @@ const command: SubcommandCommand = {
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
const config = loadConfig();
const managerRoleId = config.roles.staffRoles.find(
@ -52,18 +54,15 @@ const command: SubcommandCommand = {
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
await interaction.editReply({
content:
'You do not have permission to use this command. This command is restricted to users with the Manager role.',
flags: ['Ephemeral'],
});
return;
}
const subcommand = interaction.options.getSubcommand();
await interaction.deferReply({ flags: ['Ephemeral'] });
try {
if (subcommand === 'database') {
await handleDatabaseReconnect(interaction);

View file

@ -18,12 +18,16 @@ const command: Command = {
.setName('restart')
.setDescription('(Manager Only) Restart the bot'),
execute: async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply({ flags: ['Ephemeral'] });
const config = loadConfig();
const managerRoleId = config.roles.staffRoles.find(
(role) => role.name === 'Manager',
)?.roleId;
const member = await interaction.guild?.members.fetch(interaction.user.id);
const member = await interaction.guild.members.fetch(interaction.user.id);
const hasManagerRole = member?.roles.cache.has(managerRoleId || '');
if (
@ -32,17 +36,15 @@ const command: Command = {
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
await interaction.editReply({
content:
'You do not have permission to restart the bot. This command is restricted to users with the Manager role.',
flags: ['Ephemeral'],
});
return;
}
await interaction.reply({
await interaction.editReply({
content: 'Restarting the bot... This may take a few moments.',
flags: ['Ephemeral'],
});
const dbConnected = await ensureDatabaseConnection();

View file

@ -7,8 +7,10 @@ const command: Command = {
.setName('server')
.setDescription('Provides information about the server.'),
execute: async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.reply(
`The server **${interaction!.guild!.name}** has **${interaction!.guild!.memberCount}** members and was created on **${interaction!.guild!.createdAt}**. It is **${new Date().getFullYear() - interaction!.guild!.createdAt.getFullYear()!}** years old.`,
`The server **${interaction.guild.name}** has **${interaction.guild.memberCount}** members and was created on **${interaction.guild.createdAt}**. It is **${new Date().getFullYear() - interaction.guild.createdAt.getFullYear()}** years old.`,
);
},
};

View file

@ -19,21 +19,25 @@ const command: OptionsCommand = {
.setRequired(true),
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand() || !interaction.guild) return;
await interaction.deferReply();
const userOption = interaction.options.get(
'user',
) as unknown as GuildMember;
const user = userOption.user;
if (!userOption || !user) {
await interaction.reply('User not found');
await interaction.editReply('User not found');
return;
}
if (
!interaction.memberPermissions!.has(
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.ModerateMembers,
)
) {
await interaction.reply(
await interaction.editReply(
'You do not have permission to view member information.',
);
return;
@ -140,7 +144,7 @@ const command: OptionsCommand = {
iconURL: interaction.user.displayAvatarURL(),
});
await interaction.reply({ embeds: [embed] });
await interaction.editReply({ embeds: [embed] });
},
};

View file

@ -71,12 +71,16 @@ const command: SubcommandCommand = {
),
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (!interaction.isChatInputCommand() || !interaction.guild) return;
const commandUser = interaction.guild?.members.cache.get(
const commandUser = interaction.guild.members.cache.get(
interaction.user.id,
);
await interaction.deferReply({
flags: ['Ephemeral'],
});
const config = loadConfig();
const managerRoleId = config.roles.staffRoles.find(
(role) => role.name === 'Manager',
@ -87,18 +91,12 @@ const command: SubcommandCommand = {
!managerRoleId ||
commandUser.roles.highest.comparePositionTo(managerRoleId) < 0
) {
await interaction.reply({
await interaction.editReply({
content: 'You do not have permission to use this command',
flags: ['Ephemeral'],
});
return;
}
await interaction.deferReply({
flags: ['Ephemeral'],
});
await interaction.editReply('Processing...');
const subcommand = interaction.options.getSubcommand();
const user = interaction.options.getUser('user', true);
const amount = interaction.options.getInteger('amount', false);

View file

@ -133,6 +133,7 @@ export async function updateMember({
discordUsername,
currentlyInServer,
currentlyBanned,
currentlyMuted,
}: schema.memberTableTypes): Promise<void> {
try {
await ensureDbInitialized();
@ -147,6 +148,7 @@ export async function updateMember({
discordUsername,
currentlyInServer,
currentlyBanned,
currentlyMuted,
})
.where(eq(schema.memberTable.discordId, discordId));

View file

@ -12,9 +12,10 @@ async function startBot() {
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildModeration,
GatewayIntentBits.GuildInvites,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildModeration,
],
},
config,

View file

@ -6,7 +6,7 @@ import {
} from 'discord.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 { Event } from '@/types/EventTypes.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
) {
await executeUnmute(
newMember.client,
guild.id,
newMember.user.id,
undefined,
guild.members.me!,
true,
);
}
} catch (error) {
console.error('Error handling member update:', error);
}

View file

@ -11,6 +11,7 @@ import {
setDiscordClient as setRedisDiscordClient,
} from '@/db/redis.js';
import { setDiscordClient as setDbDiscordClient } from '@/db/db.js';
import { loadActiveBans, loadActiveMutes } from '@/util/helpers.js';
export default {
name: Events.ClientReady,
@ -36,6 +37,9 @@ export default {
const nonBotMembers = members.filter((m) => !m.user.bot);
await setMembers(nonBotMembers);
await loadActiveBans(client, guild);
await loadActiveMutes(client, guild);
await scheduleFactOfTheDay(client);
await scheduleGiveaways(client);

View file

@ -1,7 +1,7 @@
import { Client, ClientOptions, Collection } from 'discord.js';
import { Command } from '@/types/CommandTypes.js';
import { Config } from '@/types/ConfigTypes.js';
import { deployCommands } from '@/util/deployCommand.js';
import { deployCommands, getFilesRecursively } from '@/util/deployCommand.js';
import { registerEvents } from '@/util/eventLoader.js';
/**
@ -29,6 +29,19 @@ export class ExtendedClient extends Client {
private async loadModules() {
try {
if (process.env.SKIP_COMMAND_DEPLOY === 'true') {
console.log('Skipping command deployment (SKIP_COMMAND_DEPLOY=true)');
const commandFiles = await this.loadCommandsWithoutDeploying();
if (!commandFiles?.length) {
throw new Error('No commands found');
}
await registerEvents(this);
console.log(
`Loaded ${commandFiles.length} commands and registered events (without deployment)`,
);
} else {
const commands = await deployCommands();
if (!commands?.length) {
throw new Error('No commands found');
@ -40,9 +53,49 @@ export class ExtendedClient extends Client {
await registerEvents(this);
console.log(`Loaded ${commands.length} commands and registered events`);
}
} catch (error) {
console.error('Error loading modules:', error);
process.exit(1);
}
}
/**
* Loads commands without deploying them to Discord
* @returns Array of command objects
*/
private async loadCommandsWithoutDeploying(): Promise<Command[]> {
try {
const path = await import('path');
const __dirname = path.resolve();
const commandsPath = path.join(__dirname, 'target', 'commands');
const commandFiles = getFilesRecursively(commandsPath);
const commands: Command[] = [];
for (const file of commandFiles) {
const commandModule = await import(`file://${file}`);
const command = commandModule.default;
if (
command instanceof Object &&
'data' in command &&
'execute' in command
) {
commands.push(command);
this.commands.set(command.data.name, command);
} else {
console.warn(
`[WARNING] The command at ${file} is missing a required "data" or "execute" property.`,
);
}
}
return commands;
} catch (error) {
console.error('Error loading commands:', error);
throw error;
}
}
}

View file

@ -5,6 +5,7 @@ export interface Config {
token: string;
clientId: string;
guildId: string;
serverInvite: string;
database: {
dbConnectionString: string;
maxRetryAttempts: number;

View file

@ -17,7 +17,7 @@ const rest = new REST({ version: '10' }).setToken(token);
* @param directory - The directory to get files from
* @returns - An array of file paths
*/
const getFilesRecursively = (directory: string): string[] => {
export const getFilesRecursively = (directory: string): string[] => {
const files: string[] = [];
const filesInDirectory = fs.readdirSync(directory);

View file

@ -10,11 +10,12 @@ import {
ButtonStyle,
ButtonBuilder,
ActionRowBuilder,
DiscordAPIError,
} from 'discord.js';
import { and, eq } from 'drizzle-orm';
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';
const __dirname = path.resolve();
@ -116,6 +117,107 @@ export async function generateMemberBanner({
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
* @param client - The client to use
@ -174,7 +276,7 @@ export async function executeUnban(
guild,
action: 'unban',
target: guild.members.cache.get(userId)!,
moderator: guild.members.cache.get(client.user!.id)!,
moderator: guild.members.me!,
reason: reason ?? 'Temporary ban expired',
});
} catch (error) {

View file

@ -0,0 +1,36 @@
import { REST, Routes } from 'discord.js';
import { loadConfig } from './configLoader.js';
const config = loadConfig();
const { token, clientId, guildId } = config;
const rest = new REST({ version: '10' }).setToken(token);
/**
* Undeploys all commands from the Discord API
*/
export const undeployCommands = async () => {
try {
console.log('Undeploying all commands from the Discord API...');
await rest.put(Routes.applicationGuildCommands(clientId, guildId), {
body: [],
});
console.log('Successfully undeployed all commands');
} catch (error) {
console.error('Error undeploying commands:', error);
throw error;
}
};
if (import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'))) {
undeployCommands()
.then(() => {
console.log('Undeploy process completed successfully');
})
.catch((err) => {
console.error('Undeploy process failed:', err);
process.exitCode = 1;
});
}

View file

@ -1882,7 +1882,19 @@ __metadata:
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6":
"cross-env@npm:^7.0.3":
version: 7.0.3
resolution: "cross-env@npm:7.0.3"
dependencies:
cross-spawn: "npm:^7.0.1"
bin:
cross-env: src/bin/cross-env.js
cross-env-shell: src/bin/cross-env-shell.js
checksum: 10c0/f3765c25746c69fcca369655c442c6c886e54ccf3ab8c16847d5ad0e91e2f337d36eedc6599c1227904bf2a228d721e690324446876115bc8e7b32a866735ecf
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
dependencies:
@ -4201,6 +4213,7 @@ __metadata:
"@types/pg": "npm:^8.11.13"
"@typescript-eslint/eslint-plugin": "npm:^8.30.1"
"@typescript-eslint/parser": "npm:^8.30.1"
cross-env: "npm:^7.0.3"
discord.js: "npm:^14.18.0"
drizzle-kit: "npm:^0.31.0"
drizzle-orm: "npm:^0.42.0"