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: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
node_modules
dist
README.md
.git
.turbo
.env
.env.*
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# These are supported funding model platforms

github: duck-organization
github: vantern-org
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
Expand Down
Binary file removed .github/assets/LargeDuckBanner.png
Binary file not shown.
Binary file added .github/assets/LargeVanternBanner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ jobs:
- name: Build
run: pnpm turbo build
- name: Build production image
run: docker build -t quest-bot:test .
run: docker build -t questbot:test .
- name: Test production image
run: >
docker run --rm --entrypoint sh quest-bot:test -c
docker run --rm --entrypoint sh questbot:test -c
"set -e;
test -f /app/apps/bot/dist/index.js;
node --check /app/apps/bot/dist/index.js"
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ COPY apps/bot/package.json ./apps/bot/package.json
COPY packages/database/package.json ./packages/database/package.json
COPY packages/database/prisma.config.ts ./packages/database/prisma.config.ts
COPY packages/database/prisma ./packages/database/prisma/
RUN pnpm install --frozen-lockfile --filter @duckorganization/questbot --filter @questbot/database
RUN pnpm install --frozen-lockfile --filter @questbot/bot --filter @questbot/database

COPY . .
RUN pnpm turbo run build --filter=@duckorganization/questbot...
RUN pnpm turbo run build --filter=@questbot/bot...

FROM node:24-alpine
WORKDIR /app
Expand All @@ -25,10 +25,10 @@ COPY apps/bot/package.json ./apps/bot/package.json
COPY packages/database/package.json ./packages/database/package.json
COPY packages/database/prisma.config.ts ./packages/database/prisma.config.ts
COPY packages/database/prisma ./packages/database/prisma/
RUN pnpm install --prod --frozen-lockfile --filter @duckorganization/questbot --filter @questbot/database
RUN pnpm install --prod --frozen-lockfile --filter @questbot/bot --filter @questbot/database

COPY --from=builder /app/apps/bot/dist ./apps/bot/dist
COPY --from=builder /app/packages/database/dist ./packages/database/dist
COPY --from=builder /app/packages/database/src/generated ./packages/database/src/generated

CMD ["sh", "-c", "pnpm --filter @questbot/database db:push && pnpm --filter @duckorganization/questbot start"]
CMD ["sh", "-c", "pnpm --filter @questbot/database db:push && pnpm --filter @questbot/bot start"]
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div style="text-align: center;">

<img src=".github/assets/LargeDuckBanner.png" alt="Duck Organization Banner" />
<img src=".github/assets/LargeVanternBanner.png" alt="Vantern Banner" />

---

<img src="https://img.shields.io/badge/Created_By_Duck_Organization-white?style=for-the-badge" alt="Created By Badge" />
<img src="https://img.shields.io/badge/Created_By_Vantern-white?style=for-the-badge" alt="Created By Badge" />
<img src="https://img.shields.io/badge/Created_Using-Typescript-blue?style=for-the-badge" alt="Created Using Badge" />

</div>
Expand All @@ -24,10 +24,10 @@ Quest Bot is capable of:

# Contributing

Quest Bot is worked on by Duck Organization but we are opensource!
Quest Bot is worked on by Vantern but we are opensource!
Anyone can contribute!

Feel free to open a pull request! Just make sure to follow the guidelines at [CONTRIBUTING.md](https://github.com/duck-organization/quest-bot/blob/main/CONTRIBUTING.md).
Feel free to open a pull request! Just make sure to follow the guidelines at [CONTRIBUTING.md](https://github.com/vantern-org/questbot/blob/main/CONTRIBUTING.md).

## Running locally

Expand All @@ -50,8 +50,8 @@ This project is licensed under the Affero GNU General Public License v3.0 (AGPL-

## Links

[Status Page](https://status.duckorg.com), [Bot Documentation](https://duckorg.com/bot/docs) & [Official Discord](https://discord.gg/ksuqZ77R88)
[Status Page](https://status.vantern.org), [Bot Documentation](https://vantern.org/bot/docs) & [Official Discord](https://discord.gg/ksuqZ77R88)

## Contact

If you have any questions or suggestions, feel free to reach out to us on our [Discord server](https://discord.gg/ksuqZ77R88) or send us an email at contact@duckorg.com.
If you have any questions or suggestions, feel free to reach out to us on our [Discord server](https://discord.gg/ksuqZ77R88) or send us an email at contact@vantern.org.
6 changes: 3 additions & 3 deletions apps/bot/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@duckorganization/questbot",
"name": "@questbot/bot",
"version": "0.1.5",
"private": true,
"type": "module",
"description": "A Discord bot built with Sapphire framework",
"license": "AGPL-3.0-or-later",
"author": "Duck Organization",
"author": "Vantern",
"repository": {
"type": "git",
"url": "git+https://github.com/duck-organization/quest-bot.git"
"url": "git+https://github.com/vantern-org/questbot.git"
},
"main": "./dist/index.js",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion apps/bot/src/commands/moderation/automod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class AutoModCommand extends Command {
registry.registerChatInputCommand((builder) =>
builder
.setName('automod')
.setDescription('Block words from being said!')
.setDescription('Keep your server clean!')
.setDefaultMemberPermissions(0)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) =>
subcommand
Expand Down
9 changes: 9 additions & 0 deletions apps/bot/src/commands/moderation/nick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type SlashCommandStringOption,
type SlashCommandUserOption,
} from 'discord.js';
import { containsBlockedWord } from '#lib/automod.js';
import { errorEmbed, successEmbed } from '#utils/embeds.js';
import { emojis } from '#utils/emoji.js';

Expand Down Expand Up @@ -84,6 +85,14 @@ export class NickCommand extends Command {
return;
}

if (nickname && (await containsBlockedWord(interaction.guild.id, nickname))) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} That nickname contains a word blocked by this server.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

try {
await targetMember.setNickname(nickname);
await interaction.reply({
Expand Down
154 changes: 154 additions & 0 deletions apps/bot/src/commands/moderation/sticky.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Command } from '@sapphire/framework';
import {
type GuildTextBasedChannel,
MessageFlags,
PermissionFlagsBits,
PermissionsBitField,
type SlashCommandStringOption,
type SlashCommandSubcommandBuilder,
} from 'discord.js';
import { containsBlockedWord } from '#lib/automod.js';
import { getSticky, removeSticky, repostSticky, setSticky } from '#lib/sticky.js';
import { errorEmbed, infoEmbed, successEmbed } from '#utils/embeds.js';
import { emojis } from '#utils/emoji.js';

export class StickyCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, { ...options, preconditions: ['devMode'] });
}

public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName('sticky')
.setDescription('Pin a message to the bottom of this channel.')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) =>
subcommand
.setName('set')
.setDescription('Set the sticky message for this channel')
.addStringOption((option: SlashCommandStringOption) =>
option
.setName('message')
.setDescription('The message to pin at the bottom of this channel')
.setRequired(true)
.setMaxLength(300),
),
)
.addSubcommand((subcommand: SlashCommandSubcommandBuilder) =>
subcommand.setName('remove').setDescription('Remove the sticky message from this channel'),
),
);
}

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

const channel = interaction.channel;

if (!channel?.isTextBased()) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} This channel cannot have a sticky message.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

if (!channel.permissionsFor(interaction.member)?.has(PermissionsBitField.Flags.ManageChannels)) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} You do not have permission to manage this channel.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

if (interaction.options.getSubcommand() === 'set') {
await this.set(interaction, channel);
return;
}

await this.remove(interaction, channel);
}

private async set(interaction: Command.ChatInputCommandInteraction<'cached'>, channel: GuildTextBasedChannel) {
const me = interaction.guild.members.me;

if (!me || !channel.permissionsFor(me).has(PermissionsBitField.Flags.SendMessages)) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} I cannot send messages here.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

const content = interaction.options.getString('message', true).trim();

if (!content) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} The sticky message cannot be empty.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

if (await containsBlockedWord(interaction.guildId, content)) {
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} That message contains a word blocked by this server.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

try {
const sticky = await setSticky(interaction.guildId, interaction.guild.name, channel.id, content);
await repostSticky(channel, sticky, true);

await interaction.reply({
embeds: [successEmbed(`${emojis.rightArrow1} Sticky message set for this channel.`)],
flags: MessageFlags.Ephemeral,
});
} catch (error) {
console.error(error);
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} Failed to set the sticky message for this channel.`)],
flags: MessageFlags.Ephemeral,
});
}
}

private async remove(interaction: Command.ChatInputCommandInteraction<'cached'>, channel: GuildTextBasedChannel) {
try {
const sticky = await getSticky(interaction.guildId, channel.id);
const removed = await removeSticky(interaction.guildId, channel.id);

if (!removed) {
await interaction.reply({
embeds: [infoEmbed(`${emojis.rightArrow2} There is no sticky message in this channel.`)],
flags: MessageFlags.Ephemeral,
});
return;
}

if (sticky?.stickyMessageId) {
await channel.messages.delete(sticky.stickyMessageId).catch(() => {});
}

await interaction.reply({
embeds: [successEmbed(`${emojis.rightArrow1} Sticky message removed from this channel.`)],
flags: MessageFlags.Ephemeral,
});
} catch (error) {
console.error(error);
await interaction.reply({
embeds: [errorEmbed(`${emojis.rightArrow2} Failed to remove the sticky message from this channel.`)],
flags: MessageFlags.Ephemeral,
});
}
}
}
2 changes: 1 addition & 1 deletion apps/bot/src/commands/promotion/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ export class BotCommand extends Command {
}

public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
await interaction.reply({ embeds: [infoEmbed(`${emojis.rightArrow1} https://duckorg.com/bot/invite`)] });
await interaction.reply({ embeds: [infoEmbed(`${emojis.rightArrow1} https://vantern.org/bot/invite`)] });
}
}
2 changes: 1 addition & 1 deletion apps/bot/src/commands/promotion/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ export class DiscordCommand extends Command {
}

public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
await interaction.reply({ embeds: [infoEmbed(`${emojis.rightArrow1} https://duckorg.com/discord`)] });
await interaction.reply({ embeds: [infoEmbed(`${emojis.rightArrow1} https://vantern.org/discord`)] });
}
}
2 changes: 1 addition & 1 deletion apps/bot/src/commands/promotion/invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ export class InviteCommand extends Command {
}

public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
await interaction.reply({ embeds: [infoEmbed(`${emojis.rightArrow1} https://duckorg.com/bot/invite`)] });
await interaction.reply({ embeds: [infoEmbed(`${emojis.rightArrow1} https://vantern.org/bot/invite`)] });
}
}
10 changes: 9 additions & 1 deletion apps/bot/src/commands/utility/fun/confess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
TextInputBuilder,
TextInputStyle,
} from 'discord.js';
import { containsBlockedWord } from '#lib/automod.js';
import { isConfessionBlacklisted, storeConfessionContext } from '#lib/confessions.js';
import { getSettings } from '#lib/settings.js';
import { errorEmbed, successEmbed } from '#utils/embeds.js';
Expand All @@ -38,7 +39,7 @@ export class ConfessCommand extends Command {
return;
}

const settings = await getSettings(interaction.guild.id, interaction.guild.name);
const settings = await getSettings(interaction.guild.id);

if (settings.confessionEnabled === false) {
await interaction.reply({
Expand Down Expand Up @@ -99,6 +100,13 @@ export class ConfessCommand extends Command {

const confession = modalSubmit.fields.getTextInputValue('confession-text');

if (await containsBlockedWord(interaction.guild.id, confession)) {
await modalSubmit.editReply({
embeds: [errorEmbed(`${emojis.rightArrow2} Your confession contains a word blocked by this server.`)],
});
return;
}

const confessionChannel = await interaction.guild.channels.fetch(settings.confessionChannelId).catch(() => null);

if (!(confessionChannel instanceof TextChannel)) {
Expand Down
Loading