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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
DISCORD_TOKEN=your bot's token
APPLICATION_ID=your bot's application id
DATABASE_URL="postgresql://databaseurl"
DEV=true/false
DEVIDS=list of userids that can run commands when in dev mode
DEV=true/false (this env var is optional)
DEVIDS=list of userids that can run commands when in dev mode (this env var is optional)
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@

An opensource modern Discord Bot built for moderation, utilities and support!

## Discord Server
## Links

Quest's Official Discord Server can be joined using the following link:

https://discord.gg/ksuqZ77R88

Thanks!
[Status Page](https://status.questfoundation.dev), [Bot Documentation](https://docs.questfoundation.dev/quest-bot) & [Official Discord](https://discord.gg/ksuqZ77R88)

## Running locally

1. Clone the repository.
2. Create a PostgreSQL database for this bot.
3. Create a `.env` file based on `.env.example` and fill in the required values.
4. Install dependencies with `pnpm install`
5. Deploy the commands with `pnpm run deploy`
6. Run the development server with `pnpm dev`
7. The bot is now live on Discord with registered commands!
5. Generate prisma using `pnpm prisma generate`
6. Update your local databse using `pnpm prisma db push`
7. Deploy the commands with `pnpm run deploy`
8. Run the development server with `pnpm dev`
9. The bot is now live on Discord with registered commands!

## AI declaration

Expand Down
218 changes: 126 additions & 92 deletions src/commands/moderation/ban.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,145 @@
import type { Command } from '../index.js';
import { ButtonBuilder, ButtonStyle, ActionRowBuilder, ApplicationCommandOptionType, PermissionsBitField, GuildMember, MessageFlags } from 'discord.js';
import {
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
ApplicationCommandOptionType,
PermissionsBitField,
GuildMember,
MessageFlags,
} from 'discord.js';
import { emojis } from '#utils/emoji.js';
import ms, { type StringValue } from 'ms'
import ms, { type StringValue } from 'ms';
import { createBan, applyBan } from '#lib/bans.js';

export default {
data: {
name: 'ban',
description: 'Ban someone from the discord server.',
options: [
{
type: ApplicationCommandOptionType.User,
name: "target",
description: "Select a user to ban",
required: true
},
{
type: ApplicationCommandOptionType.String,
name: "reason",
description: "Provide a reason for their ban"
},
{
type: ApplicationCommandOptionType.String,
name: "duration",
description: "Provide a duration for their ban (if needed)"
}
]
options: [
{
type: ApplicationCommandOptionType.User,
name: 'member',
description: 'Select a member to ban',
required: true,
},
{
type: ApplicationCommandOptionType.String,
name: 'reason',
description: 'Provide a reason for their ban',
},
{
type: ApplicationCommandOptionType.String,
name: 'duration',
description: 'Provide a duration for their ban (if needed)',
},
],
},
async execute(interaction) {
if (!interaction.inCachedGuild()) {
await interaction.reply({ content: `${emojis.rightArrow2} This command can only be used in a server.`, flags: MessageFlags.Ephemeral });
return;
}

const member = interaction.member as GuildMember;

if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) {
await interaction.reply({ content: `${emojis.rightArrow2} You do not have permission to ban members.`, flags: MessageFlags.Ephemeral });
return
}
if (!interaction.inCachedGuild()) {
await interaction.reply({
content: `${emojis.rightArrow2} This command can only be used in a server.`,
flags: MessageFlags.Ephemeral,
});
return;
}

const targetMember = interaction.options.getMember('target') as GuildMember
const member = interaction.member as GuildMember;

if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) {
await interaction.reply({
content: `${emojis.rightArrow2} You do not have permission to ban members.`,
flags: MessageFlags.Ephemeral,
});
return;
}

const targetMember = interaction.options.getMember('member') as GuildMember;
const reason = interaction.options.getString('reason') ?? 'No reason provided';

const durationStr = interaction.options.getString('duration') as StringValue
const duration = durationStr ? ms(durationStr as StringValue) : null;
const expiresAt = duration ? new Date(Date.now() + duration) : null;

if (!targetMember) {
await interaction.reply({ content: `${emojis.rightArrow2} That user is not in this server.`, flags: MessageFlags.Ephemeral });
return;
}

if (targetMember.id === interaction.user.id) {
await interaction.reply({ content: `${emojis.rightArrow2} You cannot ban yourself.`, flags: MessageFlags.Ephemeral });
return;
}

if (targetMember.id === interaction.guild.ownerId) {
await interaction.reply({ content: `${emojis.rightArrow2} You cannot ban the server owner.`, flags: MessageFlags.Ephemeral });
return;
}

if (!targetMember.bannable){
await interaction.reply({ content: `${emojis.rightArrow2} I cannot ban this user.`, flags: MessageFlags.Ephemeral });
return;
}
const durationStr = interaction.options.getString('duration') as StringValue;
const duration = durationStr ? ms(durationStr as StringValue) : null;
const expiresAt = duration ? new Date(Date.now() + duration) : null;

if (!targetMember) {
await interaction.reply({
content: `${emojis.rightArrow2} That user is not in this server.`,
flags: MessageFlags.Ephemeral,
});
return;
}

if (targetMember.id === interaction.user.id) {
await interaction.reply({
content: `${emojis.rightArrow2} You cannot ban yourself.`,
flags: MessageFlags.Ephemeral,
});
return;
}

if (targetMember.id === interaction.guild.ownerId) {
await interaction.reply({
content: `${emojis.rightArrow2} You cannot ban the server owner.`,
flags: MessageFlags.Ephemeral,
});
return;
}

const confirm = new ButtonBuilder()
.setCustomId('confirm')
.setLabel('Confirm Ban')
.setStyle(ButtonStyle.Danger)

const cancel = new ButtonBuilder()
.setCustomId('cancel')
.setLabel('Cancel')
.setStyle(ButtonStyle.Secondary)

const row = new ActionRowBuilder<ButtonBuilder>().addComponents(cancel, confirm);
const response = await interaction.reply({
if (!targetMember.bannable) {
await interaction.reply({
content: `${emojis.rightArrow2} I cannot ban this user.`,
flags: MessageFlags.Ephemeral,
});
return;
}

const confirm = new ButtonBuilder().setCustomId('confirm').setLabel('Confirm Ban').setStyle(ButtonStyle.Danger);

const cancel = new ButtonBuilder().setCustomId('cancel').setLabel('Cancel').setStyle(ButtonStyle.Secondary);

const row = new ActionRowBuilder<ButtonBuilder>().addComponents(cancel, confirm);
const response = await interaction.reply({
content: `${emojis.rightArrow1} Are you sure you want to ban <@${targetMember.id}> for reason: ${reason}?`,
components: [row],
withResponse: true,
withResponse: true,
});

const collectorFilter = (i: { user: { id: string; }; }) => i.user.id === interaction.user.id;
try {
const confirmation = await response.resource!.message!.awaitMessageComponent({ filter: collectorFilter, time: 60_000 });

if (confirmation.customId === 'confirm') {
try {
await createBan(interaction.guild.id, interaction.guild.name, targetMember.id, expiresAt, reason);
await applyBan(interaction.guild, targetMember.id, reason);
await targetMember.send(
`You have been banned from **${interaction.guild.name}**.\nReason: ${reason}${expiresAt ? `\nExpires: <t:${Math.floor(expiresAt.getTime() / 1000)}:R>` : ''}`
).catch(() => {});
await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been banned with reason: ${reason}`, components: [] });
} catch (err) {
console.error(err)
await confirmation.update({ content: `${emojis.rightArrow2} Failed to ban <@${targetMember.user.id}> with reason: ${reason}`, components: [] });
}
} else if (confirmation.customId === 'cancel') {
await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] });
}
} catch (err) {
console.error(err)
await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] });
}

const collectorFilter = (i: { user: { id: string } }) => i.user.id === interaction.user.id;
try {
const confirmation = await response.resource!.message!.awaitMessageComponent({
filter: collectorFilter,
time: 60_000,
});

if (confirmation.customId === 'confirm') {
try {
await createBan(interaction.guild.id, interaction.guild.name, targetMember.id, expiresAt, reason);
await applyBan(interaction.guild, targetMember.id, reason);
await targetMember
.send(
`You have been banned from **${interaction.guild.name}**.\nReason: ${reason}${expiresAt ? `\nExpires: <t:${Math.floor(expiresAt.getTime() / 1000)}:R>` : ''}`,
)
.catch(() => {});
await confirmation.update({
content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been banned with reason: ${reason}`,
components: [],
});
} catch (err) {
console.error(err);
await confirmation.update({
content: `${emojis.rightArrow2} Failed to ban <@${targetMember.user.id}> with reason: ${reason}`,
components: [],
});
}
} else if (confirmation.customId === 'cancel') {
await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] });
}
} catch (err) {
console.error(err);
await interaction.editReply({
content: `${emojis.rightArrow2} No response within a minute or errored.`,
components: [],
});
}
},
} satisfies Command;
Loading