Added Warn and Ban Commands, Added Logging, and Much More

This commit is contained in:
Ahmad 2025-02-23 21:39:49 -05:00
parent d89de72e08
commit 86adac3f08
No known key found for this signature in database
GPG key ID: 8FD8A93530D182BF
33 changed files with 2200 additions and 204 deletions

View file

@ -0,0 +1,127 @@
import {
CommandInteraction,
PermissionsBitField,
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
} from 'discord.js';
import { updateMember, updateMemberModerationHistory } from '../../db/db.js';
import { parseDuration, scheduleUnban } from '../../util/helpers.js';
import logAction from '../../util/logging/logAction.js';
interface Command {
data: SlashCommandOptionsOnlyBuilder;
execute: (interaction: CommandInteraction) => Promise<void>;
}
const command: Command = {
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Ban a member from the server')
.addUserOption((option) =>
option
.setName('member')
.setDescription('The member to ban')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('The reason for the ban')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('duration')
.setDescription(
'The duration of the ban (ex. 5m, 1h, 1d, 1w). Leave blank for permanent ban.',
)
.setRequired(false),
),
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 banDuration = interaction.options.get('duration')?.value as
| string
| undefined;
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'],
});
return;
}
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}.`,
);
await member.ban({ reason });
if (banDuration) {
const durationMs = parseDuration(banDuration);
const expiresAt = new Date(Date.now() + durationMs);
await scheduleUnban(
interaction.client,
interaction.guild!.id,
member.id,
expiresAt,
);
}
await updateMemberModerationHistory({
discordId: member.id,
moderatorDiscordId: interaction.user.id,
action: 'ban',
reason,
duration: banDuration ?? 'indefinite',
createdAt: new Date(),
active: true,
});
await updateMember({
discordId: member.id,
currentlyBanned: true,
});
await logAction({
guild: interaction.guild!,
action: 'ban',
target: member,
moderator: moderator!,
reason,
});
await interaction.reply({
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({
content: 'Unable to ban member.',
flags: ['Ephemeral'],
});
}
},
};
export default command;

View file

@ -0,0 +1,82 @@
import {
CommandInteraction,
PermissionsBitField,
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
} from 'discord.js';
import { executeUnban } from '../../util/helpers.js';
interface Command {
data: SlashCommandOptionsOnlyBuilder;
execute: (interaction: CommandInteraction) => Promise<void>;
}
const command: Command = {
data: new SlashCommandBuilder()
.setName('unban')
.setDescription('Unban a user from the server')
.addStringOption((option) =>
option
.setName('userid')
.setDescription('The Discord ID of the user to unban')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('The reason for the unban')
.setRequired(true),
),
execute: async (interaction) => {
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)
) {
await interaction.reply({
content: 'You do not have permission to unban users.',
flags: ['Ephemeral'],
});
return;
}
try {
try {
const ban = await interaction.guild?.bans.fetch(userId);
if (!ban) {
await interaction.reply({
content: 'This user is not banned.',
flags: ['Ephemeral'],
});
return;
}
} catch {
await interaction.reply({
content: 'Error getting ban. Is this user banned?',
flags: ['Ephemeral'],
});
return;
}
await executeUnban(
interaction.client,
interaction.guildId!,
userId,
reason,
);
await interaction.reply({
content: `<@${userId}> has been unbanned. Reason: ${reason}`,
});
} catch (error) {
console.error(error);
await interaction.reply({
content: 'Unable to unban user.',
flags: ['Ephemeral'],
});
}
},
};
export default command;

View file

@ -0,0 +1,85 @@
import {
CommandInteraction,
PermissionsBitField,
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
} from 'discord.js';
import { updateMemberModerationHistory } from '../../db/db.js';
import logAction from '../../util/logging/logAction.js';
interface Command {
data: SlashCommandOptionsOnlyBuilder;
execute: (interaction: CommandInteraction) => Promise<void>;
}
const command: Command = {
data: new SlashCommandBuilder()
.setName('warn')
.setDescription('Warn a member')
.addUserOption((option) =>
option
.setName('member')
.setDescription('The member to warn')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('The reason for the warning')
.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 unknown as string,
);
const reason = interaction.options.get('reason')
?.value as unknown as string;
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'],
});
return;
}
try {
await updateMemberModerationHistory({
discordId: member!.user.id,
moderatorDiscordId: interaction.user.id,
action: 'warning',
reason: reason,
duration: '',
});
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',
target: member!,
moderator: moderator!,
reason: reason,
});
} catch (error) {
console.error(error);
await interaction.reply({
content: 'There was an error trying to warn the member.',
flags: ['Ephemeral'],
});
}
},
};
export default command;

View file

@ -0,0 +1,42 @@
import {
CommandInteraction,
PermissionsBitField,
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
} from 'discord.js';
interface Command {
data: SlashCommandOptionsOnlyBuilder;
execute: (interaction: CommandInteraction) => Promise<void>;
}
const command: Command = {
data: new SlashCommandBuilder()
.setName('testjoin')
.setDescription('Simulates a new member joining'),
execute: async (interaction) => {
const guild = interaction.guild;
if (
!interaction.memberPermissions!.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
content: 'You do not have permission to use this command.',
flags: ['Ephemeral'],
});
}
const fakeMember = await guild!.members.fetch(interaction.user.id);
guild!.client.emit('guildMemberAdd', fakeMember);
await interaction.reply({
content: 'Triggered the join event!',
flags: ['Ephemeral'],
});
},
};
export default command;

View file

@ -0,0 +1,48 @@
import {
CommandInteraction,
PermissionsBitField,
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
} from 'discord.js';
import { updateMember } from '../../db/db.js';
interface Command {
data: SlashCommandOptionsOnlyBuilder;
execute: (interaction: CommandInteraction) => Promise<void>;
}
const command: Command = {
data: new SlashCommandBuilder()
.setName('testleave')
.setDescription('Simulates a member leaving'),
execute: async (interaction) => {
const guild = interaction.guild;
if (
!interaction.memberPermissions!.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
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);
await interaction.reply({
content: 'Triggered the leave event!',
flags: ['Ephemeral'],
});
await updateMember({
discordId: interaction.user.id,
currentlyInServer: true,
});
},
};
export default command;

View file

@ -2,8 +2,14 @@ import {
SlashCommandBuilder,
CommandInteraction,
EmbedBuilder,
ButtonBuilder,
ActionRowBuilder,
ButtonStyle,
StringSelectMenuBuilder,
APIEmbed,
JSONEncodable,
} from 'discord.js';
import { getAllMembers } from '../../util/db.js';
import { getAllMembers } from '../../db/db.js';
interface Command {
data: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
@ -15,16 +21,112 @@ const command: Command = {
.setName('members')
.setDescription('Lists all non-bot members of the server'),
execute: async (interaction) => {
const members = await getAllMembers();
const memberList = members
.map((m) => `**${m.discordUsername}** (${m.discordId})`)
.join('\n');
const membersEmbed = new EmbedBuilder()
.setTitle('Members')
.setDescription(memberList)
.setColor(0x0099ff)
.addFields({ name: 'Total Members', value: members.length.toString() });
await interaction.reply({ embeds: [membersEmbed] });
let members = await getAllMembers();
members = members.sort((a, b) =>
a.discordUsername.localeCompare(b.discordUsername),
);
const ITEMS_PER_PAGE = 15;
const pages: (APIEmbed | JSONEncodable<APIEmbed>)[] = [];
for (let i = 0; i < members.length; i += ITEMS_PER_PAGE) {
const pageMembers = members.slice(i, i + ITEMS_PER_PAGE);
const memberList = pageMembers
.map((m) => `**${m.discordUsername}** (${m.discordId})`)
.join('\n');
const embed = new EmbedBuilder()
.setTitle('Members')
.setDescription(memberList || 'No members to display.')
.setColor(0x0099ff)
.addFields({ name: 'Total Members', value: members.length.toString() })
.setFooter({
text: `Page ${Math.floor(i / ITEMS_PER_PAGE) + 1} of ${Math.ceil(members.length / ITEMS_PER_PAGE)}`,
});
pages.push(embed);
}
let currentPage = 0;
const getButtonActionRow = () =>
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('previous')
.setLabel('Previous')
.setStyle(ButtonStyle.Primary)
.setDisabled(currentPage === 0),
new ButtonBuilder()
.setCustomId('next')
.setLabel('Next')
.setStyle(ButtonStyle.Primary)
.setDisabled(currentPage === pages.length - 1),
);
const getSelectMenuRow = () => {
const options = pages.map((_, index) => ({
label: `Page ${index + 1}`,
value: index.toString(),
default: index === currentPage,
}));
const select = new StringSelectMenuBuilder()
.setCustomId('select_page')
.setPlaceholder('Jump to a page')
.addOptions(options);
return new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
select,
);
};
const components =
pages.length > 1 ? [getButtonActionRow(), getSelectMenuRow()] : [];
await interaction.reply({
embeds: [pages[currentPage]],
components,
});
const message = await interaction.fetchReply();
if (pages.length <= 1) return;
const collector = message.createMessageComponentCollector({
time: 60000,
});
collector.on('collect', async (i) => {
if (i.user.id !== interaction.user.id) {
await i.reply({
content: 'These controls are not for you!',
flags: ['Ephemeral'],
});
return;
}
if (i.isButton()) {
if (i.customId === 'previous' && currentPage > 0) {
currentPage--;
} else if (i.customId === 'next' && currentPage < pages.length - 1) {
currentPage++;
}
}
if (i.isStringSelectMenu()) {
const selected = parseInt(i.values[0]);
if (!isNaN(selected) && selected >= 0 && selected < pages.length) {
currentPage = selected;
}
}
await i.update({
embeds: [pages[currentPage]],
components: [getButtonActionRow(), getSelectMenuRow()],
});
});
collector.on('end', async () => {
if (message.editable) {
await message.edit({ components: [] });
}
});
},
};

View file

@ -34,7 +34,7 @@ const rulesEmbed = new EmbedBuilder()
{
name: '**Rule #3: Use Common Sense**',
value:
'Think before you act or post. If something seems questionable, its probably best not to do it.',
'Think before you act or post. If something seems questionable, it is probably best not to do it.',
},
{
name: '**Rule #4: No Spamming**',
@ -69,7 +69,7 @@ const rulesEmbed = new EmbedBuilder()
{
name: '**Rule #10: No Ping Abuse**',
value:
'Do not ping staff members unless it\'s absolutely necessary. Use pings responsibly for all members.',
'Do not ping staff members unless it is absolutely necessary. Use pings responsibly for all members.',
},
{
name: '**Rule #11: Use Appropriate Channels**',

View file

@ -11,7 +11,7 @@ const command: Command = {
.setDescription('Provides information about the server.'),
execute: async (interaction) => {
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

@ -3,8 +3,10 @@ import {
CommandInteraction,
EmbedBuilder,
SlashCommandOptionsOnlyBuilder,
GuildMember,
PermissionsBitField,
} from 'discord.js';
import { getMember } from '../../util/db.js';
import { getMember } from '../../db/db.js';
interface Command {
data: SlashCommandOptionsOnlyBuilder;
@ -14,7 +16,7 @@ interface Command {
const command: Command = {
data: new SlashCommandBuilder()
.setName('userinfo')
.setDescription('Provides information about the user.')
.setDescription('Provides information about the specified user.')
.addUserOption((option) =>
option
.setName('user')
@ -22,46 +24,127 @@ const command: Command = {
.setRequired(true),
),
execute: async (interaction) => {
const userOption = interaction.options.get('user');
if (!userOption) {
await interaction.reply('User not found');
return;
}
const userOption = interaction.options.get(
'user',
) as unknown as GuildMember;
const user = userOption.user;
if (!user) {
if (!userOption || !user) {
await interaction.reply('User not found');
return;
}
const member = await getMember(user.id);
const [memberData] = member;
if (
!interaction.memberPermissions!.has(
PermissionsBitField.Flags.ModerateMembers,
)
) {
await interaction.reply(
'You do not have permission to view member information.',
);
return;
}
const memberData = await getMember(user.id);
const numberOfWarnings = memberData?.moderations.filter(
(moderation) => moderation.action === 'warning',
).length;
const recentWarnings = memberData?.moderations
.filter((moderation) => moderation.action === 'warning')
.sort((a, b) => b.createdAt!.getTime() - a.createdAt!.getTime())
.slice(0, 5);
const numberOfMutes = memberData?.moderations.filter(
(moderation) => moderation.action === 'mute',
).length;
const currentMute = memberData?.moderations
.filter((moderation) => moderation.action === 'mute')
.sort((a, b) => b.createdAt!.getTime() - a.createdAt!.getTime())[0];
const numberOfBans = memberData?.moderations.filter(
(moderation) => moderation.action === 'ban',
).length;
const currentBan = memberData?.moderations
.filter((moderation) => moderation.action === 'ban')
.sort((a, b) => b.createdAt!.getTime() - a.createdAt!.getTime())[0];
const embed = new EmbedBuilder()
.setTitle(`User Information - ${user?.username}`)
.setColor(user.accentColor || 'Default')
.setColor(user.accentColor || '#5865F2')
.setThumbnail(user.displayAvatarURL({ size: 256 }))
.setTimestamp()
.addFields(
{ name: 'Username', value: user.username, inline: false },
{ name: 'User ID', value: user.id, inline: false },
{
name: 'Joined Server',
value:
interaction.guild?.members.cache
.get(user.id)
?.joinedAt?.toLocaleString() || 'Not available',
name: '👤 Basic Information',
value: [
`**Username:** ${user.username}`,
`**Discord ID:** ${user.id}`,
`**Account Created:** ${user.createdAt.toLocaleString()}`,
`**Joined Server:** ${
interaction.guild?.members.cache
.get(user.id)
?.joinedAt?.toLocaleString() || 'Not available'
}`,
`**Currently in Server:** ${memberData?.currentlyInServer ? '✅ Yes' : '❌ No'}`,
].join('\n'),
inline: false,
},
{
name: 'Account Created',
value: user.createdAt.toLocaleString(),
name: '🛡️ Moderation History',
value: [
`**Total Warnings:** ${numberOfWarnings || '0'} ${numberOfWarnings ? '⚠️' : ''}`,
`**Total Mutes:** ${numberOfMutes || '0'} ${numberOfMutes ? '🔇' : ''}`,
`**Total Bans:** ${numberOfBans || '0'} ${numberOfBans ? '🔨' : ''}`,
`**Currently Muted:** ${memberData?.currentlyMuted ? '🔇 Yes' : '✅ No'}`,
`**Currently Banned:** ${memberData?.currentlyBanned ? '🚫 Yes' : '✅ No'}`,
].join('\n'),
inline: false,
},
{
name: 'Number of Warnings',
value: memberData?.numberOfWarnings.toString() || '0',
},
{
name: 'Number of Bans',
value: memberData?.numberOfBans.toString() || '0',
},
);
if (recentWarnings && recentWarnings.length > 0) {
embed.addFields({
name: '⚠️ Recent Warnings',
value: recentWarnings
.map(
(warning, index) =>
`${index + 1}. \`${warning.createdAt?.toLocaleDateString() || 'Unknown'}\` - ` +
`By <@${warning.moderatorDiscordId}>\n` +
`└ Reason: ${warning.reason || 'No reason provided'}`,
)
.join('\n\n'),
inline: false,
});
}
if (memberData?.currentlyMuted && currentMute) {
embed.addFields({
name: '🔇 Current Mute Details',
value: [
`**Reason:** ${currentMute.reason || 'No reason provided'}`,
`**Duration:** ${currentMute.duration || 'Indefinite'}`,
`**Muted At:** ${currentMute.createdAt?.toLocaleString() || 'Unknown'}`,
`**Muted By:** <@${currentMute.moderatorDiscordId}>`,
].join('\n'),
inline: false,
});
}
if (memberData?.currentlyBanned && currentBan) {
embed.addFields({
name: '📌 Current Ban Details',
value: [
`**Reason:** ${currentBan.reason || 'No reason provided'}`,
`**Duration:** ${currentBan.duration || 'Permanent'}`,
`**Banned At:** ${currentBan.createdAt?.toLocaleString() || 'Unknown'}`,
].join('\n'),
inline: false,
});
}
embed.setFooter({
text: `Requested by ${interaction.user.username}`,
iconURL: interaction.user.displayAvatarURL(),
});
await interaction.reply({ embeds: [embed] });
},
};