90 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
Anthony Berg
325dc0d0d0 refactor(arabot): run prettier 2024-08-04 20:14:44 +02:00
Anthony Berg
71a065d3ca feat(arabot): mention that vegans can access extra channels if verified 2024-08-04 20:05:24 +02:00
Anthony Berg
613f53491b build: update pnpm deps 2024-08-04 20:04:40 +02:00
Anthony
19721c10ea build(arabot): update deps 2024-06-27 11:23:09 +02:00
Anthony Berg
bd87a8b6c6 ci: add tsconfig and pnpm-lock to .prettierignore 2024-03-12 21:37:52 +00:00
Anthony Berg
46ef2fd8e2 fix(arabot): adding sus note to user who left server 2024-03-12 21:35:19 +00:00
Anthony Berg
d8c91fd39b refactor: run prettier 2024-03-12 21:34:51 +00:00
Anthony Berg
fabd381051 deps: update packages 2024-03-12 21:34:18 +00:00
Anthony Berg
a5758dc6ef feat(arabot): add vegan check to plus command 2024-02-16 21:28:18 +00:00
Anthony Berg
9ff5b78aff feat(arabot): allow verifiers to use plus command 2024-02-15 19:57:51 +00:00
Anthony Berg
f4655829e2 feat(arabot): make vcs when creating groups in outreach 2024-02-13 21:36:41 +00:00
Anthony Berg
c82d256be4 refactor(arabot): change outreach command for outreach leader only 2024-02-13 19:31:49 +00:00
Anthony Berg
a9039572d1 build: update deps 2024-02-07 20:39:39 +00:00
Anthony
2cf7998cd9 feat(arabot): add hr and mentor coordinators private channels 2024-02-07 15:03:26 +00:00
Anthony Berg
63a4d651af build: change ioredis to dev dep 2024-02-03 22:45:39 +00:00
Anthony Berg
a2dba859f2 build: fix prisma generate in Dockerfile 2024-02-03 22:39:26 +00:00
Anthony Berg
a7b772f77a build: update deps 2024-02-03 22:18:11 +00:00
Anthony Berg
1fa87b8a4a Merge remote-tracking branch 'origin/main'
# Conflicts:
#	package-lock.json
#	package.json
2024-02-03 22:14:50 +00:00
Anthony Berg
4e99a5456f build: change to pnpm 2024-02-03 22:13:35 +00:00
Anthony
62d941dfcb feat(arabot): add better checks for types for temp bans 2024-01-27 19:46:31 +01:00
Anthony
e6b1463a1a refactor: run prettier 2024-01-27 19:46:17 +01:00
Anthony
5793bbb461 fix(arabot): catch conditions for fetching guild and user 2024-01-27 19:41:22 +01:00
Anthony
cf8142b86a fix(arabot): deleted channel errors 2024-01-27 19:38:59 +01:00
Anthony
502d5c5cdf ci: update deps 2024-01-27 19:27:19 +01:00
Anthony Berg
8f071b8043 Merge pull request #187 from veganhacktivists/renovate/node-20.x-lockfile
fix(deps): update dependency @types/node to v20.11.8
2024-01-27 18:04:39 +00:00
Anthony Berg
36ce086532 Merge pull request #188 from veganhacktivists/renovate/bullmq-5.x-lockfile
fix(deps): update dependency bullmq to v5.1.5
2024-01-27 18:04:24 +00:00
Anthony Berg
617834833a Merge pull request #189 from veganhacktivists/renovate/prisma-monorepo
fix(deps): update prisma monorepo to v5.8.1
2024-01-27 18:04:15 +00:00
renovate[bot]
4d92250500 fix(deps): update prisma monorepo to v5.8.1 2024-01-27 17:59:09 +00:00
renovate[bot]
f898dada56 fix(deps): update dependency @types/node to v20.11.8 2024-01-27 17:58:51 +00:00
renovate[bot]
9e2f2c7558 fix(deps): update dependency bullmq to v5.1.5 2024-01-27 14:03:25 +00:00
Anthony Berg
e15e5da5aa Merge pull request #186 from veganhacktivists/renovate/sapphire-time-utilities-1.x-lockfile
fix(deps): update dependency @sapphire/time-utilities to v1.7.12
2024-01-27 14:02:47 +00:00
Anthony Berg
e89f056b94 Merge pull request #185 from veganhacktivists/renovate/typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to v6.19.1
2024-01-27 14:02:27 +00:00
renovate[bot]
6acd012e7a fix(deps): update dependency @sapphire/time-utilities to v1.7.12 2024-01-27 13:50:08 +00:00
renovate[bot]
75174f711d chore(deps): update typescript-eslint monorepo to v6.19.1 2024-01-27 13:49:56 +00:00
Anthony
5e69ea6126 fix(arabot): grammar in welcome message 2024-01-24 15:43:45 +00:00
Anthony Berg
3d8aba5577 Merge pull request #182 from veganhacktivists/renovate/sapphire-plugin-logger-4.x-lockfile
fix(deps): update dependency @sapphire/plugin-logger to v4.0.2
2024-01-21 23:36:36 +00:00
Anthony Berg
e7839552f8 Merge pull request #183 from veganhacktivists/renovate/sapphire-plugin-scheduled-tasks-10.x-lockfile
fix(deps): update dependency @sapphire/plugin-scheduled-tasks to v10.0.1
2024-01-21 23:36:26 +00:00
Anthony Berg
3b0666e80d Merge pull request #184 from veganhacktivists/renovate/sapphire-plugin-subcommands-6.x-lockfile
fix(deps): update dependency @sapphire/plugin-subcommands to v6.0.3
2024-01-21 23:36:17 +00:00
Anthony Berg
3d8a9be7f2 Merge pull request #180 from veganhacktivists/renovate/node-20.x-lockfile
fix(deps): update dependency @types/node to v20.11.5
2024-01-21 23:35:58 +00:00
Anthony Berg
a7f608c1f0 Merge pull request #179 from veganhacktivists/renovate/prettier-3.x
chore(deps): update dependency prettier to v3.2.4
2024-01-21 23:35:48 +00:00
Anthony Berg
cb457136d4 Merge pull request #178 from veganhacktivists/renovate/sapphire-framework-5.x-lockfile
fix(deps): update dependency @sapphire/framework to v5.0.7
2024-01-21 23:35:40 +00:00
Anthony Berg
7e984c4857 Merge pull request #177 from veganhacktivists/renovate/typescript-eslint-monorepo
chore(deps): update typescript-eslint monorepo to v6.19.0
2024-01-21 23:35:32 +00:00
Anthony Berg
0ea9ea3f64 feat(arabot): remove Patreon precondition 2024-01-21 20:11:21 +00:00
renovate[bot]
19e70ebdbc fix(deps): update dependency @sapphire/plugin-subcommands to v6.0.3 2024-01-20 16:17:44 +00:00
renovate[bot]
5409be3d75 fix(deps): update dependency @sapphire/plugin-scheduled-tasks to v10.0.1 2024-01-20 16:17:37 +00:00
renovate[bot]
edd3caf9c0 fix(deps): update dependency @sapphire/plugin-logger to v4.0.2 2024-01-20 14:29:26 +00:00
renovate[bot]
5a87f97a74 fix(deps): update dependency @sapphire/framework to v5.0.7 2024-01-19 21:34:24 +00:00
renovate[bot]
feab05c1ea chore(deps): update dependency prettier to v3.2.4 2024-01-17 11:41:50 +00:00
renovate[bot]
9b505fbece fix(deps): update dependency @types/node to v20.11.5 2024-01-17 07:25:22 +00:00
renovate[bot]
a5ba493372 chore(deps): update typescript-eslint monorepo to v6.19.0 2024-01-15 18:20:21 +00:00
Anthony Berg
98e514b5e9 fix(arabot): not giving roles back to server boosters 2024-01-13 02:06:25 +00:00
Anthony Berg
172508c741 feat(arabot): remove unholy fun command 2024-01-13 01:36:43 +00:00
Anthony Berg
730f3e6a28 feat(arabot): add fun listener for "bad" words 2024-01-13 01:17:34 +00:00
Anthony Berg
fbc2944b92 fix(arabot): 1984 preconditions 2024-01-13 01:07:20 +00:00
Anthony Berg
6172dc6ac6 feat(arabot): add logging for sus note purges 2024-01-13 00:57:14 +00:00
Anthony Berg
b762ae3bc8 feat(arabot): add logging for one sus note removal 2024-01-13 00:52:54 +00:00
Anthony Berg
c8eb8299dd refactor(arabot): change removing sus notes to have more checks for types 2024-01-13 00:29:57 +00:00
Anthony Berg
785e844da8 feat(arabot): sus note logging for added sus note 2024-01-13 00:11:06 +00:00
82 changed files with 2936 additions and 4908 deletions

View File

@@ -1,6 +1,6 @@
.idea .idea
dist dist
node_modules node_modules
tsconfig.tsbuildinfo
npm-debug.log npm-debug.log
.env .env
*.md

View File

@@ -3,16 +3,18 @@ DISCORD_TOKEN= # Bot token from: https://discord.com/developers/
# Configuration # Configuration
DEFAULT_PREFIX= # Prefix used to run commands in Discord DEFAULT_PREFIX= # Prefix used to run commands in Discord
DEVELOPMENT= # (true/false) Enables developer mode DEVELOPMENT= # (true/false) Enables developer mode
# Docker # Docker
POSTGRES_USER=USERNAME 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
BULLMQ_URL # URL for redis database, but without redis:// and credentials 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"

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
node-linker=hoisted
shamefully-hoist=true
public-hoist-pattern[]=@sapphire/*

View File

@@ -1,2 +1,4 @@
dist dist
node_modules node_modules
tsconfig.json
pnpm-lock.yaml

View File

@@ -1,24 +1,26 @@
FROM node:20 FROM node:22 AS base
# PNPM
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /opt/app COPY . /app
WORKDIR /app
ENV NODE_ENV=production FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
COPY --chown=node:node package.json . FROM base AS build
COPY --chown=node:node package-lock.json . RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY --chown=node:node tsconfig.json . RUN pnpm exec prisma generate
COPY --chown=node:node prisma ./prisma/ RUN pnpm run build
RUN npm install FROM base
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma
COPY --from=build /app/dist /app/dist
COPY . . RUN chown node:node .
RUN npx prisma generate
RUN npm run build
RUN chown node:node /opt/app/
USER node USER node
CMD [ "pnpm", "run", "start:migrate"]
CMD [ "npm", "run", "start:migrate"]

5
nixpacks.toml Normal file
View File

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

4556
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,11 @@
"description": "A Discord bot for Animal Rights Advocates", "description": "A Discord bot for Animal Rights Advocates",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm",
"build": "tsc", "build": "tsc",
"cleanBuild": "rm -rf ./dist && tsc", "cleanBuild": "rm -rf ./dist && tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
"start:migrate": "prisma migrate deploy && npm run start" "start:migrate": "prisma migrate deploy && pnpm run start"
}, },
"imports": { "imports": {
"#utils/*": "./dist/utils/*.js" "#utils/*": "./dist/utils/*.js"
@@ -28,31 +29,35 @@
"url": "https://github.com/veganhacktivists/arabot/issues" "url": "https://github.com/veganhacktivists/arabot/issues"
}, },
"homepage": "https://github.com/veganhacktivists/arabot#readme", "homepage": "https://github.com/veganhacktivists/arabot#readme",
"engines": {
"node": ">=20",
"pnpm": ">=9"
},
"dependencies": { "dependencies": {
"@prisma/client": "^5.7.1", "@prisma/client": "^5.22.0",
"@sapphire/discord.js-utilities": "^7.1.5", "@sapphire/discord.js-utilities": "^7.3.2",
"@sapphire/framework": "^5.0.5", "@sapphire/framework": "^5.3.2",
"@sapphire/plugin-logger": "^4.0.1", "@sapphire/plugin-logger": "^4.0.2",
"@sapphire/plugin-scheduled-tasks": "^10.0.0", "@sapphire/plugin-scheduled-tasks": "^10.0.2",
"@sapphire/plugin-subcommands": "^6.0.2", "@sapphire/plugin-subcommands": "^6.0.3",
"@sapphire/stopwatch": "^1.5.1", "@sapphire/stopwatch": "^1.5.4",
"@sapphire/time-utilities": "^1.7.11", "@sapphire/time-utilities": "^1.7.14",
"@sapphire/ts-config": "^5.0.0", "@sapphire/ts-config": "^5.0.1",
"@sapphire/utilities": "^3.15.1", "@sapphire/utilities": "^3.18.1",
"@types/node": "^20.10.6", "bullmq": "^5.34.10",
"bullmq": "^5.1.1", "discord.js": "^14.17.3",
"discord.js": "^14.14.1", "ioredis": "^5.4.2",
"redis": "^4.6.12", "ts-node": "^10.9.2",
"ts-node": "^10.9.1", "typescript": "~5.4.5"
"typescript": "^5.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/ioredis": "^5.0.0", "@types/node": "^20.17.13",
"@typescript-eslint/eslint-plugin": "^6.17.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.17.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.1.1", "prettier": "3.2.4",
"prisma": "^5.7.1" "prisma": "^5.22.0"
} },
"packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"
} }

1899
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `channelId` to the `StatRole` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "StatRole" ADD COLUMN "channelId" TEXT NOT NULL;

View File

@@ -222,9 +222,10 @@ model Stat {
} }
model StatRole { model StatRole {
stat Stat @relation(fields: [statId], references: [id]) stat Stat @relation(fields: [statId], references: [id])
statId Int @id statId Int @id
roleId String roleId String
channelId String
} }
model ParticipantStat { model ParticipantStat {

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

@@ -356,6 +356,12 @@ export class PrivateCommand extends Subcommand {
} else if (user.roles.cache.has(IDs.roles.staff.eventCoordinator)) { } else if (user.roles.cache.has(IDs.roles.staff.eventCoordinator)) {
name = 'event'; name = 'event';
id = IDs.roles.staff.eventCoordinator; id = IDs.roles.staff.eventCoordinator;
} else if (user.roles.cache.has(IDs.roles.staff.mediaCoordinator)) {
name = 'media';
id = IDs.roles.staff.mediaCoordinator;
} else if (user.roles.cache.has(IDs.roles.staff.hrCoordinator)) {
name = 'hr';
id = IDs.roles.staff.hrCoordinator;
} else { } else {
name = 'coordinator'; name = 'coordinator';
id = IDs.roles.staff.coordinator; id = IDs.roles.staff.coordinator;

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) {
@@ -28,7 +28,7 @@ export class N1984Command extends Command {
...options, ...options,
name: '1984', name: '1984',
description: 'this is literally 1984', description: 'this is literally 1984',
preconditions: ['CoordinatorOnly', 'ModOnly'], preconditions: [['CoordinatorOnly', 'ModOnly']],
}); });
} }

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

@@ -27,7 +27,6 @@ export class HappyCommand extends Command {
...options, ...options,
name: 'happy', name: 'happy',
description: 'Express your happiness', description: 'Express your happiness',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }

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

@@ -27,7 +27,6 @@ export class SadCommand extends Command {
...options, ...options,
name: 'sad', name: 'sad',
description: 'Express your sadness', description: 'Express your sadness',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }

View File

@@ -27,7 +27,6 @@ export class ShrugCommand extends Command {
...options, ...options,
name: 'shrug', name: 'shrug',
description: 'Ugh... whatever... idk...', description: 'Ugh... whatever... idk...',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }

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

@@ -25,17 +25,21 @@ import {
ButtonBuilder, ButtonBuilder,
ButtonInteraction, ButtonInteraction,
ButtonStyle, ButtonStyle,
User,
Guild,
TextChannel,
GuildMember,
Snowflake,
MessageFlagsBitField,
} from 'discord.js'; } from 'discord.js';
import type { Message, GuildMember } from 'discord.js'; import type { Message } from 'discord.js';
import { isMessageInstance } from '@sapphire/discord.js-utilities';
import { addExistingUser } from '#utils/database/dbExistingUser';
import { import {
addToDatabase, 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';
@@ -54,8 +58,8 @@ export class SusCommand extends Subcommand {
{ {
name: 'add', name: 'add',
default: true, default: true,
chatInputRun: 'addNote', chatInputRun: 'addNoteChatInput',
messageRun: 'addMessage', messageRun: 'addNoteMessage',
}, },
{ {
name: 'view', name: 'view',
@@ -143,7 +147,9 @@ export class SusCommand extends Subcommand {
} }
// Subcommand to add sus note // Subcommand to add sus note
public async addNote(interaction: Subcommand.ChatInputCommandInteraction) { public async addNoteChatInput(
interaction: Subcommand.ChatInputCommandInteraction,
) {
// Get the arguments // Get the arguments
const user = interaction.options.getUser('user', true); const user = interaction.options.getUser('user', true);
const note = interaction.options.getString('note', true); const note = interaction.options.getString('note', true);
@@ -154,40 +160,127 @@ 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;
} }
const info = await this.addNote(user, mod, note, guild);
await interaction.reply({
content: info.message,
flags: MessageFlagsBitField.Flags.Ephemeral,
});
}
// Non Application Command method of adding a sus note
public async addNoteMessage(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const note = args.finished ? null : await args.rest('string');
const mod = message.author;
if (note === null) {
await message.react('❌');
await message.reply('No sus note was provided!');
return;
}
const guild = message.guild;
if (guild === null) {
await message.react('❌');
await message.reply(
'Could not find guild! Make sure you run this command in a server.',
);
return;
}
const info = await this.addNote(user, mod, note, guild);
if (!info.success) {
await message.react('❌');
return;
}
await message.react('✅');
}
private async addNote(user: User, mod: User, note: string, guild: Guild) {
const info = {
message: '',
success: false,
};
// Add the data to the database // Add the data to the database
await addSusNoteDB(user.id, mod.id, note);
// Check if the user exists on the database // Gives the sus role to the user
const member = guild.members.cache.get(user.id); await this.addSusRole(user, guild);
const modMember = guild.members.cache.get(mod.id);
if (member === undefined || modMember === undefined) { info.message = `Added the sus note for ${user}: ${note}`;
await interaction.reply({ info.success = true;
content: 'Error fetching users!',
ephemeral: true, // Log the sus note
}); let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as
return; | TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(IDs.channels.logs.sus)) as
| TextChannel
| undefined;
if (logChannel === undefined) {
this.container.logger.error('Sus Error: Could not fetch log channel');
info.message = `Added a sus note for ${user} but could not find the log channel. This has been logged to the database.`;
return info;
}
} }
// Check if user and mod are on the database const message = new EmbedBuilder()
await addExistingUser(member); .setColor('#0099ff')
await addExistingUser(modMember); .setAuthor({
name: `Added sus note for ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Note', value: note },
)
.setTimestamp()
.setFooter({ text: `ID: ${user.id}` });
await addToDatabase(user.id, mod.id, note); await logChannel.send({ embeds: [message] });
return info;
}
private async addSusRole(user: User, guild: Guild) {
// Get GuildMember for user to add a sus note for
let member = guild.members.cache.get(user.id);
// Checks if Member was not found in cache
if (member === undefined) {
// Fetches Member from API call to Discord
member = await guild.members.fetch(user.id).catch(() => undefined);
}
if (member === undefined) {
return;
}
// Give the user the sus role they don't already have the sus note // Give the user the sus role they don't already have the sus note
if (!member.roles.cache.has(IDs.roles.restrictions.sus)) { if (!member.roles.cache.has(IDs.roles.restrictions.sus)) {
await member.roles.add(IDs.roles.restrictions.sus); await member.roles.add(IDs.roles.restrictions.sus);
} }
await interaction.reply({
content: `${user} note: ${note}`,
ephemeral: true,
});
} }
public async listNote(interaction: Subcommand.ChatInputCommandInteraction) { public async listNote(interaction: Subcommand.ChatInputCommandInteraction) {
@@ -199,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;
} }
@@ -213,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;
} }
@@ -225,22 +318,23 @@ 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,
}); });
} }
public async removeNote(interaction: Subcommand.ChatInputCommandInteraction) { public async removeNote(interaction: Subcommand.ChatInputCommandInteraction) {
// Get the arguments // Get the arguments
const noteId = interaction.options.getInteger('id', true); const noteId = interaction.options.getInteger('id', true);
const mod = interaction.user;
const { guild, channel } = interaction; const { guild, channel } = interaction;
// Checks if all the variables are of the right type // Checks if all the variables are of the right type
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;
} }
@@ -252,45 +346,46 @@ 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;
} }
const userId = note.userId;
const modId = note.modId;
// Get user GuildMembers for user and mod and person who ran command // Get user GuildMembers for user and mod and person who ran command
const member = await guild.members.cache.get(note.userId); let user = guild.client.users.cache.get(userId);
const mod = await guild.members.cache.get(note.modId); if (!(user instanceof User)) {
user = await guild.client.users.fetch(userId).catch(() => undefined);
// TODO fix if user left the server }
if (member === undefined || mod === undefined) { if (user === undefined) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching users from Discord!', content: 'Error fetching user!',
ephemeral: true, flags: MessageFlagsBitField.Flags.Ephemeral,
fetchReply: true, withResponse: true,
}); });
return; return;
} }
// Get user's name let modCreator = guild.client.users.cache.get(modId);
let userName = note.userId; if (!(modCreator instanceof User)) {
if (member !== undefined) { modCreator = await guild.client.users.fetch(modId).catch(() => undefined);
userName = member.displayName;
} }
// Get mod name let modCreatorDisplay = modId;
let modName = note.modId; if (modCreator instanceof User) {
if (mod !== undefined) { modCreatorDisplay = modCreator.displayName;
modName = mod.displayName;
} }
// Create an embed for the note // Create an embed for the note
const noteEmbed = new EmbedBuilder() const noteEmbed = new EmbedBuilder()
.setColor('#ff0000') .setColor('#ff0000')
.setTitle(`Sus note for ${userName}`) .setTitle(`Sus note for ${user.tag}`)
.setThumbnail(member.displayAvatarURL()) .setThumbnail(user.displayAvatarURL())
.addFields({ .addFields({
name: `ID: ${noteId} | Moderator: ${modName} | Date: <t:${Math.floor( name: `ID: ${noteId} | Moderator: ${modCreatorDisplay} | Date: <t:${Math.floor(
note.time.getTime() / 1000, note.time.getTime() / 1000,
)}>`, )}>`,
value: note.note, value: note.note,
@@ -312,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
@@ -333,18 +433,28 @@ export class SusCommand extends Subcommand {
if (button.customId === `delete${noteId}`) { if (button.customId === `delete${noteId}`) {
await deactivateNote(noteId); await deactivateNote(noteId);
await interaction.editReply({ await interaction.editReply({
content: `${member}'s sus note (ID: ${noteId}) has been successfully removed`, content: `${user}'s sus note (ID: ${noteId}) has been successfully removed`,
embeds: [], embeds: [],
}); });
// TODO create a new Prisma function to only count and not to get a whole list of sus notes // TODO create a new Prisma function to only count and not to get a whole list of sus notes
// Check how many notes the user has and if 0, then remove sus note // Check how many notes the user has and if 0, then remove sus note
const notes = await findNotes(member.id, true); const notes = await findNotes(userId, true);
// Checks if there are no notes on the user and if there's none, remove the sus role // Checks if there are no notes on the user and if there's none, remove the sus role
if (notes.length === 0) { if (notes.length === 0) {
await member.roles.remove(IDs.roles.restrictions.sus); let member = guild.members.cache.get(userId);
if (!(member instanceof GuildMember)) {
member = await guild.members.fetch(userId).catch(() => undefined);
}
if (member instanceof GuildMember) {
await member.roles.remove(IDs.roles.restrictions.sus);
}
} }
// Logs the removal of the sus note
await this.deleteNoteLogger(userId, mod, noteId, guild);
} }
}); });
@@ -356,19 +466,66 @@ export class SusCommand extends Subcommand {
}); });
} }
// Logs removal of 1 sus note
private async deleteNoteLogger(
userId: Snowflake,
mod: User,
noteId: number,
guild: Guild,
) {
// Find user
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId).catch(() => undefined);
}
if (user === undefined) return;
// Log the sus note
let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(IDs.channels.logs.sus)) as
| TextChannel
| undefined;
if (logChannel === undefined) {
this.container.logger.error('Sus Error: Could not fetch log channel');
return;
}
}
const embed = new EmbedBuilder()
.setColor('#28A745')
.setAuthor({
name: `Removed sus note for ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Note ID', value: `${noteId}`, inline: true },
)
.setTimestamp()
.setFooter({ text: `ID: ${user.id}` });
await logChannel.send({ embeds: [embed] });
}
public async removeAllNotes( public async removeAllNotes(
interaction: Subcommand.ChatInputCommandInteraction, interaction: Subcommand.ChatInputCommandInteraction,
) { ) {
// Get the arguments // Get the arguments
const user = interaction.options.getUser('user', true); const user = interaction.options.getUser('user', true);
const mod = interaction.user;
const { guild, channel } = interaction; const { guild, channel } = interaction;
// Checks if all the variables are of the right type // Checks if all the variables are of the right type
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;
} }
@@ -379,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;
} }
@@ -393,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;
} }
@@ -444,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
@@ -470,6 +632,8 @@ export class SusCommand extends Subcommand {
embeds: [], embeds: [],
}); });
} }
await this.deleteAllNotesLogger(user, mod, guild);
}); });
// Remove the buttons after they have been clicked // Remove the buttons after they have been clicked
@@ -483,46 +647,36 @@ export class SusCommand extends Subcommand {
await member.roles.remove(IDs.roles.restrictions.sus); await member.roles.remove(IDs.roles.restrictions.sus);
} }
// Non Application Command method of adding a sus note // Logs removal of 1 sus note
// xlevra begged me to add this... so I guess here it is private async deleteAllNotesLogger(user: User, mod: User, guild: Guild) {
public async addMessage(message: Message, args: Args) { // Log the sus note
// Get arguments let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as
let user: GuildMember; | TextChannel
try { | undefined;
user = await args.pick('member');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const note = args.finished ? null : await args.rest('string');
const mod = message.member;
if (note === null) { if (logChannel === undefined) {
await message.react('❌'); logChannel = (await guild.channels.fetch(IDs.channels.logs.sus)) as
await message.reply('No sus note was provided!'); | TextChannel
return; | undefined;
if (logChannel === undefined) {
this.container.logger.error('Sus Error: Could not fetch log channel');
return;
}
} }
if (mod === null) { const embed = new EmbedBuilder()
await message.react('❌'); .setColor('#28A745')
await message.reply( .setAuthor({
'Moderator not found! Try again or contact a developer!', name: `Purged all sus notes for ${user.tag}`,
); iconURL: `${user.displayAvatarURL()}`,
return; })
} .addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
)
.setTimestamp()
.setFooter({ text: `ID: ${user.id}` });
// Check if user and mod are on the database await logChannel.send({ embeds: [embed] });
await addExistingUser(user);
await addExistingUser(mod);
await addToDatabase(user.id, mod.id, note);
// Give the user the sus role they don't already have the sus note
if (!user.roles.cache.has(IDs.roles.restrictions.sus)) {
await user.roles.add(IDs.roles.restrictions.sus);
}
await message.react('✅');
} }
} }

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

@@ -19,7 +19,7 @@
import { Subcommand } from '@sapphire/plugin-subcommands'; import { Subcommand } from '@sapphire/plugin-subcommands';
import { RegisterBehavior } from '@sapphire/framework'; import { RegisterBehavior } from '@sapphire/framework';
import type { Snowflake } from 'discord.js'; import { ChannelType, PermissionsBitField, Snowflake } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser'; import { updateUser } from '#utils/database/dbExistingUser';
import { import {
addStatUser, addStatUser,
@@ -66,7 +66,6 @@ export class OutreachCommand extends Subcommand {
], ],
}, },
], ],
preconditions: ['ModOnly'],
}); });
} }
@@ -200,15 +199,15 @@ export class OutreachCommand extends Subcommand {
if (mod === undefined) { if (mod === undefined) {
await interaction.reply({ await interaction.reply({
content: 'Mod was not found!', content: 'Outreach Leader was not found!',
ephemeral: true, ephemeral: true,
}); });
return; return;
} }
if (!mod.roles.cache.has(IDs.roles.staff.outreachCoordinator)) { if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) {
await interaction.reply({ await interaction.reply({
content: 'You need to be an Outreach Coordinator to run this command!', content: 'You need to be an Outreach Leader to run this command!',
ephemeral: true, ephemeral: true,
}); });
return; return;
@@ -254,9 +253,9 @@ export class OutreachCommand extends Subcommand {
return; return;
} }
if (!mod.roles.cache.has(IDs.roles.staff.outreachCoordinator)) { if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) {
await interaction.reply({ await interaction.reply({
content: 'You need to be an Outreach Coordinator to run this command!', content: 'You need to be an Outreach Leader to run this command!',
ephemeral: true, ephemeral: true,
}); });
return; return;
@@ -275,7 +274,8 @@ export class OutreachCommand extends Subcommand {
stat.forEach(({ role }) => { stat.forEach(({ role }) => {
if (role !== null) { if (role !== null) {
guild.roles.delete(role.roleId); guild.roles.delete(role.roleId); // Delete role
guild.channels.delete(role.channelId); // Delete VC
} }
}); });
@@ -388,14 +388,66 @@ export class OutreachCommand extends Subcommand {
await updateUser(leaderMember); await updateUser(leaderMember);
// Create role for group
const role = await guild.roles.create({ const role = await guild.roles.create({
name: `Outreach Group ${groupNo}`, name: `Outreach Group ${groupNo}`,
mentionable: true,
}); });
await createStat(event.id, leader.id, role.id); // Create a voice channel for group
const channel = await guild.channels.create({
name: `Outreach Group ${groupNo}`,
type: ChannelType.GuildVoice,
parent: IDs.categories.activism,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
PermissionsBitField.Flags.ViewChannel,
],
},
{
id: IDs.roles.vegan.activist,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: role.id, // Permissions for the specific group
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
],
},
{
id: IDs.roles.staff.outreachLeader,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
],
},
],
});
// Create stats in database
await createStat(event.id, leader.id, role.id, channel.id);
// Give group leader role
await leaderMember.roles.add(role); await leaderMember.roles.add(role);
// Send message in VC with a welcome and reminder
await channel.send(
`Welcome ${role}, ${leaderMember} is going to be the leader of your group!\n\n` +
'Remember to keep track of stats during activism with `/outreach group update` and' +
'to have these questions in mind whilst doing activism:\n' +
'- How many said would go vegan?\n' +
'- How many seriously considered being vegan?\n' +
'- How many people had anti-vegan viewpoints?\n' +
'- How many thanked you for the conversation?\n' +
'- How many said they would watch a vegan documentary?\n' +
'- How many got educated on veganism or the animal industry?',
);
await interaction.editReply({ await interaction.editReply({
content: `Created a group with the leader being ${leader}`, content: `Created a group with the leader being ${leader}`,
}); });
@@ -442,7 +494,7 @@ export class OutreachCommand extends Subcommand {
if ( if (
leader.id !== stat.stat.leaderId && leader.id !== stat.stat.leaderId &&
!leaderMember.roles.cache.has(IDs.roles.staff.outreachCoordinator) !leaderMember.roles.cache.has(IDs.roles.staff.outreachLeader)
) { ) {
await interaction.editReply({ await interaction.editReply({
content: `You are not the leader for ${group}`, content: `You are not the leader for ${group}`,

View File

@@ -29,7 +29,7 @@ export class PlusCommand extends Command {
name: 'plus', name: 'plus',
aliases: ['+'], aliases: ['+'],
description: 'Give/remove the plus role', description: 'Give/remove the plus role',
preconditions: [['CoordinatorOnly', 'ModOnly']], preconditions: [['CoordinatorOnly', 'VerifierOnly', 'ModOnly']],
}); });
} }
@@ -138,6 +138,14 @@ export class PlusCommand extends Command {
info.success = true; info.success = true;
return info; return info;
} }
// Checks if the user is vegan before giving the plus role
// If not, stop from giving the plus role
if (!member.roles.cache.has(IDs.roles.vegan.vegan)) {
info.message = `Can't give ${user} the vegan role as they are not vegan!`;
return info;
}
// Add Plus role to the user // Add Plus role to the user
await member.roles.add(plus); await member.roles.add(plus);
await roleAddLog(user.id, mod.id, plus); await roleAddLog(user.id, mod.id, plus);

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

@@ -25,8 +25,11 @@ import { LogLevel, SapphireClient, container } from '@sapphire/framework';
import '@sapphire/plugin-scheduled-tasks/register'; import '@sapphire/plugin-scheduled-tasks/register';
import '@sapphire/plugin-logger/register'; import '@sapphire/plugin-logger/register';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { createClient } from 'redis'; import { Redis } from 'ioredis';
import type { RedisClientType } from 'redis';
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({
@@ -50,7 +53,10 @@ const client = new SapphireClient({
tasks: { tasks: {
bull: { bull: {
connection: { connection: {
host: process.env.BULLMQ_URL, host: process.env.REDIS_HOST,
username: process.env.REDIS_USER,
password: process.env.REDIS_PASSWORD,
port: REDIS_PORT,
}, },
}, },
}, },
@@ -63,11 +69,14 @@ 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 = createClient({ container.redis = new Redis({
url: process.env.REDIS_URL, host: process.env.REDIS_HOST,
username: process.env.REDIS_USER,
password: process.env.REDIS_PASSWORD,
port: REDIS_PORT,
db: 1,
}); });
await container.redis.connect();
// Log the bot in to Discord // Log the bot in to Discord
await client.login(token); await client.login(token);
@@ -83,7 +92,7 @@ const main = async () => {
declare module '@sapphire/pieces' { declare module '@sapphire/pieces' {
interface Container { interface Container {
database: PrismaClient; database: PrismaClient;
redis: RedisClientType; redis: Redis;
} }
} }

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,14 +68,25 @@ 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);
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 discussion and debates.` + `and remember to follow the <#${IDs.channels.information.conduct}> and to respect ongoing discussions and debates.` +
"\n\nIf you would like to be verified as a vegan, join the 'Verification' voice channel.", `\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!',
); );

View File

@@ -1,38 +0,0 @@
// 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, ListenerErrorPayload } from '@sapphire/framework';
export class ErrorListener extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, {
...options,
event: 'listenerError',
});
}
public run(error: Error, payload: ListenerErrorPayload) {
this.container.logger.debug(
`TEST ERROR: ${error.stack}\n\nPAYLOAD: ${payload.piece.name}`,
);
}
}

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

@@ -26,34 +26,33 @@ import type {
} from 'discord.js'; } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class PatreonOnlyPrecondition extends AllFlowsPrecondition { export class OutreachLeaderOnlyPrecondition extends AllFlowsPrecondition {
public override async messageRun(message: Message) { public override async messageRun(message: Message) {
// for message command // for message command
return this.checkPatreon(message.member!); return this.checkCoordinator(message.member!);
} }
public override async chatInputRun(interaction: CommandInteraction) { public override async chatInputRun(interaction: CommandInteraction) {
// for slash command // for slash command
return this.checkPatreon(interaction.member! as GuildMember); return this.checkCoordinator(interaction.member! as GuildMember);
} }
public override async contextMenuRun( public override async contextMenuRun(
interaction: ContextMenuCommandInteraction, interaction: ContextMenuCommandInteraction,
) { ) {
// for context menu command // for context menu command
return this.checkPatreon(interaction.member! as GuildMember); return this.checkCoordinator(interaction.member! as GuildMember);
} }
private async checkPatreon(user: GuildMember) { private async checkCoordinator(user: GuildMember) {
return user.roles.cache.has(IDs.roles.patron) || return user.roles.cache.has(IDs.roles.staff.outreachLeader)
user.roles.cache.has(IDs.roles.patreon)
? this.ok() ? this.ok()
: this.error({ message: 'Only Patreon members can run this command!' }); : this.error({ message: 'Only outreach leaders can run this command!' });
} }
} }
declare module '@sapphire/framework' { declare module '@sapphire/framework' {
interface Preconditions { interface Preconditions {
PatreonOnly: never; OutreachLeaderOnly: never;
} }
} }

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

@@ -19,9 +19,12 @@
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 { TextChannel, 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(
@@ -36,7 +39,9 @@ export class TempBan extends ScheduledTask {
// Get the guild where the user is in // Get the guild where the user is in
let guild = this.container.client.guilds.cache.get(payload.guildId); let guild = this.container.client.guilds.cache.get(payload.guildId);
if (guild === undefined) { if (guild === undefined) {
guild = await this.container.client.guilds.fetch(payload.guildId); guild = await this.container.client.guilds
.fetch(payload.guildId)
.catch(() => undefined);
if (guild === undefined) { if (guild === undefined) {
this.container.logger.error('Temp Unban Task: Guild not found!'); this.container.logger.error('Temp Unban Task: Guild not found!');
return; return;
@@ -48,7 +53,7 @@ export class TempBan extends ScheduledTask {
let user = guild.client.users.cache.get(userId); let user = guild.client.users.cache.get(userId);
if (user === undefined) { if (user === undefined) {
user = await guild.client.users.fetch(userId); user = await guild.client.users.fetch(userId).catch(() => undefined);
if (user === undefined) { if (user === undefined) {
this.container.logger.error( this.container.logger.error(
'Temp Unban Task: Could not fetch banned user!', 'Temp Unban Task: Could not fetch banned user!',
@@ -70,20 +75,27 @@ export class TempBan extends ScheduledTask {
await removeTempBan(userId); await removeTempBan(userId);
// Log unban // Log unban
let logChannel = guild.channels.cache.get(IDs.channels.logs.restricted) as let logChannel = guild.channels.cache.get(IDs.channels.logs.restricted);
| TextChannel
| undefined;
if (logChannel === undefined) { if (logChannel === undefined) {
logChannel = (await guild.channels.fetch( const logChannelFetch = await guild.channels
IDs.channels.logs.restricted, .fetch(IDs.channels.logs.restricted)
)) as TextChannel | undefined; .catch(() => null);
if (logChannel === undefined) { if (logChannelFetch === null) {
this.container.logger.error( this.container.logger.error(
`Temp Ban Listener: Could not fetch log channel. User Snowflake: ${userId}`, `Temp Ban Listener: Could not fetch log channel. User Snowflake: ${userId}`,
); );
return; return;
} }
logChannel = logChannelFetch;
}
if (!logChannel.isTextBased()) {
this.container.logger.error(
'Temp Ban Listener: Log channel is not a text based channel!',
);
return;
} }
const log = new EmbedBuilder() const log = new EmbedBuilder()

View File

@@ -17,8 +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 type { VoiceChannel } from 'discord.js';
import { ScheduledTask } from '@sapphire/plugin-scheduled-tasks'; import { ScheduledTask } from '@sapphire/plugin-scheduled-tasks';
import { ChannelType } from 'discord.js';
export class VerifyTimeout extends ScheduledTask { export class VerifyTimeout extends ScheduledTask {
public constructor( public constructor(
@@ -30,17 +30,24 @@ export class VerifyTimeout extends ScheduledTask {
public async run(payload: { channelId: string; userId: string }) { public async run(payload: { channelId: string; userId: string }) {
// Get the guild where the user is in // Get the guild where the user is in
let channel = this.container.client.channels.cache.get( let channel = this.container.client.channels.cache.get(payload.channelId);
payload.channelId,
) as VoiceChannel | undefined;
if (channel === undefined) { if (channel === undefined) {
channel = (await this.container.client.channels.fetch( const channelFetch = await this.container.client.channels
payload.channelId, .fetch(payload.channelId)
)) as VoiceChannel | undefined; .catch(() => null);
if (channel === undefined) { if (channelFetch === null) {
this.container.logger.error('verifyTimeout: Channel not found!'); this.container.logger.error('verifyTimeout: Channel not found!');
return; return;
} }
channel = channelFetch;
}
if (channel.type !== ChannelType.GuildVoice) {
this.container.logger.error(
'verifyTimeout: Channel is not a voice channel!',
);
return;
} }
if (channel.members.size < 2 && channel.members.has(payload.userId)) { if (channel.members.size < 2 && channel.members.has(payload.userId)) {

View File

@@ -32,7 +32,9 @@ export class VerifyUnblock extends ScheduledTask {
// Get the guild where the user is in // Get the guild where the user is in
let guild = this.container.client.guilds.cache.get(payload.guildId); let guild = this.container.client.guilds.cache.get(payload.guildId);
if (guild === undefined) { if (guild === undefined) {
guild = await this.container.client.guilds.fetch(payload.guildId); guild = await this.container.client.guilds
.fetch(payload.guildId)
.catch(() => undefined);
if (guild === undefined) { if (guild === undefined) {
this.container.logger.error('verifyUnblock: Guild not found!'); this.container.logger.error('verifyUnblock: Guild not found!');
return; return;
@@ -42,7 +44,7 @@ export class VerifyUnblock extends ScheduledTask {
// Find GuildMember for the user // Find GuildMember for the user
let user = guild.members.cache.get(payload.userId); let user = guild.members.cache.get(payload.userId);
if (user === undefined) { if (user === undefined) {
user = await guild.members.fetch(payload.userId).catch(undefined); user = await guild.members.fetch(payload.userId).catch(() => undefined);
if (user === undefined) { if (user === undefined) {
this.container.logger.error('verifyUnblock: GuildMember not found!'); this.container.logger.error('verifyUnblock: GuildMember not found!');
return; return;

View File

@@ -29,6 +29,7 @@ export const blockedRoles = [
IDs.roles.staff.trialVerifier, IDs.roles.staff.trialVerifier,
IDs.roles.staff.mentor, IDs.roles.staff.mentor,
IDs.roles.stageHost, IDs.roles.stageHost,
IDs.roles.booster,
]; ];
export const blockedRolesAfterRestricted = [ export const blockedRolesAfterRestricted = [

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

@@ -1,7 +1,7 @@
import { container } from '@sapphire/framework'; import { container } from '@sapphire/framework';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
export async function addToDatabase( export async function addSusNoteDB(
userId: string, userId: string,
modId: string, modId: string,
message: string, message: string,
@@ -10,13 +10,23 @@ export async function addToDatabase(
await container.database.sus.create({ await container.database.sus.create({
data: { data: {
user: { user: {
connect: { connectOrCreate: {
id: userId, where: {
id: userId,
},
create: {
id: userId,
},
}, },
}, },
mod: { mod: {
connect: { connectOrCreate: {
id: modId, where: {
id: modId,
},
create: {
id: modId,
},
}, },
}, },
note: message, note: message,

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

@@ -85,6 +85,7 @@ export async function createStat(
eventId: number, eventId: number,
leaderId: Snowflake, leaderId: Snowflake,
roleId: Snowflake, roleId: Snowflake,
channelId: Snowflake,
) { ) {
await container.database.stat.create({ await container.database.stat.create({
data: { data: {
@@ -110,6 +111,7 @@ export async function createStat(
role: { role: {
create: { create: {
roleId, roleId,
channelId,
}, },
}, },
}, },

View File

@@ -18,8 +18,10 @@
*/ */
const devIDs = { const devIDs = {
guild: '999431674972618792',
roles: { roles: {
trusted: '999431675081666599', trusted: '999431675081666599',
booster: '',
nonvegan: { nonvegan: {
nonvegan: '999431675081666598', nonvegan: '999431675081666598',
vegCurious: '999431675098447932', vegCurious: '999431675098447932',
@@ -57,6 +59,9 @@ const devIDs = {
verifierCoordinator: '999431675140382810', verifierCoordinator: '999431675140382810',
eventCoordinator: '999431675165556817', eventCoordinator: '999431675165556817',
outreachCoordinator: '999431675140382807', outreachCoordinator: '999431675140382807',
mediaCoordinator: '1204801056404676618',
hrCoordinator: '1204795893480431657',
outreachLeader: '999431675123597409',
restricted: '999431675123597407', restricted: '999431675123597407',
moderator: '999431675123597408', moderator: '999431675123597408',
trialModerator: '999431675123597404', trialModerator: '999431675123597404',
@@ -96,6 +101,7 @@ const devIDs = {
}, },
nonVegan: { nonVegan: {
general: '999431677325615189', general: '999431677325615189',
vcText: '999431677535338567',
}, },
vegan: { vegan: {
general: '999431677535338575', general: '999431677535338575',
@@ -121,6 +127,7 @@ const devIDs = {
}, },
logs: { logs: {
restricted: '999431681217937513', restricted: '999431681217937513',
bot: '999431681217937516',
economy: '999431681599623198', economy: '999431681599623198',
sus: '999431681599623199', sus: '999431681599623199',
}, },
@@ -129,10 +136,12 @@ const devIDs = {
staff: '999431676058927253', staff: '999431676058927253',
modMail: '1095453371411996762', modMail: '1095453371411996762',
verification: '999431677006860409', verification: '999431677006860409',
activism: '999431677795389549',
diversity: '999431679053660185', diversity: '999431679053660185',
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

@@ -1,18 +0,0 @@
// 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/>.
*/

View File

@@ -20,33 +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',
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
], ],
}, },
@@ -59,6 +61,9 @@ let IDs = {
verifierCoordinator: '940721280376778822', verifierCoordinator: '940721280376778822',
eventCoordinator: '944732860554817586', eventCoordinator: '944732860554817586',
outreachCoordinator: '954804769476730890', outreachCoordinator: '954804769476730890',
mediaCoordinator: '1203778509449723914',
hrCoordinator: '1203802120180989993',
outreachLeader: '730915698544607232',
restricted: '851624392928264222', restricted: '851624392928264222',
moderator: '826157475815489598', moderator: '826157475815489598',
trialModerator: '982074555596152904', trialModerator: '982074555596152904',
@@ -71,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',
@@ -98,6 +103,7 @@ let IDs = {
}, },
nonVegan: { nonVegan: {
general: '798967615636504657', general: '798967615636504657',
vcText: '808191982169096232',
}, },
vegan: { vegan: {
general: '787738272616808509', general: '787738272616808509',
@@ -123,6 +129,7 @@ let IDs = {
}, },
logs: { logs: {
restricted: '920993034462715925', restricted: '920993034462715925',
bot: '872126272015314966',
economy: '932050015034159174', economy: '932050015034159174',
sus: '872884989950324826', sus: '872884989950324826',
}, },
@@ -131,10 +138,12 @@ let IDs = {
staff: '768685283583328257', staff: '768685283583328257',
modMail: '867077297664426006', modMail: '867077297664426006',
verification: '797505409073676299', verification: '797505409073676299',
activism: '873918877019545640',
diversity: '933078380394459146', diversity: '933078380394459146',
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

View File

@@ -31,7 +31,7 @@
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "src" /* Specify the base directory to resolve non-relative module names. */, "baseUrl": "src" /* Specify the base directory to resolve non-relative module names. */,
"paths": { "paths": {
"#utils/*": ["./utils/*"] "#utils/*": ["./utils/*"],
} /* Specify a set of entries that re-map imports to additional lookup locations. */, } /* Specify a set of entries that re-map imports to additional lookup locations. */,
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
@@ -79,7 +79,7 @@
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */ /* Type Checking */
"strict": true /* Enable all strict type-checking options. */ "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
@@ -103,5 +103,5 @@
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
// "skipLibCheck": true /* Skip type checking all .d.ts files. */ // "skipLibCheck": true /* Skip type checking all .d.ts files. */
}, },
"include": ["src"] "include": ["src"],
} }