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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

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

Quest is capable of:
- All moderation commands such as: /ban, /kick, /mute & /warn.
- Autoroles
- Utilties such as reminders.
- Complete ticket system.
- Welcoming new members.

## Links

[Status Page](https://status.questfoundation.dev), [Bot Documentation](https://docs.questfoundation.dev/quest-bot) & [Official Discord](https://discord.gg/ksuqZ77R88)
Expand Down
15 changes: 15 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ model Server {
bans Ban[]
reminders Reminder[]
tickets Ticket[]
autoroles AutoRole[]

@@map("server")
}
Expand Down Expand Up @@ -98,3 +99,17 @@ model Ticket {
@@index([guildId, userId])
@@map("tickets")
}

model AutoRole {
id String @id @default(cuid())
guildId String
roleId String
botRole Boolean?
createdAt DateTime @default(now())

server Server @relation(fields: [guildId], references: [id], onDelete: Cascade)

@@unique([guildId, roleId])
@@index([guildId])
@@map("autoroles")
}
150 changes: 150 additions & 0 deletions src/commands/moderation/autorole/autorole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Command } from '@sapphire/framework';
import { MessageFlags } from 'discord.js';
import { createAutoRole, getAutoRole, getAutoRoles, removeAutoRole } from '#lib/autorole.js';
import { LimitError } from '#lib/limits.js';
import { emojis } from '#utils/emoji.js';

export class AutoRoleCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, { ...options });
}

public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName('autorole')
.setDescription('Automatically assign roles to new members!')
.addSubcommand((sub) =>
sub
.setName('add')
.setDescription('Create a new auto role.')
.addRoleOption((option) =>
option.setName('role').setDescription('The role to assign to new members').setRequired(true)
)
.addBooleanOption((option) =>
option.setName('bot_role').setDescription('Whether this role should be assigned to bots').setRequired(true)
)
)
.addSubcommand((sub) =>
sub
.setName('remove')
.setDescription('Remove an auto role.')
.addStringOption((option) =>
option
.setName('role')
.setDescription('The auto role to remove')
.setAutocomplete(true)
.setRequired(true)
)
)
.addSubcommand((sub) =>
sub
.setName('list')
.setDescription('List all auto roles.')
)
);
}

public override async autocompleteRun(interaction: Command.AutocompleteInteraction) {
if (!interaction.guildId) {
await interaction.respond([]);
return;
}

const focusedOption = interaction.options.getFocused(true);

if (interaction.options.getSubcommand() !== 'remove' || focusedOption.name !== 'role') {
await interaction.respond([]);
return;
}

const autoRoles = await getAutoRoles(interaction.guildId);
const choices = autoRoles.slice(0, 25).map((autoRole) => ({
name: interaction.guild?.roles.cache.get(autoRole.roleId)?.name ?? autoRole.roleId,
value: autoRole.id
}));

await interaction.respond(choices);
}

public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
if (!interaction.inCachedGuild()) {
await interaction.reply({
content: `${emojis.rightArrow2} This command can only be used in a server.`,
flags: MessageFlags.Ephemeral
});
return;
}

const subcommand = interaction.options.getSubcommand();

if (subcommand === 'add') {
const role = interaction.options.getRole('role', true);
const botRole = interaction.options.getBoolean('bot_role', true);

try {
await createAutoRole(interaction.guildId, interaction.guild.name, role.id, botRole);
await interaction.reply({
content: `${emojis.rightArrow2} Added auto role ${role} (Bot Role: ${botRole}).`,
flags: MessageFlags.Ephemeral
});
} catch (err) {
console.error(err);
if (err instanceof LimitError) {
await interaction.reply({
content: `${emojis.rightArrow2} ${err.message}`,
flags: MessageFlags.Ephemeral
});
return;
}

await interaction.reply({
content: `${emojis.rightArrow2} That role is already an auto role in this server.`,
flags: MessageFlags.Ephemeral
});
}
}

if (subcommand === 'remove') {
const autoRoleId = interaction.options.getString('role', true);
const autoRole = await getAutoRole(autoRoleId);

if (!autoRole) {
await interaction.reply({
content: `${emojis.rightArrow2} That auto role no longer exists.`,
flags: MessageFlags.Ephemeral
});
return;
}

await removeAutoRole(autoRole.id);
await interaction.reply({
content: `${emojis.rightArrow2} Removed auto role for <@&${autoRole.roleId}>.`,
flags: MessageFlags.Ephemeral
});
}

if (subcommand === 'list') {
const autoRoles = await getAutoRoles(interaction.guildId);
if (autoRoles.length === 0) {
await interaction.reply({
content: `${emojis.rightArrow2} There are no auto roles set up in this server.`,
flags: MessageFlags.Ephemeral
});
return;
}

const autoRoleList = autoRoles.map(autoRole => {
const role = interaction.guild?.roles.cache.get(autoRole.roleId);
const roleName = role ? `<@&${role.id}>` : `Unknown Role (${autoRole.roleId})`;
const botRoleText = autoRole.botRole ? ' (Bot Role)' : '';
return `${emojis.rightArrow1} ${roleName}${botRoleText}`;
}).join('\n');

await interaction.reply({
content: `**Auto Roles:**\n${autoRoleList}`,
flags: MessageFlags.Ephemeral
});
}
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
34 changes: 34 additions & 0 deletions src/commands/utility/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Command } from '@sapphire/framework';
import { emojis } from '#utils/emoji.js';
import { MessageFlags } from 'discord.js';

export class HelpCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, { ...options });
}

public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder.setName('help').setDescription("Show what the bot is capable of.")
);
}

public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const commands = this.container.stores.get('commands');

const commandList = Array.from(commands.values())
.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
.map((cmd) => {
const description = cmd.applicationCommandRegistry['apiCalls'][0]?.builtData.description ?? cmd.description;
const commandName = cmd.applicationCommandRegistry['apiCalls'][0]?.builtData.name ?? cmd.name;

return `${emojis.rightArrow1} **/${commandName}** - ${description}`;
})
.join('\n');

await interaction.reply({
content: commandList + `\n\n**Status:** https://status.questfoundation.dev/\n**Official Discord Server:** https://discord.gg/F4HYE8frK2\n**Documentation:** https://docs.questfoundation.dev/`,
flags: MessageFlags.SuppressEmbeds | MessageFlags.Ephemeral
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Command } from '@sapphire/framework';
import { MessageFlags } from 'discord.js';
import ms, { type StringValue } from 'ms';
import { createReminder, getReminder, removeReminder } from '#lib/reminders.js';
import { LimitError } from '#lib/limits.js';
import { emojis } from '#utils/emoji.js';

export class ReminderCommand extends Command {
Expand Down Expand Up @@ -62,26 +63,39 @@ export class ReminderCommand extends Command {

const remindAt = new Date(Date.now() + duration);

const reminder = interaction.inCachedGuild()
? await createReminder(
interaction.user.id,
message,
remindAt,
interaction.guild.id,
interaction.guild.name,
interaction.channelId
)
: await createReminder(interaction.user.id, message, remindAt);

const unix = Math.floor(remindAt.getTime() / 1000);
await interaction.reply({
content: `${emojis.rightArrow2} Reminder set to go off in <t:${unix}:R> message: ${message}\nID: \`${reminder.id}\``
});
try {
const reminder = interaction.inCachedGuild()
? await createReminder(
interaction.user.id,
message,
remindAt,
interaction.guild.id,
interaction.guild.name,
interaction.channelId
)
: await createReminder(interaction.user.id, message, remindAt);

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 5000);
return;
const unix = Math.floor(remindAt.getTime() / 1000);
await interaction.reply({
content: `${emojis.rightArrow2} Reminder set to go off in <t:${unix}:R> message: ${message}\nID: \`${reminder.id}\``
});

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 5000);
return;
} catch (err) {
console.error(err);
if (err instanceof LimitError) {
await interaction.reply({
content: `${emojis.rightArrow2} ${err.message}`,
flags: MessageFlags.Ephemeral
});
return;
}

throw err;
}
}

if (subcommand === 'remove') {
Expand Down
33 changes: 29 additions & 4 deletions src/commands/utility/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
InteractionContextType,
MessageFlags,
PermissionFlagsBits,
RoleSelectMenuBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder
} from 'discord.js';
Expand Down Expand Up @@ -42,7 +43,7 @@ function buildWelcomePanel(settings: ServerSettings, guild: Guild, status?: stri
.setValue('enable'),
new StringSelectMenuOptionBuilder()
.setLabel('Disable')
.setDescription('Do not send a message when a user joins the server.')
.setDescription("Don't send a message when a user joins the server.")
.setValue('disable')
);

Expand Down Expand Up @@ -79,14 +80,30 @@ function buildTicketPanel(settings: ServerSettings, guild: Guild, status?: strin
.setLabel('Remove Category')
.setStyle(ButtonStyle.Danger)
.setDisabled(!settings.ticketCategoryId);

const currentStaffRole = settings.staffRole
? guild.roles.cache.get(settings.staffRole)?.name
: null;

const staffRole = new RoleSelectMenuBuilder()
.setCustomId('staffRole')
.setPlaceholder(currentStaffRole ?? 'Select a ticket staff role');

const removeStaffRoleButton = new ButtonBuilder()
.setCustomId('removeStaffRole')
.setLabel('Remove Staff Role')
.setStyle(ButtonStyle.Danger)
.setDisabled(!settings.staffRole);

return {
content: status
? `${emojis.rightArrow1} **Tickets** module:\n${emojis.rightArrow2} ${status}`
: `${emojis.rightArrow1} **Tickets** module:`,
components: [
new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(categoryMenu),
new ActionRowBuilder<ButtonBuilder>().addComponents(removeButton)
new ActionRowBuilder<ButtonBuilder>().addComponents(removeButton),
new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(staffRole),
new ActionRowBuilder<ButtonBuilder>().addComponents(removeStaffRoleButton)
]
};
}
Expand Down Expand Up @@ -206,13 +223,21 @@ export class SettingsCommand extends Command {
} else if (i.customId === 'ticketCategory' && i.isChannelSelectMenu()) {
const categoryId = i.values[0];
const next = await updateSettings(guildId, guild.name, { ticketCategoryId: categoryId });
const categoryName = guild.channels.cache.get(categoryId)?.name ?? 'selected category';

await i.update(buildTicketPanel(next, guild, `Ticket category set to **${categoryName}**.`));
await i.update(buildTicketPanel(next, guild, `Ticket category set to <#${categoryId}>.`));
} else if (i.customId === 'ticketCategoryRemove' && i.isButton()) {
const next = await updateSettings(guildId, guild.name, { ticketCategoryId: null });

await i.update(buildTicketPanel(next, guild, 'Ticket category removed.'));
} else if (i.customId === 'staffRole' && i.isRoleSelectMenu()) {
const roleId = i.values[0];
const next = await updateSettings(guildId, guild.name, { staffRole: roleId });

await i.update(buildTicketPanel(next, guild, `Ticket staff role set to <@&${roleId}>.`));
} else if (i.customId === 'removeStaffRole' && i.isButton()) {
const next = await updateSettings(guildId, guild.name, { staffRole: null });

await i.update(buildTicketPanel(next, guild, 'Ticket staff role removed.'));
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import {
PermissionFlagsBits
} from 'discord.js';

export class SetupTicketCommand extends Command {
export class SetupTicketsCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, { ...options });
}

public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName('setup-ticket')
.setName('setup-tickets')
.setDescription('Post the ticket panel in a channel.')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addChannelOption((option) =>
Expand Down
Loading