32 Commits

Author SHA1 Message Date
Anthony Berg
71f0ee9f01 refactor(arabot): cleanup sus command from deprecated functions
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
ESLint / Run eslint scanning (push) Has been cancelled
Prettier / Run prettier scanning (push) Has been cancelled
2025-01-16 00:58:11 +01:00
Anthony Berg
881f9bfc24 fix(arabot): new sus role was put in the wrong place 2025-01-16 00:57:25 +01:00
Anthony Berg
98b9ac6fde feat(arabot): add a check if the user is restricted when trying to gain access to the server 2025-01-15 20:46:25 +01:00
Anthony Berg
1f92bf5d68 fix: update the role snowflake to new ones 2025-01-15 20:35:47 +01:00
Anthony Berg
d9f04e8d49 Revert "fix: remove accidentally given nv roles from vegans"
This reverts commit b4c8f0785c.
2025-01-15 20:31:08 +01:00
Anthony Berg
b4c8f0785c fix: remove accidentally given nv roles from vegans 2025-01-15 20:26:49 +01:00
Anthony Berg
7918f73e7d feat: turn off the fixer for the roles reassignment 2025-01-15 20:06:22 +01:00
Anthony Berg
ea211a9111 feat: add fixer for when roles get recreated 2025-01-15 19:20:07 +01:00
Anthony Berg
32776a2311 feat: add log on Discord when bot has started
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
ESLint / Run eslint scanning (push) Waiting to run
Prettier / Run prettier scanning (push) Waiting to run
2025-01-15 17:42:54 +01:00
Anthony Berg
d72b66f988 refactor: update activist role to new snowflake 2025-01-15 16:47:48 +01:00
Anthony Berg
e03bd6e85e refactor: update roles to new snowflake 2025-01-15 16:22:31 +01:00
Anthony Berg
a400cf9507 refactor: update roles to new snowflake 2025-01-15 16:13:19 +01:00
Anthony Berg
2fbb6c9265 feat: update deps and breaking changes 2025-01-15 16:07:21 +01:00
Anthony Berg
fc8c12b346 Merge pull request #212 from veganhacktivists/coolify
Coolify support
2025-01-15 15:25:03 +01:00
Joaquín Triñanes
9ebf8a6938 Allow redis to use password auth 2024-10-25 10:58:10 +02:00
Joaquín Triñanes
bc7f2ffcfd Add nixpacks config 2024-10-25 10:30:56 +02:00
Joaquín Triñanes
86f391e131 Enable corepack 2024-10-25 10:23:20 +02:00
Anthony Berg
63c3b14b1c feat(interaction): update info for welcome message 2024-09-03 17:48:28 +02:00
Anthony Berg
a5187ec567 feat(utils): update info for verification message 2024-09-02 22:36:43 +02:00
Anthony Berg
222c3cb81a feat(utils): add non-vegan vc text channel to IDs 2024-09-02 22:36:29 +02:00
Anthony Berg
8f8580398e refactor(arabot): ran prettier 2024-09-02 22:24:24 +02:00
Anthony Berg
fe88e9f87b feat(arabot): add information that you can use apply command for verification 2024-09-02 22:24:12 +02:00
Anthony Berg
4ad35f5b57 fix(arabot): add type safety to deleting text channels 2024-08-26 23:11:30 +02:00
Anthony Berg
1c9f6612a3 build: update deps 2024-08-26 22:43:31 +02:00
Anthony Berg
88dd678bdc feat(arabot): make rules on trusted shorter and clearer 2024-08-11 02:03:30 +01:00
Anthony Berg
9c51be9ab6 feat(arabot): make the information message for trusted more blunt 2024-08-11 01:54:15 +01:00
Anthony Berg
128b15f18f feat(arabot): nerf autotruster to level 7 2024-08-11 01:48:03 +01:00
Anthony Berg
dba9aa970e refactor(arabot): run prettier 2024-08-07 01:39:21 +02:00
Anthony Berg
0ac0ff7f5c feat(arabot): add automatic trusted role at level 5 2024-08-07 01:39:02 +02:00
Anthony Berg
ae0afa02db feat(db): add checking previous warns/restrictions 2024-08-07 01:38:38 +02:00
Anthony Berg
3009a0f923 feat(arabot): add emitter when user levels up 2024-08-07 01:38:16 +02:00
Anthony Berg
a09b007831 refactor(db): move db functions to separate folders to aid distinguishing functions 2024-08-07 00:48:40 +02:00
63 changed files with 924 additions and 484 deletions

View File

@@ -10,8 +10,11 @@ POSTGRES_USER=USERNAME
POSTGRES_PASSWORD=PASSWORD POSTGRES_PASSWORD=PASSWORD
POSTGRES_DB=DB POSTGRES_DB=DB
# Redis # Redis (if running everything within docker compose, use "redis" for the host and leave the rest empty)
REDIS_URL= # URL to redis database (if running everything within docker compose, use "redis") REDIS_HOST= # URL to redis database
REDIS_USER= # redis database user
REDIS_PASSWORD= # redis database password
REDIS_PORT= # redis database port
# Database URL (designed for Postgres, but designed on Prisma) # Database URL (designed for Postgres, but designed on Prisma)
DATABASE_URL= # "postgresql://USERNAME:PASSWORD@postgres:5432/DB?schema=ara&sslmode=prefer" DATABASE_URL= # "postgresql://USERNAME:PASSWORD@postgres:5432/DB?schema=ara&sslmode=prefer"

5
nixpacks.toml Normal file
View File

@@ -0,0 +1,5 @@
[phases.build]
cmds = ["pnpm prisma generate", "..."]
[start]
cmd = 'pnpm run start:migrate'

View File

@@ -33,31 +33,31 @@
"node": ">=20", "node": ">=20",
"pnpm": ">=9" "pnpm": ">=9"
}, },
"packageManager": "pnpm@9.6.0",
"dependencies": { "dependencies": {
"@prisma/client": "^5.17.0", "@prisma/client": "^5.22.0",
"@sapphire/discord.js-utilities": "^7.3.0", "@sapphire/discord.js-utilities": "^7.3.2",
"@sapphire/framework": "^5.2.1", "@sapphire/framework": "^5.3.2",
"@sapphire/plugin-logger": "^4.0.2", "@sapphire/plugin-logger": "^4.0.2",
"@sapphire/plugin-scheduled-tasks": "^10.0.1", "@sapphire/plugin-scheduled-tasks": "^10.0.2",
"@sapphire/plugin-subcommands": "^6.0.3", "@sapphire/plugin-subcommands": "^6.0.3",
"@sapphire/stopwatch": "^1.5.2", "@sapphire/stopwatch": "^1.5.4",
"@sapphire/time-utilities": "^1.7.12", "@sapphire/time-utilities": "^1.7.14",
"@sapphire/ts-config": "^5.0.1", "@sapphire/ts-config": "^5.0.1",
"@sapphire/utilities": "^3.17.0", "@sapphire/utilities": "^3.18.1",
"bullmq": "^5.12.0", "bullmq": "^5.34.10",
"discord.js": "^14.15.3", "discord.js": "^14.17.3",
"ioredis": "^5.4.1", "ioredis": "^5.4.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "~5.4.5" "typescript": "~5.4.5"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.14", "@types/node": "^20.17.13",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"prettier": "3.2.4", "prettier": "3.2.4",
"prisma": "^5.17.0" "prisma": "^5.22.0"
} },
"packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"
} }

658
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,6 @@ model User {
TempBanEndMod TempBan[] @relation("endTbanMod") TempBanEndMod TempBan[] @relation("endTbanMod")
VCMuteUser VCMute[] @relation("vcMuteUser") VCMuteUser VCMute[] @relation("vcMuteUser")
VCMuteMod VCMute[] @relation("vcMuteMod") VCMuteMod VCMute[] @relation("vcMuteMod")
ClearCommandMod ClearCommand[]
} }
model Verify { model Verify {
@@ -313,11 +312,3 @@ model VCMute {
endTime DateTime? endTime DateTime?
reason String? reason String?
} }
model ClearCommand {
id Int @id @default(autoincrement())
mod User @relation(fields: [modId], references: [id])
modId String
messages Int
time DateTime @default(now())
}

View File

@@ -18,7 +18,7 @@
*/ */
import { Args, Command, RegisterBehavior } from '@sapphire/framework'; import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Message } from 'discord.js'; import { Message, MessageFlagsBitField } from 'discord.js';
import { ChannelType, TextChannel } from 'discord.js'; import { ChannelType, TextChannel } from 'discord.js';
export class AnonymousCommand extends Command { export class AnonymousCommand extends Command {
@@ -67,8 +67,8 @@ export class AnonymousCommand extends Command {
if (guild === null) { if (guild === null) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching guild!', content: 'Error fetching guild!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
@@ -77,8 +77,17 @@ export class AnonymousCommand extends Command {
if (interaction.channel === null) { if (interaction.channel === null) {
await interaction.reply({ await interaction.reply({
content: 'Error getting the channel!', content: 'Error getting the channel!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
});
return;
}
if (!interaction.channel.isSendable()) {
await interaction.reply({
content: `I do not have sufficient permissions to send a message in ${interaction.channel}!`,
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
}); });
return; return;
} }
@@ -86,8 +95,8 @@ export class AnonymousCommand extends Command {
await interaction.channel.send(message); await interaction.channel.send(message);
await interaction.reply({ await interaction.reply({
content: 'Sent the message', content: 'Sent the message',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
@@ -95,8 +104,8 @@ export class AnonymousCommand extends Command {
if (channel.type !== ChannelType.GuildText) { if (channel.type !== ChannelType.GuildText) {
await interaction.reply({ await interaction.reply({
content: 'Could not send, unsupported text channel!', content: 'Could not send, unsupported text channel!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
} }
@@ -105,8 +114,8 @@ export class AnonymousCommand extends Command {
await interaction.reply({ await interaction.reply({
content: 'Sent the message', content: 'Sent the message',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
} }
@@ -121,7 +130,7 @@ export class AnonymousCommand extends Command {
return; return;
} }
if (channel.isTextBased()) { if (channel.isSendable()) {
await channel.send(text); await channel.send(text);
} else { } else {
await message.react('❌'); await message.react('❌');

View File

@@ -18,7 +18,6 @@
*/ */
import { Args, Command, RegisterBehavior } from '@sapphire/framework'; import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { PermissionFlagsBits } from 'discord.js';
import type { Message } from 'discord.js'; import type { Message } from 'discord.js';
export class ClearCommand extends Command { export class ClearCommand extends Command {
@@ -27,8 +26,7 @@ export class ClearCommand extends Command {
...options, ...options,
name: 'clear', name: 'clear',
description: 'Deletes 1-100 messages in bulk', description: 'Deletes 1-100 messages in bulk',
preconditions: [['CoordinatorOnly', 'ModOnly']], preconditions: ['CoordinatorOnly'],
requiredUserPermissions: [PermissionFlagsBits.ManageMessages]
}); });
} }

View File

@@ -20,7 +20,7 @@
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Guild, Message } from 'discord.js'; import type { User, Guild, Message } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser'; import { updateUser } from '#utils/database/dbExistingUser';
import { getBalance } from '#utils/database/economy'; import { getBalance } from '#utils/database/fun/economy';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
export class BalanceCommand extends Command { export class BalanceCommand extends Command {

View File

@@ -21,7 +21,7 @@ import { Command, RegisterBehavior } from '@sapphire/framework';
import { Time } from '@sapphire/time-utilities'; import { Time } from '@sapphire/time-utilities';
import type { User, Guild, GuildMember, Message } from 'discord.js'; import type { User, Guild, GuildMember, Message } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser'; import { updateUser } from '#utils/database/dbExistingUser';
import { daily, getLastDaily } from '#utils/database/economy'; import { daily, getLastDaily } from '#utils/database/fun/economy';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';

View File

@@ -20,7 +20,7 @@
import { Args, Command, RegisterBehavior } from '@sapphire/framework'; import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Guild, Message } from 'discord.js'; import type { User, Guild, Message } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser'; import { updateUser } from '#utils/database/dbExistingUser';
import { getBalance, transfer } from '#utils/database/economy'; import { getBalance, transfer } from '#utils/database/fun/economy';
import { EmbedBuilder, TextChannel } from 'discord.js'; import { EmbedBuilder, TextChannel } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';

View File

@@ -20,7 +20,7 @@
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder, GuildMember } from 'discord.js';
import { N1984 } from '#utils/gifs'; import { N1984 } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun'; import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class N1984Command extends Command { export class N1984Command extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {

View File

@@ -20,7 +20,7 @@
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder, GuildMember } from 'discord.js';
import { Cringe } from '#utils/gifs'; import { Cringe } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun'; import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class CringeCommand extends Command { export class CringeCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {

View File

@@ -20,7 +20,7 @@
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder, GuildMember } from 'discord.js';
import { Hugs } from '#utils/gifs'; import { Hugs } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun'; import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class HugCommand extends Command { export class HugCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {

View File

@@ -20,7 +20,7 @@
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder, GuildMember } from 'discord.js';
import { Kill } from '#utils/gifs'; import { Kill } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun'; import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class KillCommand extends Command { export class KillCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {

View File

@@ -20,7 +20,7 @@
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder, GuildMember } from 'discord.js';
import { Poke } from '#utils/gifs'; import { Poke } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun'; import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class PokeCommand extends Command { export class PokeCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {

View File

@@ -21,9 +21,12 @@ import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Message, Snowflake, TextChannel, Guild } from 'discord.js'; import type { User, Message, Snowflake, TextChannel, Guild } from 'discord.js';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { addBan, checkBan } from '#utils/database/ban'; import { addBan, checkBan } from '#utils/database/moderation/ban';
import { addEmptyUser, updateUser } from '#utils/database/dbExistingUser'; import { addEmptyUser, updateUser } from '#utils/database/dbExistingUser';
import { checkTempBan, removeTempBan } from '#utils/database/tempBan'; import {
checkTempBan,
removeTempBan,
} from '#utils/database/moderation/tempBan';
export class BanCommand extends Command { export class BanCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {

View File

@@ -22,7 +22,7 @@ import { Duration, DurationFormatter } from '@sapphire/time-utilities';
import type { User, Snowflake, TextChannel, Guild } from 'discord.js'; import type { User, Snowflake, TextChannel, Guild } from 'discord.js';
import { EmbedBuilder, Message } from 'discord.js'; import { EmbedBuilder, Message } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { addTempBan, checkTempBan } from '#utils/database/tempBan'; import { addTempBan, checkTempBan } from '#utils/database/moderation/tempBan';
import { addEmptyUser, updateUser } from '#utils/database/dbExistingUser'; import { addEmptyUser, updateUser } from '#utils/database/dbExistingUser';
export class TempBanCommand extends Command { export class TempBanCommand extends Command {

View File

@@ -28,8 +28,11 @@ import type {
} from 'discord.js'; } from 'discord.js';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { removeBan, checkBan, addBan } from '#utils/database/ban'; import { removeBan, checkBan, addBan } from '#utils/database/moderation/ban';
import { checkTempBan, removeTempBan } from '#utils/database/tempBan'; import {
checkTempBan,
removeTempBan,
} from '#utils/database/moderation/tempBan';
import { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser'; import { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser';
export class UnbanCommand extends Command { export class UnbanCommand extends Command {

View File

@@ -36,7 +36,7 @@ import {
updateUser, updateUser,
fetchRoles, fetchRoles,
} from '#utils/database/dbExistingUser'; } from '#utils/database/dbExistingUser';
import { restrict, checkActive } from '#utils/database/restriction'; import { restrict, checkActive } from '#utils/database/moderation/restriction';
import { randint } from '#utils/maths'; import { randint } from '#utils/maths';
import { blockedRolesAfterRestricted } from '#utils/blockedRoles'; import { blockedRolesAfterRestricted } from '#utils/blockedRoles';

View File

@@ -21,7 +21,7 @@ import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { ChannelType, EmbedBuilder } from 'discord.js'; import { ChannelType, EmbedBuilder } from 'discord.js';
import type { Message, TextChannel, Guild, Snowflake } from 'discord.js'; import type { Message, TextChannel, Guild, Snowflake } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { getRestrictions } from '#utils/database/restriction'; import { getRestrictions } from '#utils/database/moderation/restriction';
import { checkStaff } from '#utils/checker'; import { checkStaff } from '#utils/checker';
export class RestrictLogsCommand extends Command { export class RestrictLogsCommand extends Command {

View File

@@ -26,7 +26,7 @@ import {
unRestrict, unRestrict,
checkActive, checkActive,
unRestrictLegacy, unRestrictLegacy,
} from '#utils/database/restriction'; } from '#utils/database/moderation/restriction';
export class UnRestrictCommand extends Command { export class UnRestrictCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {

View File

@@ -30,16 +30,16 @@ import {
TextChannel, TextChannel,
GuildMember, GuildMember,
Snowflake, Snowflake,
MessageFlagsBitField,
} from 'discord.js'; } from 'discord.js';
import type { Message } from 'discord.js'; import type { Message } from 'discord.js';
import { isMessageInstance } from '@sapphire/discord.js-utilities';
import { import {
addSusNoteDB, addSusNoteDB,
findNotes, findNotes,
getNote, getNote,
deactivateNote, deactivateNote,
deactivateAllNotes, deactivateAllNotes,
} from '#utils/database/sus'; } from '#utils/database/moderation/sus';
import { checkStaff } from '#utils/checker'; import { checkStaff } from '#utils/checker';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { createSusLogEmbed } from '#utils/embeds'; import { createSusLogEmbed } from '#utils/embeds';
@@ -157,10 +157,10 @@ export class SusCommand extends Subcommand {
const { guild } = interaction; const { guild } = interaction;
// Checks if all the variables are of the right type // Checks if all the variables are of the right type
if (!(guild instanceof Guild)) { if (guild === null) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching guild!', content: 'Error fetching guild!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
}); });
return; return;
} }
@@ -169,7 +169,7 @@ export class SusCommand extends Subcommand {
await interaction.reply({ await interaction.reply({
content: info.message, content: info.message,
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
}); });
} }
@@ -195,7 +195,7 @@ export class SusCommand extends Subcommand {
const guild = message.guild; const guild = message.guild;
if (!(guild instanceof Guild)) { if (guild === null) {
await message.react('❌'); await message.react('❌');
await message.reply( await message.reply(
'Could not find guild! Make sure you run this command in a server.', 'Could not find guild! Make sure you run this command in a server.',
@@ -292,7 +292,7 @@ export class SusCommand extends Subcommand {
if (guild == null) { if (guild == null) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching guild!', content: 'Error fetching guild!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
}); });
return; return;
} }
@@ -306,8 +306,8 @@ export class SusCommand extends Subcommand {
if (notes.length === 0) { if (notes.length === 0) {
await interaction.reply({ await interaction.reply({
content: `${user} has no sus notes!`, content: `${user} has no sus notes!`,
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
@@ -318,8 +318,8 @@ export class SusCommand extends Subcommand {
// Sends the notes to the user // Sends the notes to the user
await interaction.reply({ await interaction.reply({
embeds: [noteEmbed], embeds: [noteEmbed],
ephemeral: !staffChannel, flags: staffChannel ? undefined : MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
} }
@@ -333,8 +333,8 @@ export class SusCommand extends Subcommand {
if (guild === null || channel === null) { if (guild === null || channel === null) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching guild or channel!', content: 'Error fetching guild or channel!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
@@ -346,8 +346,8 @@ export class SusCommand extends Subcommand {
if (note === null) { if (note === null) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching note from database!', content: 'Error fetching note from database!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
@@ -363,8 +363,8 @@ export class SusCommand extends Subcommand {
if (user === undefined) { if (user === undefined) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching user!', content: 'Error fetching user!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
@@ -407,16 +407,21 @@ export class SusCommand extends Subcommand {
const message = await interaction.reply({ const message = await interaction.reply({
embeds: [noteEmbed], embeds: [noteEmbed],
components: [buttons], components: [buttons],
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
// Checks if the message is not an APIMessage // Checks if the message is not an APIMessage
if (!isMessageInstance(message)) { if (message.resource === null) {
await interaction.editReply('Failed to retrieve the message :('); await interaction.editReply('Failed to retrieve the message :(');
return; return;
} }
if (!channel.isSendable()) {
await interaction.editReply('Cannot send messages in this channel!');
return;
}
// Listen for the button presses // Listen for the button presses
const collector = channel.createMessageComponentCollector({ const collector = channel.createMessageComponentCollector({
max: 1, // Maximum of 1 button press max: 1, // Maximum of 1 button press
@@ -470,10 +475,10 @@ export class SusCommand extends Subcommand {
) { ) {
// Find user // Find user
let user = guild.client.users.cache.get(userId); let user = guild.client.users.cache.get(userId);
if (!(user instanceof User)) { if (user === undefined) {
user = await guild.client.users.fetch(userId).catch(() => undefined); user = await guild.client.users.fetch(userId).catch(() => undefined);
} }
if (!(user instanceof User)) return; if (user === undefined) return;
// Log the sus note // Log the sus note
let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as
@@ -519,8 +524,8 @@ export class SusCommand extends Subcommand {
if (guild === null || channel === null) { if (guild === null || channel === null) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching guild or channel!', content: 'Error fetching guild or channel!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
@@ -531,8 +536,8 @@ export class SusCommand extends Subcommand {
if (member === undefined) { if (member === undefined) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching user!', content: 'Error fetching user!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
@@ -545,8 +550,8 @@ export class SusCommand extends Subcommand {
if (notes.length === 0) { if (notes.length === 0) {
await interaction.reply({ await interaction.reply({
content: `${user} had no notes!`, content: `${user} had no notes!`,
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
@@ -596,16 +601,21 @@ export class SusCommand extends Subcommand {
const message = await interaction.reply({ const message = await interaction.reply({
embeds: [noteEmbed], embeds: [noteEmbed],
components: [buttons], components: [buttons],
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
// Checks if the message is not an APIMessage // Checks if the message is not an APIMessage
if (!isMessageInstance(message)) { if (message.resource === null) {
await interaction.editReply('Failed to retrieve the message :('); await interaction.editReply('Failed to retrieve the message :(');
return; return;
} }
if (!channel.isSendable()) {
await interaction.editReply('Cannot send messages in this channel!');
return;
}
// Listen for the button presses // Listen for the button presses
const collector = channel.createMessageComponentCollector({ const collector = channel.createMessageComponentCollector({
max: 1, // Maximum of 1 button press max: 1, // Maximum of 1 button press

View File

@@ -19,7 +19,11 @@
import { Args, Command, RegisterBehavior } from '@sapphire/framework'; import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { GuildMember, Message } from 'discord.js'; import type { GuildMember, Message } from 'discord.js';
import { addMute, removeMute, checkActive } from '#utils/database/vcMute'; import {
addMute,
removeMute,
checkActive,
} from '#utils/database/moderation/vcMute';
import { addExistingUser } from '#utils/database/dbExistingUser'; import { addExistingUser } from '#utils/database/dbExistingUser';
export class VCMuteCommand extends Command { export class VCMuteCommand extends Command {

View File

@@ -21,7 +21,10 @@ import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, TextChannel } from 'discord.js'; import { EmbedBuilder, TextChannel } from 'discord.js';
import type { Message, Guild, User } from 'discord.js'; import type { Message, Guild, User } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { deleteWarning, fetchWarning } from '#utils/database/warnings'; import {
deleteWarning,
fetchWarning,
} from '#utils/database/moderation/warnings';
import { checkStaff } from '#utils/checker'; import { checkStaff } from '#utils/checker';
export class DeleteWarningCommand extends Command { export class DeleteWarningCommand extends Command {

View File

@@ -25,7 +25,7 @@ import {
} from '@sapphire/framework'; } from '@sapphire/framework';
import type { User, Message, Snowflake, Guild, TextChannel } from 'discord.js'; import type { User, Message, Snowflake, Guild, TextChannel } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser'; import { updateUser } from '#utils/database/dbExistingUser';
import { addWarn } from '#utils/database/warnings'; import { addWarn } from '#utils/database/moderation/warnings';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';

View File

@@ -21,7 +21,7 @@ import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { ChannelType, EmbedBuilder } from 'discord.js'; import { ChannelType, EmbedBuilder } from 'discord.js';
import type { Message, Guild, User } from 'discord.js'; import type { Message, Guild, User } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { fetchWarnings } from '#utils/database/warnings'; import { fetchWarnings } from '#utils/database/moderation/warnings';
import { checkStaff } from '#utils/checker'; import { checkStaff } from '#utils/checker';
import { createWarningsEmbed } from '#utils/embeds'; import { createWarningsEmbed } from '#utils/embeds';

View File

@@ -146,8 +146,8 @@ export class TrustedCommand extends Command {
.send( .send(
`You have been given the ${trusted.name} role by ${mod}!` + `You have been given the ${trusted.name} role by ${mod}!` +
'\n\nThis role allows you to post attachments to the server and stream in VCs.' + '\n\nThis role allows you to post attachments to the server and stream in VCs.' +
"\nMake sure that you follow the rules, and don't post anything NSFW, anything objectifying animals and follow Discord's ToS." + '\nMake sure that you follow the rules, especially by **not** posting anything **NSFW**, and **no animal products or consumption of animal products**.' +
`\nNot following these rules can result in the removal of the ${trusted.name} role.`, `\n\nNot following these rules will result in the **immediate removal** of the ${trusted.name} role.`,
) )
.catch(() => {}); .catch(() => {});
info.success = true; info.success = true;

View File

@@ -18,6 +18,7 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import IDs from '#utils/ids';
export class InfoCommand extends Command { export class InfoCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {
@@ -94,7 +95,9 @@ export class InfoCommand extends Command {
message = message =
"If you want to have the vegan or activist role, you'll need to do a voice verification. " + "If you want to have the vegan or activist role, you'll need to do a voice verification. " +
"To do this, hop into the 'Verification' voice channel." + "To do this, hop into the 'Verification' voice channel." +
"\n\nIf there aren't any verifiers available, you'll be disconnected, and you can rejoin later."; "\n\nIf there aren't any verifiers available, you'll be disconnected, and you can rejoin later." +
`\n\nAlternatively if you would like text verification, you can use \`/apply\` in <#${IDs.channels.nonVegan.vcText}> ` +
'to be able fill out a Vegan Verification form through the Appy Bot.';
break; break;
case 'modMail': case 'modMail':
message = message =

View File

@@ -17,9 +17,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { isMessageInstance } from '@sapphire/discord.js-utilities';
import { Command } from '@sapphire/framework'; import { Command } from '@sapphire/framework';
import type { Message } from 'discord.js'; import { Message, MessageFlagsBitField } from 'discord.js';
export class PingCommand extends Command { export class PingCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {
@@ -41,12 +40,13 @@ export class PingCommand extends Command {
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const msg = await interaction.reply({ const msg = await interaction.reply({
content: 'Ping?', content: 'Ping?',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
if (isMessageInstance(msg)) { if (msg.resource !== null && msg.resource.message !== null) {
const diff = msg.createdTimestamp - interaction.createdTimestamp; const diff =
msg.resource.message.createdTimestamp - interaction.createdTimestamp;
const ping = Math.round(this.container.client.ws.ping); const ping = Math.round(this.container.client.ws.ping);
return interaction.editReply( return interaction.editReply(
`Pong 🏓! (Round trip took: ${diff}ms. Heartbeat: ${ping}ms.)`, `Pong 🏓! (Round trip took: ${diff}ms. Heartbeat: ${ping}ms.)`,
@@ -57,6 +57,11 @@ export class PingCommand extends Command {
} }
public async messageRun(message: Message) { public async messageRun(message: Message) {
if (!message.channel.isSendable()) {
// TODO manage logging/errors properly
return;
}
const msg = await message.channel.send('Ping?'); const msg = await message.channel.send('Ping?');
const diff = msg.createdTimestamp - message.createdTimestamp; const diff = msg.createdTimestamp - message.createdTimestamp;

View File

@@ -20,7 +20,7 @@
import { Args, Command, RegisterBehavior } from '@sapphire/framework'; import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Guild, Message } from 'discord.js'; import type { User, Guild, Message } from 'discord.js';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { getRank, xpToNextLevel } from '#utils/database/xp'; import { getRank, xpToNextLevel } from '#utils/database/fun/xp';
export class RankCommand extends Command { export class RankCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.LoaderContext, options: Command.Options) {

View File

@@ -27,6 +27,10 @@ import '@sapphire/plugin-logger/register';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { Redis } from 'ioredis'; import { Redis } from 'ioredis';
const REDIS_PORT = process.env.REDIS_PORT
? parseInt(process.env.REDIS_PORT)
: undefined;
// Setting up the Sapphire client // Setting up the Sapphire client
const client = new SapphireClient({ const client = new SapphireClient({
defaultPrefix: process.env.DEFAULT_PREFIX, defaultPrefix: process.env.DEFAULT_PREFIX,
@@ -49,7 +53,10 @@ const client = new SapphireClient({
tasks: { tasks: {
bull: { bull: {
connection: { connection: {
host: process.env.REDIS_URL, host: process.env.REDIS_HOST,
username: process.env.REDIS_USER,
password: process.env.REDIS_PASSWORD,
port: REDIS_PORT,
}, },
}, },
}, },
@@ -62,9 +69,12 @@ const main = async () => {
client.logger.info('Logging in'); client.logger.info('Logging in');
// Create databases // Create databases
container.database = await new PrismaClient(); container.database = new PrismaClient();
container.redis = new Redis({ container.redis = new Redis({
host: process.env.REDIS_URL, host: process.env.REDIS_HOST,
username: process.env.REDIS_USER,
password: process.env.REDIS_PASSWORD,
port: REDIS_PORT,
db: 1, db: 1,
}); });

View File

@@ -21,8 +21,14 @@ import {
InteractionHandler, InteractionHandler,
InteractionHandlerTypes, InteractionHandlerTypes,
} from '@sapphire/framework'; } from '@sapphire/framework';
import type { ButtonInteraction, GuildMember, TextChannel } from 'discord.js'; import {
ButtonInteraction,
GuildMember,
MessageFlagsBitField,
TextChannel,
} from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { checkActive } from '#utils/database/moderation/restriction';
export class WelcomeButtonHandler extends InteractionHandler { export class WelcomeButtonHandler extends InteractionHandler {
public constructor( public constructor(
@@ -54,7 +60,7 @@ export class WelcomeButtonHandler extends InteractionHandler {
await interaction.reply({ await interaction.reply({
content: content:
'There was an error giving you the role, please try again later or contact ModMail to be let into this server.', 'There was an error giving you the role, please try again later or contact ModMail to be let into this server.',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
}); });
return; return;
} }
@@ -62,6 +68,16 @@ export class WelcomeButtonHandler extends InteractionHandler {
try { try {
member = member as GuildMember; member = member as GuildMember;
// Checks if the user is currently restricted
if (await checkActive(member.id)) {
await interaction.reply({
content: `You are currently restricted from this server! Contact the moderators by sending a DM to <@${IDs.modMail}>.`,
flags: MessageFlagsBitField.Flags.Ephemeral,
});
return;
}
// Give non-vegan role // Give non-vegan role
if (!member.voice.channel) { if (!member.voice.channel) {
await member.roles.add(IDs.roles.nonvegan.nonvegan); await member.roles.add(IDs.roles.nonvegan.nonvegan);
@@ -69,7 +85,8 @@ export class WelcomeButtonHandler extends InteractionHandler {
await general.send( await general.send(
`${member} Welcome to ARA! :D Please check <#${IDs.channels.information.roles}> ` + `${member} Welcome to ARA! :D Please check <#${IDs.channels.information.roles}> ` +
`and remember to follow the <#${IDs.channels.information.conduct}> and to respect ongoing discussions and debates.` + `and remember to follow the <#${IDs.channels.information.conduct}> and to respect ongoing discussions and debates.` +
"\n\nIf you are vegan, you can join the 'Verification' voice channel to be verified and gain access to more channels.", `\n\nIf you are vegan, you can join the 'Verification' voice channel, or use \`/apply\` with the Appy bot in <#${IDs.channels.nonVegan.vcText}>, ` +
'to be verified and gain access to more channels.',
); );
return; return;
} }
@@ -77,13 +94,13 @@ export class WelcomeButtonHandler extends InteractionHandler {
await interaction.reply({ await interaction.reply({
content: content:
"You're currently in a verification, you'll have to leave the verification or get verified before being able to access the server again.", "You're currently in a verification, you'll have to leave the verification or get verified before being able to access the server again.",
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
}); });
} catch (error) { } catch (error) {
await interaction.reply({ await interaction.reply({
content: content:
'There was an error giving you the role, please try again later or contact ModMail to be let into this server.', 'There was an error giving you the role, please try again later or contact ModMail to be let into this server.',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
}); });
} }
} }

View File

@@ -20,7 +20,7 @@
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
import type { GuildBan } from 'discord.js'; import type { GuildBan } from 'discord.js';
import { AuditLogEvent, EmbedBuilder, TextChannel } from 'discord.js'; import { AuditLogEvent, EmbedBuilder, TextChannel } from 'discord.js';
import { addBan, checkBan } from '#utils/database/ban'; import { addBan, checkBan } from '#utils/database/moderation/ban';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser'; import { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser';

View File

@@ -19,8 +19,8 @@
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
import type { GuildMember } from 'discord.js'; import type { GuildMember } from 'discord.js';
import { checkBan, getBanReason } from '#utils/database/ban'; import { checkBan, getBanReason } from '#utils/database/moderation/ban';
import { checkTempBan } from '#utils/database/tempBan'; import { checkTempBan } from '#utils/database/moderation/tempBan';
export class BanJoinListener extends Listener { export class BanJoinListener extends Listener {
public constructor( public constructor(

View File

@@ -20,7 +20,7 @@
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
import type { GuildBan } from 'discord.js'; import type { GuildBan } from 'discord.js';
import { AuditLogEvent, EmbedBuilder, TextChannel } from 'discord.js'; import { AuditLogEvent, EmbedBuilder, TextChannel } from 'discord.js';
import { addBan, checkBan, removeBan } from '#utils/database/ban'; import { addBan, checkBan, removeBan } from '#utils/database/moderation/ban';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser'; import { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser';

View File

@@ -21,7 +21,7 @@
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
import type { Message } from 'discord.js'; import type { Message } from 'discord.js';
import { getLastCount, addCount } from '#utils/database/counting'; import { getLastCount, addCount } from '#utils/database/fun/counting';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class XpListener extends Listener { export class XpListener extends Listener {
@@ -49,6 +49,11 @@ export class XpListener extends Listener {
// If no counts exist on the database, then create the first count from the bot // If no counts exist on the database, then create the first count from the bot
if (lastCount === null) { if (lastCount === null) {
if (this.container.client.id === null) { if (this.container.client.id === null) {
if (!message.channel.isSendable()) {
// TODO manage logging/errors properly
return;
}
message.channel.send( message.channel.send(
'An unexpected error occurred trying to set up the counting channel, please contact a developer!', 'An unexpected error occurred trying to set up the counting channel, please contact a developer!',
); );
@@ -63,6 +68,11 @@ export class XpListener extends Listener {
lastCount = await getLastCount(); lastCount = await getLastCount();
if (lastCount === null) { if (lastCount === null) {
if (!message.channel.isSendable()) {
// TODO manage logging/errors properly
return;
}
message.channel.send( message.channel.send(
'An unexpected error occurred, please contact a developer!', 'An unexpected error occurred, please contact a developer!',
); );

162
src/listeners/fixRoles.ts Normal file
View File

@@ -0,0 +1,162 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2025 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
I used the Sapphire documentation and parts of the code from the Sapphire CLI to
create this file.
*/
import { Listener } from '@sapphire/framework';
import { DurationFormatter } from '@sapphire/time-utilities';
import type { Client } from 'discord.js';
import IDs from '#utils/ids';
import { fetchRoles } from '#utils/database/dbExistingUser';
import { checkActive } from '#utils/database/moderation/restriction';
export class FixRolesOnReady extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, {
...options,
once: true,
event: 'ready',
// !!!!!!!!!!!! WARNING !!!!!!!!!!!!
// THIS SHOULD BE DISABLED BY DEFAULT
// THIS IS ONLY USED FOR RESTORING ROLES TO THE SERVER!
// ENABLING THIS UNINTENTIONALLY WILL CAUSE SLOWDOWNS TO THE BOT DUE TO RATE LIMITING!
enabled: false,
});
}
public async run(client: Client) {
this.container.logger.info(
'FixRolesOnReady: Preparation before starting to fix the roles for each user...',
);
// Fetching the Guild
const guild = await client.guilds.fetch(IDs.guild).catch(() => undefined);
if (guild === undefined) {
this.container.logger.error('FixRolesOnReady: Could not find the server');
return;
}
// Fetching the channel for the logs
// Leave the snowflake parameter empty for no logs
const logChannel = await client.channels.fetch('');
const sendLogs = logChannel !== null;
if (!sendLogs) {
this.container.logger.error(
'FixRolesOnReady: Could not find the channel for bot logs.',
);
} else if (sendLogs && !logChannel.isSendable()) {
this.container.logger.info(
'FixRolesOnReady: No permission to send in bots logs channel.',
);
return;
}
// Get all the current users
this.container.logger.info('FixRolesOnReady: Fetching all the members...');
if (sendLogs) {
logChannel.send('Fetching all the users in ARA!');
}
const members = await guild.members.fetch().catch(() => undefined);
if (members === undefined) {
this.container.logger.error(
'FixRolesOnReady: Could fetch all the members, this function is stopping now.',
);
if (sendLogs) {
logChannel.send("Never mind, something went wrong :'(");
}
return;
}
const totalMembers = members.size;
this.container.logger.info(
`FixRolesOnReady: Done fetching ${totalMembers} members!`,
);
// Giving the roles to each user
let count = 0;
const startTime = new Date().getTime();
this.container.logger.info(
'FixRolesOnReady: Starting the process of fixing the roles for every member...',
);
for (const [userId, member] of members) {
// Send a message with an update for every 50 completions
// Checks if `channelLog` has been set to null
// The RHS of the modulo should be around 100
if (sendLogs && count % 250 === 0) {
const currentTime = new Date().getTime();
const runningTime = currentTime - startTime;
const remaining = totalMembers - count;
// Basing this on the fact that
const eta = remaining * (runningTime / count);
const estimate = new DurationFormatter().format(eta);
logChannel.send(
`Given roles to ${count} out of ${totalMembers} members. Estimated time until completion: ${estimate}`,
);
}
// Checks if the user is restricted, and skips over them if they are
const restricted = await checkActive(userId);
if (restricted) {
continue;
}
// Fetch the roles for the member in the database
const dbRoles = await fetchRoles(userId);
// Filters out the roles that the member does not have
const roles = dbRoles.filter((role) => !member.roles.cache.has(role));
// Give the roles to the member
if (roles.length > 0) {
await member.roles.add(roles);
}
// Log the completion
count += 1;
this.container.logger.info(
`FixRolesOnReady: Given roles to ${count}/${totalMembers}.`,
);
}
// Send the logs that the fix has finished.
const endTime = new Date().getTime();
const totalTime = endTime - startTime;
const totalTimeWritten = new DurationFormatter().format(totalTime);
const finishMessage = `Finished fixing roles for all ${totalMembers} members! It took ${totalTimeWritten} to complete.`;
this.container.logger.info(`FixRolesOnReady: ${finishMessage}`);
if (sendLogs) {
logChannel.send(finishMessage);
}
}
}

View File

@@ -22,14 +22,17 @@ import { ChannelType } from 'discord.js';
import type { GuildChannel, EmbedBuilder } from 'discord.js'; import type { GuildChannel, EmbedBuilder } from 'discord.js';
import { setTimeout } from 'timers/promises'; import { setTimeout } from 'timers/promises';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { checkActive, getRestrictions } from '#utils/database/restriction'; import {
import { findNotes } from '#utils/database/sus'; checkActive,
getRestrictions,
} from '#utils/database/moderation/restriction';
import { findNotes } from '#utils/database/moderation/sus';
import { import {
createRestrictLogEmbed, createRestrictLogEmbed,
createSusLogEmbed, createSusLogEmbed,
createWarningsEmbed, createWarningsEmbed,
} from '#utils/embeds'; } from '#utils/embeds';
import { fetchWarnings } from '#utils/database/warnings'; import { fetchWarnings } from '#utils/database/moderation/warnings';
export class ModMailCreateListener extends Listener { export class ModMailCreateListener extends Listener {
public constructor( public constructor(

View File

@@ -22,6 +22,7 @@
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
import type { Client } from 'discord.js'; import type { Client } from 'discord.js';
import IDs from '#utils/ids';
export class ReadyListener extends Listener { export class ReadyListener extends Listener {
public constructor( public constructor(
@@ -35,8 +36,24 @@ export class ReadyListener extends Listener {
}); });
} }
public run(client: Client) { public async run(client: Client) {
const { username, id } = client.user!; const { username, id } = client.user!;
this.container.logger.info(`Successfully logged in as ${username} (${id})`); this.container.logger.info(`Successfully logged in as ${username} (${id})`);
const botLogChannel = await client.channels.fetch(IDs.channels.logs.bot);
if (botLogChannel === null) {
this.container.logger.error(
'ReadyListener: Could not find the channel for bot logs.',
);
return;
} else if (!botLogChannel.isSendable()) {
this.container.logger.info(
'ReadyListener: No permission to send in bots logs channel.',
);
return;
}
botLogChannel.send('The bot has started up!');
} }
} }

View File

@@ -28,7 +28,10 @@ import type {
import { ChannelType } from 'discord.js'; import { ChannelType } from 'discord.js';
import { fetchRoles, getLeaveRoles } from '#utils/database/dbExistingUser'; import { fetchRoles, getLeaveRoles } from '#utils/database/dbExistingUser';
import { blockTime } from '#utils/database/verification'; import { blockTime } from '#utils/database/verification';
import { checkActive, getSection } from '#utils/database/restriction'; import {
checkActive,
getSection,
} from '#utils/database/moderation/restriction';
import { blockedRoles, blockedRolesAfterRestricted } from '#utils/blockedRoles'; import { blockedRoles, blockedRolesAfterRestricted } from '#utils/blockedRoles';
import IDs from '#utils/ids'; import IDs from '#utils/ids';

View File

@@ -83,6 +83,11 @@ export class Suggestions extends Listener {
return; return;
} }
if (!mailbox.isSendable()) {
// TODO manage logging/errors properly
return;
}
const sent = await mailbox.send({ const sent = await mailbox.send({
embeds: [suggestion], embeds: [suggestion],
content: message.author.toString(), content: message.author.toString(),

View File

@@ -19,7 +19,7 @@
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
import type { VoiceState } from 'discord.js'; import type { VoiceState } from 'discord.js';
import { checkActive, removeMute } from '#utils/database/vcMute'; import { checkActive, removeMute } from '#utils/database/moderation/vcMute';
export class VCMuteListener extends Listener { export class VCMuteListener extends Listener {
public constructor( public constructor(

View File

@@ -50,7 +50,7 @@ import {
startVerification, startVerification,
finishVerification, finishVerification,
} from '#utils/database/verification'; } from '#utils/database/verification';
import { findNotes } from '#utils/database/sus'; import { findNotes } from '#utils/database/moderation/sus';
import { addExistingUser } from '#utils/database/dbExistingUser'; import { addExistingUser } from '#utils/database/dbExistingUser';
import { rolesToString } from '#utils/formatter'; import { rolesToString } from '#utils/formatter';
import IDs from '#utils/ids'; import IDs from '#utils/ids';

View File

@@ -148,7 +148,10 @@ export class VerificationLeaveVCListener extends Listener {
listTextChannels.forEach((c) => { listTextChannels.forEach((c) => {
const textChannel = c as TextChannel; const textChannel = c as TextChannel;
// Checks if the channel topic has the user's snowflake // Checks if the channel topic has the user's snowflake
if (textChannel.topic!.includes(userSnowflake!)) { if (
textChannel.topic !== null &&
textChannel.topic.includes(userSnowflake!)
) {
textChannel.delete(); textChannel.delete();
} }
}); });

View File

@@ -80,7 +80,10 @@ export class VerificationReady extends Listener {
const textChannel = c as TextChannel; const textChannel = c as TextChannel;
// Checks if the channel topic has the user's snowflake // Checks if the channel topic has the user's snowflake
emptyVC.forEach((snowflake) => { emptyVC.forEach((snowflake) => {
if (textChannel.topic!.includes(snowflake)) { if (
textChannel.topic !== null &&
textChannel.topic.includes(snowflake)
) {
textChannel.delete(); textChannel.delete();
} }
}); });

View File

@@ -0,0 +1,88 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2024 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Listener } from '@sapphire/framework';
import { GuildMember } from 'discord.js';
import IDs from '#utils/ids';
import { noModHistory, userPreviouslyHadRole } from '#utils/database/memberMod';
/**
* Gives the trusted role to users who have levelled up to level 5
* and has not gotten any other warnings/restrictions prior.
*/
export class TrustedListener extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, {
...options,
event: 'xpLevelUp',
});
}
public async run(member: GuildMember, level: number) {
// Checks if the member has gotten level 7
// Has been nefred. Should take around 1.5 hours to get the trusted role now
if (level !== 7) {
return;
}
// Checks if the user has been previously moderated
const noModerationHistory = await noModHistory(member.id);
if (!noModerationHistory) {
return;
}
const { guild } = member;
const trusted = guild.roles.cache.get(IDs.roles.trusted);
if (trusted === undefined) {
this.container.logger.error(
'TrustedXP Listener: the Trusted role could not be found in the guild.',
);
return;
}
// Checks if the member has previously had the trusted role given/removed
const previouslyHadRole = await userPreviouslyHadRole(
member.id,
trusted.id,
);
if (previouslyHadRole) {
return;
}
// Checks if the user already has the trusted role
if (member.roles.cache.has(trusted.id)) {
return;
}
// Gives the trusted role to the member
await member.roles.add(trusted);
// Send a DM to inform the member that they have been given the trusted role
await member.user.send(
`Hi, you have been given the ${trusted.name} as you have been interacting in ARA for a long enough time!` +
'\n\nThis role allows you to post attachments to the server and stream in VCs.' +
'\nMake sure that you follow the rules, especially by **not** posting anything **NSFW**, and **no animal products or consumption of animal products**.' +
`\n\nNot following these rules will result in the **immediate removal** of the ${trusted.name} role.`,
);
}
}

View File

@@ -19,7 +19,7 @@
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
import type { Message } from 'discord.js'; import type { Message } from 'discord.js';
import { addXp, checkCanAddXp } from '#utils/database/xp'; import { addXp, checkCanAddXp } from '#utils/database/fun/xp';
import { randint } from '#utils/maths'; import { randint } from '#utils/maths';
export class XpListener extends Listener { export class XpListener extends Listener {
@@ -46,6 +46,12 @@ export class XpListener extends Listener {
const xp = randint(15, 25); const xp = randint(15, 25);
await addXp(user.id, xp); const level = await addXp(user.id, xp);
// Emits that a user has leveled up
if (level !== null) {
this.container.logger.info('User is levelling up!');
this.container.client.emit('xpLevelUp', message.member, level);
}
} }
} }

View File

@@ -52,7 +52,9 @@ export class VerifyReminder extends ScheduledTask {
await channel.send( await channel.send(
"If you want to have the vegan or activist role, you'll need to do a voice verification. " + "If you want to have the vegan or activist role, you'll need to do a voice verification. " +
"To do this, hop into the 'Verification' voice channel." + "To do this, hop into the 'Verification' voice channel." +
"\n\nIf there aren't any verifiers available, you'll be disconnected, and you can rejoin later.", "\n\nIf there aren't any verifiers available, you'll be disconnected, and you can rejoin later." +
`\nAlternatively if you would like text verification, you can use \`/apply\` in <#${IDs.channels.nonVegan.vcText}> ` +
'to be able fill out a Vegan Verification form through the Appy Bot.',
); );
// Reset the total message counter to 0 // Reset the total message counter to 0

View File

@@ -20,8 +20,11 @@
import { ScheduledTask } from '@sapphire/plugin-scheduled-tasks'; import { ScheduledTask } from '@sapphire/plugin-scheduled-tasks';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { checkBan } from '#utils/database/ban'; import { checkBan } from '#utils/database/moderation/ban';
import { checkTempBan, removeTempBan } from '#utils/database/tempBan'; import {
checkTempBan,
removeTempBan,
} from '#utils/database/moderation/tempBan';
export class TempBan extends ScheduledTask { export class TempBan extends ScheduledTask {
public constructor( public constructor(

View File

@@ -36,7 +36,7 @@ export async function addXp(userId: Snowflake, xp: number) {
} }
} }
await container.database.xp.upsert({ const info = await container.database.xp.upsert({
where: { where: {
userId, userId,
}, },
@@ -63,6 +63,12 @@ export async function addXp(userId: Snowflake, xp: number) {
xpForNextLevel: xp, xpForNextLevel: xp,
}, },
}); });
if (level === 1) {
return info.level;
} else {
return null;
}
} }
export async function checkCanAddXp(userId: Snowflake) { export async function checkCanAddXp(userId: Snowflake) {

View File

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2024 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Snowflake } from 'discord.js';
import { countWarnings } from '#utils/database/moderation/warnings';
import { countRestrictions } from '#utils/database/moderation/restriction';
import { container } from '@sapphire/framework';
/**
* Checks if the user has
* @param userId Discord Snowflake of the user to check
* @return Boolean true if no prior moderation action
*/
export async function noModHistory(userId: Snowflake) {
const warnCount = await countWarnings(userId);
const restrictCount = await countRestrictions(userId);
return warnCount === 0 && restrictCount === 0;
}
/**
* Checks if the user has previously had a role given or taken away by a moderator.
* @param userId Discord Snowflake of the user to check
* @param roleId Snowflake of the role being checked for the user
* @return Boolean true if the user has had a moderator give/remove the specified role
*/
export async function userPreviouslyHadRole(
userId: Snowflake,
roleId: Snowflake,
) {
const count = await container.database.roleLog.count({
where: {
userId,
roleId,
},
});
return count !== 0;
}

View File

@@ -114,6 +114,19 @@ export async function getSection(userId: Snowflake) {
return restriction.section; return restriction.section;
} }
/**
* Returns the amount of restrictions a user has.
* @param userId Discord Snowflake of the user to check
* @return number The amount of restrictions the user has
*/
export async function countRestrictions(userId: Snowflake) {
return container.database.restrict.count({
where: {
userId,
},
});
}
// This is only for restrictions created with the old bot // This is only for restrictions created with the old bot
export async function unRestrictLegacy( export async function unRestrictLegacy(
userId: Snowflake, userId: Snowflake,

View File

@@ -61,3 +61,16 @@ export async function deleteWarning(warningId: number) {
}, },
}); });
} }
/**
* Returns the amount of warnings a user has.
* @param userId Discord Snowflake of the user to check
* @return number The amount of warnings the user has
*/
export async function countWarnings(userId: Snowflake) {
return container.database.warning.count({
where: {
userId,
},
});
}

View File

@@ -18,6 +18,7 @@
*/ */
const devIDs = { const devIDs = {
guild: '999431674972618792',
roles: { roles: {
trusted: '999431675081666599', trusted: '999431675081666599',
booster: '', booster: '',
@@ -100,6 +101,7 @@ const devIDs = {
}, },
nonVegan: { nonVegan: {
general: '999431677325615189', general: '999431677325615189',
vcText: '999431677535338567',
}, },
vegan: { vegan: {
general: '999431677535338575', general: '999431677535338575',
@@ -125,6 +127,7 @@ const devIDs = {
}, },
logs: { logs: {
restricted: '999431681217937513', restricted: '999431681217937513',
bot: '999431681217937516',
economy: '999431681599623198', economy: '999431681599623198',
sus: '999431681599623199', sus: '999431681599623199',
}, },
@@ -138,6 +141,7 @@ const devIDs = {
private: '999431679527628818', private: '999431679527628818',
restricted: '999431679812845654', restricted: '999431679812845654',
}, },
modMail: '575252669443211264',
}; };
export default devIDs; export default devIDs;

View File

@@ -18,9 +18,9 @@
*/ */
import type { Guild, User } from 'discord.js'; import type { Guild, User } from 'discord.js';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import type { SusNotes } from '#utils/database/sus'; import type { SusNotes } from '#utils/database/moderation/sus';
import { RestrictionLogs } from '#utils/database/restriction'; import { RestrictionLogs } from '#utils/database/moderation/restriction';
import { Warnings } from '#utils/database/warnings'; import { Warnings } from '#utils/database/moderation/warnings';
export function createSusLogEmbed(notes: SusNotes, user: User, guild: Guild) { export function createSusLogEmbed(notes: SusNotes, user: User, guild: Guild) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()

View File

@@ -20,34 +20,35 @@
import devIDs from '#utils/devIDs'; import devIDs from '#utils/devIDs';
let IDs = { let IDs = {
guild: '730907954345279591',
roles: { roles: {
trusted: '731563158011117590', trusted: '1329089675977035879',
booster: '731213264540795012', booster: '731213264540795012',
nonvegan: { nonvegan: {
nonvegan: '774763753308815400', nonvegan: '1329093962153332848',
vegCurious: '832656046572961803', vegCurious: '1329107984227369020',
convinced: '797132019166871612', convinced: '797132019166871612',
}, },
vegan: { vegan: {
vegan: '788114978020392982', vegan: '788114978020392982',
activist: '730915638746546257', activist: '1329112833115295815',
nvAccess: '1076857105648209971', nvAccess: '1076857105648209971',
plus: '798682625619132428', plus: '798682625619132428',
araVegan: '995394977658044506', araVegan: '995394977658044506',
}, },
restrictions: { restrictions: {
sus: '859145930640457729', sus: '1329125130949103626',
muted: '730924813681688596', muted: '730924813681688596',
softMute: '775934741139554335', softMute: '775934741139554335',
restricted1: '809769217477050369', restricted1: '809769217477050369',
restricted2: '872482843304001566', restricted2: '872482843304001566',
restricted3: '856582673258774538', restricted3: '1329126085207789658',
restricted4: '872472182888992858', restricted4: '1329126181164945499',
restricted: [ restricted: [
'809769217477050369', // Restricted 1 '809769217477050369', // Restricted 1
'872482843304001566', // Restricted 2 '872482843304001566', // Restricted 2
'856582673258774538', // Restricted 3 '1329126085207789658', // Restricted 3
'872472182888992858', // Restricted 4 '1329126181164945499', // Restricted 4
'1075951477379567646', // Restricted Vegan '1075951477379567646', // Restricted Vegan
], ],
}, },
@@ -75,7 +76,7 @@ let IDs = {
stageHost: '854893757593419786', stageHost: '854893757593419786',
patron: '765370219207852055', patron: '765370219207852055',
patreon: '993848684640997406', patreon: '993848684640997406',
verifyBlock: '1032765019269640203', verifyBlock: '1329107805130461247',
bookClub: '955516408249352212', bookClub: '955516408249352212',
debateHost: '935508325615931443', debateHost: '935508325615931443',
gameNightHost: '952779915701415966', gameNightHost: '952779915701415966',
@@ -102,6 +103,7 @@ let IDs = {
}, },
nonVegan: { nonVegan: {
general: '798967615636504657', general: '798967615636504657',
vcText: '808191982169096232',
}, },
vegan: { vegan: {
general: '787738272616808509', general: '787738272616808509',
@@ -127,6 +129,7 @@ let IDs = {
}, },
logs: { logs: {
restricted: '920993034462715925', restricted: '920993034462715925',
bot: '872126272015314966',
economy: '932050015034159174', economy: '932050015034159174',
sus: '872884989950324826', sus: '872884989950324826',
}, },
@@ -140,6 +143,7 @@ let IDs = {
private: '992581296901599302', private: '992581296901599302',
restricted: '809765577236283472', restricted: '809765577236283472',
}, },
modMail: '575252669443211264',
}; };
// Check if the bot is in development mode // Check if the bot is in development mode