9 Commits

Author SHA1 Message Date
Anthony Berg
ada16c485e Merge pull request #163 from MercStef/main
feat(arabot): add topBalances command & kill gifs
2024-01-02 23:15:29 +00:00
Stefanie Merceron
285a3fefbc feat(arabot): add new KIll gifs thanks to Lithium 2023-12-30 00:11:25 -05:00
Stefanie Merceron
9758e91701 Merge branch 'main' of https://github.com/MercStef/arabot 2023-11-18 20:06:04 -05:00
Stefanie Merceron
330b4bd8a6 Merge branch 'main' of https://github.com/MercStef/arabot 2023-11-18 20:05:08 -05:00
Stefanie Merceron
ab66a8b5c0 Merge branch 'main' of https://github.com/MercStef/arabot 2023-11-18 20:04:39 -05:00
Stefanie Merceron
72a9593c85 Merge branch 'veganhacktivists:main' into main 2023-11-18 20:04:05 -05:00
Stefanie Merceron
573fbe0c09 Merge branch 'veganhacktivists:main' into main 2023-11-18 19:24:09 -05:00
Stefanie Merceron
940c25a5ed Merge branch 'veganhacktivists:main' into main 2023-11-18 18:11:03 -05:00
Stefanie Merceron
2aab5a514e feat(arabot): add topBalances command 2023-11-18 17:14:53 -05:00
132 changed files with 5322 additions and 4168 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,18 +3,16 @@ 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 (if running everything within docker compose, use "redis" for the host and leave the rest empty) # Redis
REDIS_HOST= # URL to redis database REDIS_URL= # URL to redis database (if running everything within docker compose, use "redis")
REDIS_USER= # redis database user BULLMQ_URL # URL for redis database, but without redis:// and credentials
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"

View File

@@ -42,7 +42,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -55,7 +55,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -68,4 +68,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v2

View File

@@ -43,7 +43,7 @@ jobs:
continue-on-error: true continue-on-error: true
- name: Upload analysis results to GitHub - name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v3 uses: github/codeql-action/upload-sarif@v2
with: with:
sarif_file: eslint-results.sarif sarif_file: eslint-results.sarif
wait-for-processing: true wait-for-processing: true

3
.npmrc
View File

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

View File

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

View File

@@ -1,26 +1,24 @@
FROM node:22 AS base FROM node:20
# PNPM
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . /app WORKDIR /opt/app
WORKDIR /app
FROM base AS prod-deps ENV NODE_ENV=production
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM base AS build COPY --chown=node:node package.json .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile COPY --chown=node:node package-lock.json .
RUN pnpm exec prisma generate COPY --chown=node:node tsconfig.json .
RUN pnpm run build COPY --chown=node:node prisma ./prisma/
FROM base RUN npm install
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
RUN chown node:node . COPY . .
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"]

View File

@@ -1,7 +1,7 @@
version: '3.7' version: '3.7'
services: services:
postgres: postgres:
image: postgres:16 image: postgres:15
container_name: postgres container_name: postgres
restart: always restart: always
env_file: env_file:

View File

@@ -14,6 +14,7 @@
- `/daily`/`?daily` - Gives you a daily reward of ARAs - `/daily`/`?daily` - Gives you a daily reward of ARAs
- `/pay <user> <amount> <reason>`/`?pay <user> <amount> <reason>` - Give a user an amount of ARAs - `/pay <user> <amount> <reason>`/`?pay <user> <amount> <reason>` - Give a user an amount of ARAs
- `/balance`/`?balance` - Checks how many ARAs you have - `/balance`/`?balance` - Checks how many ARAs you have
- `/topbalances`/`?topbalances` - Displays the richest server members
## XP ## XP

View File

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

4436
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,10 @@
"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 && pnpm run start" "start:migrate": "prisma migrate deploy && npm run start"
}, },
"imports": { "imports": {
"#utils/*": "./dist/utils/*.js" "#utils/*": "./dist/utils/*.js"
@@ -29,35 +28,31 @@
"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.22.0", "@prisma/client": "^5.5.2",
"@sapphire/discord.js-utilities": "^7.3.2", "@sapphire/discord.js-utilities": "^7.0.2",
"@sapphire/framework": "^5.3.2", "@sapphire/framework": "^4.7.2",
"@sapphire/plugin-logger": "^4.0.2", "@sapphire/plugin-logger": "^3.0.6",
"@sapphire/plugin-scheduled-tasks": "^10.0.2", "@sapphire/plugin-scheduled-tasks": "^8.0.0",
"@sapphire/plugin-subcommands": "^6.0.3", "@sapphire/plugin-subcommands": "^5.0.0",
"@sapphire/stopwatch": "^1.5.4", "@sapphire/stopwatch": "^1.5.0",
"@sapphire/time-utilities": "^1.7.14", "@sapphire/time-utilities": "^1.7.10",
"@sapphire/ts-config": "^5.0.1", "@sapphire/ts-config": "^5.0.0",
"@sapphire/utilities": "^3.18.1", "@sapphire/utilities": "^3.13.0",
"bullmq": "^5.34.10", "@types/node": "^20.8.9",
"discord.js": "^14.17.3", "bullmq": "^4.12.7",
"ioredis": "^5.4.2", "discord.js": "^14.13.0",
"ts-node": "^10.9.2", "redis": "^4.6.10",
"typescript": "~5.4.5" "ts-node": "^10.9.1",
"typescript": "^5.2.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.17.13", "@types/ioredis": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.9.0",
"eslint": "8.56.0", "eslint": "8.54.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.0.0",
"prettier": "3.2.4", "prettier": "3.1.0",
"prisma": "^5.22.0" "prisma": "^5.5.2"
}, }
"packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"
} }

1899
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- You are about to drop the column `active` on the `Warning` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Warning" DROP COLUMN "active";

View File

@@ -1,8 +0,0 @@
/*
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,10 +222,9 @@ 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 {
@@ -257,6 +256,7 @@ model Warning {
mod User @relation("warnMod", fields: [modId], references: [id]) mod User @relation("warnMod", fields: [modId], references: [id])
modId String modId String
time DateTime @default(now()) time DateTime @default(now())
active Boolean @default(true)
note String note String
} }

View File

@@ -22,7 +22,7 @@ import { ChannelType } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class AccessCommand extends Command { export class AccessCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'access', name: 'access',

View File

@@ -18,11 +18,11 @@
*/ */
import { Args, Command, RegisterBehavior } from '@sapphire/framework'; import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { Message, MessageFlagsBitField } from 'discord.js'; import type { Message } 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 {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'anonymous', name: 'anonymous',
@@ -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!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
return; return;
} }
@@ -77,17 +77,8 @@ 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!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: 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;
} }
@@ -95,8 +86,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',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
return; return;
} }
@@ -104,8 +95,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!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
} }
@@ -114,8 +105,8 @@ export class AnonymousCommand extends Command {
await interaction.reply({ await interaction.reply({
content: 'Sent the message', content: 'Sent the message',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
} }
@@ -130,7 +121,7 @@ export class AnonymousCommand extends Command {
return; return;
} }
if (channel.isSendable()) { if (channel.isTextBased()) {
await channel.send(text); await channel.send(text);
} else { } else {
await message.react('❌'); await message.react('❌');

View File

@@ -21,7 +21,7 @@ import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Message } from 'discord.js'; import type { Message } from 'discord.js';
export class ClearCommand extends Command { export class ClearCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'clear', name: 'clear',

View File

@@ -31,10 +31,7 @@ import {
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class PrivateCommand extends Subcommand { export class PrivateCommand extends Subcommand {
public constructor( public constructor(context: Subcommand.Context, options: Subcommand.Options) {
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, { super(context, {
...options, ...options,
name: 'private', name: 'private',
@@ -356,12 +353,6 @@ 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,11 +20,11 @@
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/fun/economy'; import { getBalance } from '#utils/database/economy';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
export class BalanceCommand extends Command { export class BalanceCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'balance', name: 'balance',

View File

@@ -21,12 +21,12 @@ 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/fun/economy'; import { daily, getLastDaily } from '#utils/database/economy';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class DailyCommand extends Command { export class DailyCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'daily', name: 'daily',

View File

@@ -20,12 +20,12 @@
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/fun/economy'; import { getBalance, transfer } from '#utils/database/economy';
import { EmbedBuilder, TextChannel } from 'discord.js'; import { EmbedBuilder, TextChannel } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class BalanceCommand extends Command { export class BalanceCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'pay', name: 'pay',

View File

@@ -0,0 +1,147 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Stefanie Merceron
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 { Command, RegisterBehavior } from '@sapphire/framework';
import { Guild, GuildMember, Message, EmbedBuilder } from 'discord.js';
import { getTopBalances } from '#utils/database/economy';
export class TopBalancesCommand extends Command {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'topbalances',
aliases: ['topbal', 'leaderboard'],
description: 'Shows the top 5 largest balances on the server',
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) => builder.setName(this.name).setDescription(this.description),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const { guild } = interaction;
if (guild === null) {
await interaction.reply({
content: 'Could not find the guild!',
ephemeral: true,
});
return;
}
await interaction.deferReply();
const info = await this.showTopBalances(guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
public async messageRun(message: Message) {
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Could not find the guild!');
return;
}
const info = await this.showTopBalances(guild);
await message.reply({
content: info.message,
embeds: info.embeds,
});
if (!info.success) {
await message.react('❌');
}
}
private async showTopBalances(guild: Guild) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
success: false,
};
const embed = new EmbedBuilder()
.setColor('#cc802c')
.setTitle('Top Balances on the Server')
.setAuthor({
name: 'ARA',
iconURL:
'https://github.com/veganhacktivists/arabot/blob/main/docs/images/logo.png?raw=true',
});
const leaders = await getTopBalances(5);
const fetchMemberPromises: Promise<GuildMember | null>[] = [];
for (const leader of leaders) {
fetchMemberPromises.push(
guild.members.fetch(leader.userId).catch(() => null),
);
}
const members = await Promise.all(fetchMemberPromises);
for (let i = 0; i < leaders.length; i += 1) {
const leader = leaders[i];
const member = members[i];
// Server Members Display on The Leaderboard
if (member) {
embed.addFields(
{
name: i + 1 + '.',
value:
'[' +
member.displayName +
'](<https://discord.com/users/' +
leader.userId +
'>)',
inline: true,
},
{
name: 'Balance',
value: leader.balance + " ARA's",
inline: true,
},
{
name: '\u200b',
value: '\u200b',
inline: true,
},
);
}
}
info.success = true;
info.embeds.push(embed);
return info;
}
}

View File

@@ -18,17 +18,17 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { N1984 } from '#utils/gifs'; import { N1984 } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun'; import { addFunLog, countTotal } from '#utils/database/fun';
export class N1984Command extends Command { export class N1984Command extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: '1984', name: '1984',
description: 'this is literally 1984', description: 'this is literally 1984',
preconditions: [['CoordinatorOnly', 'ModOnly']], preconditions: ['ModOnly'],
}); });
} }
@@ -45,26 +45,10 @@ export class N1984Command extends Command {
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the user // Get the user
const { member } = interaction; const { user } = interaction;
// Type checks await addFunLog(user.id, '1984');
if (!(member instanceof GuildMember)) { const count = await countTotal(user.id, '1984');
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
await addFunLog(member.id, '1984');
const count = await countTotal(member.id, '1984');
let embedFooter: string;
if (count === 1) {
embedFooter = `${member.displayName} 1984'd the server for the first time!`;
} else {
embedFooter = `${member.displayName} 1984'd the server ${count} times!`;
}
// Creates the embed for the 1984 reaction // Creates the embed for the 1984 reaction
// Add a 1 in 1000 chance of Dantas literally making ARA 1984 // Add a 1 in 1000 chance of Dantas literally making ARA 1984
@@ -74,9 +58,9 @@ export class N1984Command extends Command {
: N1984[Math.floor(Math.random() * N1984.length)]; : N1984[Math.floor(Math.random() * N1984.length)];
const n1984Embed = new EmbedBuilder() const n1984Embed = new EmbedBuilder()
.setColor('#ffffff') .setColor('#ffffff')
.setTitle(`${member.displayName} is happy!`) .setTitle(`${user.username} is happy!`)
.setImage(random1984) .setImage(random1984)
.setFooter({ text: embedFooter }); .setFooter({ text: `${user.username}'s 1984 count: ${count}` });
// Send the embed // Send the embed
await interaction.reply({ embeds: [n1984Embed], fetchReply: true }); await interaction.reply({ embeds: [n1984Embed], fetchReply: true });

View File

@@ -18,12 +18,12 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { Cringe } from '#utils/gifs'; import { Cringe } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun'; import { addFunLog, countTotal } from '#utils/database/fun';
export class CringeCommand extends Command { export class CringeCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'cringe', name: 'cringe',
@@ -44,34 +44,19 @@ export class CringeCommand extends Command {
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the user // Get the user
const { member } = interaction; // TODO exception handling
const { user } = interaction;
// Type check await addFunLog(user.id, 'cringe');
if (!(member instanceof GuildMember)) { const count = await countTotal(user.id, 'cringe');
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
await addFunLog(member.id, 'cringe');
const count = await countTotal(member.id, 'cringe');
let embedFooter: string;
if (count === 1) {
embedFooter = `${member.displayName} cringed for the first time!`;
} else {
embedFooter = `${member.displayName} cringed ${count} times!`;
}
// Creates the embed for the cringe reaction // Creates the embed for the cringe reaction
const randomCringe = Cringe[Math.floor(Math.random() * Cringe.length)]; const randomCringe = Cringe[Math.floor(Math.random() * Cringe.length)];
const cringeEmbed = new EmbedBuilder() const cringeEmbed = new EmbedBuilder()
.setColor('#001148') .setColor('#001148')
.setTitle(`${member.displayName} feels immense cringe...`) .setTitle(`${user.username} feels immense cringe...`)
.setImage(randomCringe) .setImage(randomCringe)
.setFooter({ text: embedFooter }); .setFooter({ text: `${user.username}'s cringe count: ${count}` });
// Send the embed // Send the embed
await interaction.reply({ embeds: [cringeEmbed], fetchReply: true }); await interaction.reply({ embeds: [cringeEmbed], fetchReply: true });

View File

@@ -18,15 +18,16 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { Happy } from '#utils/gifs'; import { Happy } from '#utils/gifs';
export class HappyCommand extends Command { export class HappyCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'happy', name: 'happy',
description: 'Express your happiness', description: 'Express your happiness',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }
@@ -43,22 +44,15 @@ export class HappyCommand extends Command {
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the user // Get the user
const { member } = interaction; // TODO exception handling
const member = interaction.member!.user;
// Type checks const memberGuildMember = interaction.guild!.members.cache.get(member.id)!;
if (!(member instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
// Creates the embed for the happy reaction // Creates the embed for the happy reaction
const randomHappy = Happy[Math.floor(Math.random() * Happy.length)]; const randomHappy = Happy[Math.floor(Math.random() * Happy.length)];
const happyEmbed = new EmbedBuilder() const happyEmbed = new EmbedBuilder()
.setColor('#40ff00') .setColor('#40ff00')
.setTitle(`${member.displayName} is happy!`) .setTitle(`${memberGuildMember.displayName} is happy!`)
.setImage(randomHappy); .setImage(randomHappy);
// Send the embed // Send the embed

View File

@@ -18,12 +18,12 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { Hugs } from '#utils/gifs'; import { Hugs } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun'; import { addFunLog, countTotal } from '#utils/database/fun';
export class HugCommand extends Command { export class HugCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'hug', name: 'hug',
@@ -54,43 +54,20 @@ export class HugCommand extends Command {
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the users // Get the users
const user = interaction.options.getUser('user', true); const user = interaction.options.getUser('user', true);
const hugger = interaction.member; const hugger = interaction.user;
// Type Checks
if (!(hugger instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
await addFunLog(hugger.id, 'hug', user.id); await addFunLog(hugger.id, 'hug', user.id);
const count = await countTotal(hugger.id, 'hug', user.id); const count = await countTotal(hugger.id, 'hug', user.id);
let embedFooter: string;
if (hugger.id === user.id) {
if (count === 1) {
embedFooter = `You hugged yourself for the first time!`;
} else {
embedFooter = `You hugged yourself ${count} times!`;
}
} else {
if (count === 1) {
embedFooter = `${hugger.displayName} hugged you for the first time!`;
} else {
embedFooter = `${hugger.displayName} hugged you ${count} times!`;
}
}
// Creates the embed for the hug // Creates the embed for the hug
const randomHug = Hugs[Math.floor(Math.random() * Hugs.length)]; const randomHug = Hugs[Math.floor(Math.random() * Hugs.length)];
const hugEmbed = new EmbedBuilder() const hugEmbed = new EmbedBuilder()
.setColor('#0099ff') .setColor('#0099ff')
.setTitle(`Hug from ${hugger.displayName}`) .setTitle(`Hug from ${hugger.username}`)
.setImage(randomHug) .setImage(randomHug)
.setFooter({ text: embedFooter }); .setFooter({
text: `Amount of hugs given from ${hugger.username} to you: ${count}`,
});
// Send the hug // Send the hug
await interaction.reply({ await interaction.reply({

View File

@@ -18,12 +18,12 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { Kill } from '#utils/gifs'; import { Kill } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun'; import { addFunLog, countTotal } from '#utils/database/fun';
export class KillCommand extends Command { export class KillCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'kill', name: 'kill',
@@ -54,16 +54,7 @@ export class KillCommand extends Command {
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the users // Get the users
const user = interaction.options.getUser('user', true)!; const user = interaction.options.getUser('user', true)!;
const sender = interaction.member; const sender = interaction.user;
// Type checks
if (!(sender instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
if (user.id === sender.id) { if (user.id === sender.id) {
await interaction.reply('You changed your mind'); await interaction.reply('You changed your mind');
@@ -73,20 +64,15 @@ export class KillCommand extends Command {
await addFunLog(sender.id, 'kill', user.id); await addFunLog(sender.id, 'kill', user.id);
const count = await countTotal(sender.id, 'kill', user.id); const count = await countTotal(sender.id, 'kill', user.id);
let embedFooter: string;
if (count === 1) {
embedFooter = `${sender.displayName} killed you for the first time!`;
} else {
embedFooter = `${sender.displayName} killed you ${count} times!`;
}
// Creates the embed for the kill // Creates the embed for the kill
const randomKill = Kill[Math.floor(Math.random() * Kill.length)]; const randomKill = Kill[Math.floor(Math.random() * Kill.length)];
const killEmbed = new EmbedBuilder() const killEmbed = new EmbedBuilder()
.setColor('#ff0000') .setColor('#ff0000')
.setTitle(`Kill from ${sender.displayName}`) .setTitle(`Kill from ${sender.username}`)
.setImage(randomKill) .setImage(randomKill)
.setFooter({ text: embedFooter }); .setFooter({
text: `Amount of kills from ${sender.username} to you: ${count}`,
});
// Send the kill // Send the kill
await interaction.reply({ await interaction.reply({

View File

@@ -18,12 +18,12 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { Poke } from '#utils/gifs'; import { Poke } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun'; import { addFunLog, countTotal } from '#utils/database/fun';
export class PokeCommand extends Command { export class PokeCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'poke', name: 'poke',
@@ -54,42 +54,20 @@ export class PokeCommand extends Command {
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the users // Get the users
const user = interaction.options.getUser('user', true)!; const user = interaction.options.getUser('user', true)!;
const sender = interaction.member; const sender = interaction.user;
// Type checks
if (!(sender instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
await addFunLog(sender.id, 'poke', user.id); await addFunLog(sender.id, 'poke', user.id);
const count = await countTotal(sender.id, 'poke', user.id); const count = await countTotal(sender.id, 'poke', user.id);
let embedFooter: string;
if (sender.id === user.id) {
if (count === 1) {
embedFooter = `You poked yourself for the first time!`;
} else {
embedFooter = `You poked yourself ${count} times!`;
}
} else {
if (count === 1) {
embedFooter = `${sender.displayName} poked you for the first time!`;
} else {
embedFooter = `${sender.displayName} poked you ${count} times!`;
}
}
// Creates the embed for the poke // Creates the embed for the poke
const randomPoke = Poke[Math.floor(Math.random() * Poke.length)]; const randomPoke = Poke[Math.floor(Math.random() * Poke.length)];
const pokeEmbed = new EmbedBuilder() const pokeEmbed = new EmbedBuilder()
.setColor('#0099ff') .setColor('#0099ff')
.setTitle(`Poke from ${sender.displayName}`) .setTitle(`Poke from ${sender.username}`)
.setImage(randomPoke) .setImage(randomPoke)
.setFooter({ text: embedFooter }); .setFooter({
text: `Amount of pokes from ${sender.username} to you: ${count}`,
});
// Send the poke // Send the poke
await interaction.reply({ await interaction.reply({

View File

@@ -18,15 +18,16 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { Sad } from '#utils/gifs'; import { Sad } from '#utils/gifs';
export class SadCommand extends Command { export class SadCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'sad', name: 'sad',
description: 'Express your sadness', description: 'Express your sadness',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }
@@ -43,22 +44,15 @@ export class SadCommand extends Command {
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the user // Get the user
const { member } = interaction; // TODO exception handling
const member = interaction.member!.user;
// Type checks const memberGuildMember = interaction.guild!.members.cache.get(member.id)!;
if (!(member instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
// Creates the embed for the sad reaction // Creates the embed for the sad reaction
const randomSad = Sad[Math.floor(Math.random() * Sad.length)]; const randomSad = Sad[Math.floor(Math.random() * Sad.length)];
const sadEmbed = new EmbedBuilder() const sadEmbed = new EmbedBuilder()
.setColor('#001148') .setColor('#001148')
.setTitle(`${member.displayName} is sad...`) .setTitle(`${memberGuildMember.displayName} is sad...`)
.setImage(randomSad); .setImage(randomSad);
// Send the embed // Send the embed

View File

@@ -18,15 +18,16 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { Shrug } from '#utils/gifs'; import { Shrug } from '#utils/gifs';
export class ShrugCommand extends Command { export class ShrugCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'shrug', name: 'shrug',
description: 'Ugh... whatever... idk...', description: 'Ugh... whatever... idk...',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }
@@ -43,22 +44,15 @@ export class ShrugCommand extends Command {
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the user // Get the user
const { member } = interaction; // TODO exception handling
const member = interaction.member!.user;
// Type checks const memberGuildMember = interaction.guild!.members.cache.get(member.id)!;
if (!(member instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
// Creates the embed for the shrug reaction // Creates the embed for the shrug reaction
const randomShrug = Shrug[Math.floor(Math.random() * Shrug.length)]; const randomShrug = Shrug[Math.floor(Math.random() * Shrug.length)];
const shrugEmbed = new EmbedBuilder() const shrugEmbed = new EmbedBuilder()
.setColor('#001980') .setColor('#001980')
.setTitle(`${member.displayName} shrugs`) .setTitle(`${memberGuildMember.displayName} shrugs`)
.setImage(randomShrug); .setImage(randomShrug);
// Send the embed // Send the embed

View File

@@ -21,15 +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/moderation/ban'; import { addBan, checkBan } from '#utils/database/ban';
import { addEmptyUser, updateUser } from '#utils/database/dbExistingUser'; import { addEmptyUser, updateUser } from '#utils/database/dbExistingUser';
import { import { checkTempBan, removeTempBan } from '#utils/database/tempBan';
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.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'ban', name: 'ban',

View File

@@ -22,11 +22,11 @@ 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/moderation/tempBan'; import { addTempBan, checkTempBan } from '#utils/database/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 {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'tempban', name: 'tempban',
@@ -258,13 +258,11 @@ export class TempBanCommand extends Command {
await addTempBan(userId, modId, time.fromNow, reason); await addTempBan(userId, modId, time.fromNow, reason);
// Create scheduled task to unban // Create scheduled task to unban
await this.container.tasks.create( this.container.tasks.create(
'tempBan',
{ {
name: 'tempBan', userId: user.id,
payload: { guildId: guild.id,
userId: user.id,
guildId: guild.id,
},
}, },
time.offset, time.offset,
); );

View File

@@ -28,15 +28,12 @@ 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/moderation/ban'; import { removeBan, checkBan, addBan } from '#utils/database/ban';
import { import { checkTempBan, removeTempBan } from '#utils/database/tempBan';
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 {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'unban', name: 'unban',

View File

@@ -31,10 +31,7 @@ import type { TextChannel, Snowflake } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class DiversityCommand extends Subcommand { export class DiversityCommand extends Subcommand {
public constructor( public constructor(context: Subcommand.Context, options: Subcommand.Options) {
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, { super(context, {
...options, ...options,
name: 'diversity', name: 'diversity',

View File

@@ -25,7 +25,7 @@ import type { Message } from 'discord.js';
import { ChannelType } from 'discord.js'; import { ChannelType } from 'discord.js';
export class MoveAllCommand extends Command { export class MoveAllCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'moveall', name: 'moveall',

View File

@@ -21,7 +21,7 @@ import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { GuildMember, Message } from 'discord.js'; import type { GuildMember, Message } from 'discord.js';
export class RenameUserCommand extends Command { export class RenameUserCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'rename', name: 'rename',

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/moderation/restriction'; import { restrict, checkActive } from '#utils/database/restriction';
import { randint } from '#utils/maths'; import { randint } from '#utils/maths';
import { blockedRolesAfterRestricted } from '#utils/blockedRoles'; import { blockedRolesAfterRestricted } from '#utils/blockedRoles';
@@ -277,7 +277,7 @@ export async function restrictRun(
} }
export class RestrictCommand extends Command { export class RestrictCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'restrict', name: 'restrict',

View File

@@ -21,15 +21,15 @@ 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/moderation/restriction'; import { getRestrictions } from '#utils/database/restriction';
import { checkStaff } from '#utils/checker'; import { checkStaff } from '#utils/checker';
export class RestrictLogsCommand extends Command { export class RestrictLogsCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'restrictlogs', name: 'restrictlogs',
description: 'Shows restriction history for a user', description: 'Unrestricts a user',
preconditions: ['ModOnly'], preconditions: ['ModOnly'],
}); });
} }
@@ -75,9 +75,10 @@ export class RestrictLogsCommand extends Command {
userId = user.id; userId = user.id;
} }
const staffChannel = checkStaff(channel); let staffChannel = false;
if (staffChannel) { if (channel.type === ChannelType.GuildText) {
channel = channel as TextChannel; channel = channel as TextChannel;
staffChannel = checkStaff(channel);
if (userId === null) { if (userId === null) {
let topic: string[]; let topic: string[];

View File

@@ -22,7 +22,7 @@ import type { User, Message } from 'discord.js';
import { restrictRun } from './restrict'; import { restrictRun } from './restrict';
export class RestrictToleranceCommand extends Command { export class RestrictToleranceCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'restricttolerance', name: 'restricttolerance',

View File

@@ -24,10 +24,7 @@ import { CategoryChannel, ChannelType } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class RestrictToolsCommand extends Subcommand { export class RestrictToolsCommand extends Subcommand {
public constructor( public constructor(context: Subcommand.Context, options: Subcommand.Options) {
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, { super(context, {
...options, ...options,
name: 'restricttools', name: 'restricttools',

View File

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

View File

@@ -24,7 +24,7 @@ import { Duration, DurationFormatter } from '@sapphire/time-utilities';
import { isNumber } from '#utils/maths'; import { isNumber } from '#utils/maths';
export class SlowmodeCommand extends Command { export class SlowmodeCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'slowmode', name: 'slowmode',

View File

@@ -22,7 +22,7 @@ import type { GuildMember, Message } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class SoftMuteCommand extends Command { export class SoftMuteCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'softmute', name: 'softmute',

View File

@@ -20,37 +20,30 @@
import { RegisterBehavior, Args } from '@sapphire/framework'; import { RegisterBehavior, Args } from '@sapphire/framework';
import { Subcommand } from '@sapphire/plugin-subcommands'; import { Subcommand } from '@sapphire/plugin-subcommands';
import { import {
ChannelType,
EmbedBuilder, EmbedBuilder,
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
ButtonInteraction, ButtonInteraction,
ButtonStyle, ButtonStyle,
User,
Guild,
TextChannel,
GuildMember,
Snowflake,
MessageFlagsBitField,
} from 'discord.js'; } from 'discord.js';
import type { Message } from 'discord.js'; import type { Message, GuildMember, TextChannel } from 'discord.js';
import { isMessageInstance } from '@sapphire/discord.js-utilities';
import { addExistingUser } from '#utils/database/dbExistingUser';
import { import {
addSusNoteDB, addToDatabase,
findNotes, findNotes,
getNote, getNote,
deactivateNote, deactivateNote,
deactivateAllNotes, deactivateAllNotes,
} from '#utils/database/moderation/sus'; } from '#utils/database/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';
// TODO add a check when they join the server to give the user the sus role again // TODO add a check when they join the server to give the user the sus role again
export class SusCommand extends Subcommand { export class SusCommand extends Subcommand {
public constructor( public constructor(context: Subcommand.Context, options: Subcommand.Options) {
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, { super(context, {
...options, ...options,
name: 'sus', name: 'sus',
@@ -58,8 +51,8 @@ export class SusCommand extends Subcommand {
{ {
name: 'add', name: 'add',
default: true, default: true,
chatInputRun: 'addNoteChatInput', chatInputRun: 'addNote',
messageRun: 'addNoteMessage', messageRun: 'addMessage',
}, },
{ {
name: 'view', name: 'view',
@@ -147,9 +140,7 @@ export class SusCommand extends Subcommand {
} }
// Subcommand to add sus note // Subcommand to add sus note
public async addNoteChatInput( public async addNote(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 note = interaction.options.getString('note', true); const note = interaction.options.getString('note', true);
@@ -160,127 +151,40 @@ 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!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
}); });
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);
// Gives the sus role to the user // Check if the user exists on the database
await this.addSusRole(user, guild); const member = guild.members.cache.get(user.id);
const modMember = guild.members.cache.get(mod.id);
info.message = `Added the sus note for ${user}: ${note}`; if (member === undefined || modMember === undefined) {
info.success = true; await interaction.reply({
content: 'Error fetching users!',
// Log the sus note ephemeral: true,
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');
info.message = `Added a sus note for ${user} but could not find the log channel. This has been logged to the database.`;
return info;
}
}
const message = new EmbedBuilder()
.setColor('#0099ff')
.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 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; return;
} }
// Check if user and mod are on the database
await addExistingUser(member);
await addExistingUser(modMember);
await addToDatabase(user.id, mod.id, note);
// 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) {
@@ -292,12 +196,19 @@ 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!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
}); });
return; return;
} }
const staffChannel = checkStaff(interaction.channel); let staffChannel = false;
let { channel } = interaction;
if (channel !== null) {
if (channel.type === ChannelType.GuildText) {
channel = channel as TextChannel;
staffChannel = checkStaff(channel);
}
}
// Gets the sus notes from the database // Gets the sus notes from the database
const notes = await findNotes(user.id, true); const notes = await findNotes(user.id, true);
@@ -306,35 +217,61 @@ 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!`,
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
return; return;
} }
// Creates the embed to display the sus note // Creates the embed to display the sus note
const noteEmbed = createSusLogEmbed(notes, user, guild); const noteEmbed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(`${notes.length} sus notes for ${user.username}`)
.setThumbnail(user.displayAvatarURL());
// Add up to 10 of the latest sus notes to the embed
for (
let i = notes.length > 10 ? notes.length - 10 : 0;
i < notes.length;
i += 1
) {
// Get mod name
let mod = notes[i].modId;
const modMember = guild.members.cache.get(mod);
if (modMember !== undefined) {
mod = modMember.displayName;
}
// Add sus note to embed
noteEmbed.addFields({
name: `Sus ID: ${
notes[i].id
} | Moderator: ${mod} | Date: <t:${Math.floor(
notes[i].time.getTime() / 1000,
)}>`,
value: notes[i].note,
});
}
// Sends the notes to the user // Sends the notes to the user
await interaction.reply({ await interaction.reply({
embeds: [noteEmbed], embeds: [noteEmbed],
flags: staffChannel ? undefined : MessageFlagsBitField.Flags.Ephemeral, ephemeral: !staffChannel,
withResponse: true, fetchReply: 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!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
return; return;
} }
@@ -346,46 +283,45 @@ 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!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: 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
let user = guild.client.users.cache.get(userId); const member = await guild.members.cache.get(note.userId);
if (!(user instanceof User)) { const mod = await guild.members.cache.get(note.modId);
user = await guild.client.users.fetch(userId).catch(() => undefined);
} // TODO fix if user left the server
if (user === undefined) { if (member === undefined || mod === undefined) {
await interaction.reply({ await interaction.reply({
content: 'Error fetching user!', content: 'Error fetching users from Discord!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
return; return;
} }
let modCreator = guild.client.users.cache.get(modId); // Get user's name
if (!(modCreator instanceof User)) { let userName = note.userId;
modCreator = await guild.client.users.fetch(modId).catch(() => undefined); if (member !== undefined) {
userName = member.displayName;
} }
let modCreatorDisplay = modId; // Get mod name
if (modCreator instanceof User) { let modName = note.modId;
modCreatorDisplay = modCreator.displayName; if (mod !== undefined) {
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 ${user.tag}`) .setTitle(`Sus note for ${userName}`)
.setThumbnail(user.displayAvatarURL()) .setThumbnail(member.displayAvatarURL())
.addFields({ .addFields({
name: `ID: ${noteId} | Moderator: ${modCreatorDisplay} | Date: <t:${Math.floor( name: `ID: ${noteId} | Moderator: ${modName} | Date: <t:${Math.floor(
note.time.getTime() / 1000, note.time.getTime() / 1000,
)}>`, )}>`,
value: note.note, value: note.note,
@@ -407,21 +343,16 @@ export class SusCommand extends Subcommand {
const message = await interaction.reply({ const message = await interaction.reply({
embeds: [noteEmbed], embeds: [noteEmbed],
components: [buttons], components: [buttons],
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
// Checks if the message is not an APIMessage // Checks if the message is not an APIMessage
if (message.resource === null) { if (!isMessageInstance(message)) {
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
@@ -433,28 +364,18 @@ 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: `${user}'s sus note (ID: ${noteId}) has been successfully removed`, content: `${member}'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(userId, true); const notes = await findNotes(member.id, 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) {
let member = guild.members.cache.get(userId); await member.roles.remove(IDs.roles.restrictions.sus);
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);
} }
}); });
@@ -466,66 +387,19 @@ 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!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
return; return;
} }
@@ -536,8 +410,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!',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
return; return;
} }
@@ -550,8 +424,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!`,
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
return; return;
} }
@@ -601,21 +475,16 @@ export class SusCommand extends Subcommand {
const message = await interaction.reply({ const message = await interaction.reply({
embeds: [noteEmbed], embeds: [noteEmbed],
components: [buttons], components: [buttons],
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
// Checks if the message is not an APIMessage // Checks if the message is not an APIMessage
if (message.resource === null) { if (!isMessageInstance(message)) {
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
@@ -632,8 +501,6 @@ 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
@@ -647,36 +514,51 @@ export class SusCommand extends Subcommand {
await member.roles.remove(IDs.roles.restrictions.sus); await member.roles.remove(IDs.roles.restrictions.sus);
} }
// Logs removal of 1 sus note // Non Application Command method of adding a sus note
private async deleteAllNotesLogger(user: User, mod: User, guild: Guild) { // xlevra begged me to add this... so I guess here it is
// Log the sus note public async addMessage(message: Message, args: Args) {
let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as // Get arguments
| TextChannel let user: GuildMember;
| undefined; try {
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 (logChannel === undefined) { if (note === null) {
logChannel = (await guild.channels.fetch(IDs.channels.logs.sus)) as await message.react('❌');
| TextChannel await message.reply('No sus note was provided!');
| undefined; return;
if (logChannel === undefined) {
this.container.logger.error('Sus Error: Could not fetch log channel');
return;
}
} }
const embed = new EmbedBuilder() if (mod === null) {
.setColor('#28A745') await message.react('❌');
.setAuthor({ await message.reply(
name: `Purged all sus notes for ${user.tag}`, 'Moderator not found! Try again or contact a developer!',
iconURL: `${user.displayAvatarURL()}`, );
}) return;
.addFields( }
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
)
.setTimestamp()
.setFooter({ text: `ID: ${user.id}` });
await logChannel.send({ embeds: [embed] }); // Check if user and mod are on the database
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);
}
// Checks if the user is xlevra to send a very kind message
if (mod.id === '259624904746467329') {
await message.reply('Fuck you for making me add this feature 🤬');
}
await message.react('✅');
} }
} }

View File

@@ -19,15 +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 { import { addMute, removeMute, checkActive } from '#utils/database/vcMute';
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 {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'vcmute', name: 'vcmute',

127
src/commands/mod/warn.ts Normal file
View File

@@ -0,0 +1,127 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 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 { Args, Command } from '@sapphire/framework';
import type { User, Message, Snowflake, Guild } from 'discord.js';
import { addExistingUser, updateUser } from '#utils/database/dbExistingUser';
import { addWarn } from '#utils/database/warnings';
/*
This command is not intended to be functional for now, this is purely to log
warnings onto a database, so if we were to switch purely to ARA Bot, it would
mean we would have a lot of the warns already in the database.
*/
export class WarnCommand extends Command {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'warn',
description: 'Warns a user (only used for logging to a database for now)',
preconditions: [['CoordinatorOnly', 'ModOnly']],
});
}
public async messageRun(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 reason = args.finished ? null : await args.rest('string');
const mod = message.member;
if (reason === null) {
await message.react('❌');
await message.reply('Warn reason was not provided!');
return;
}
if (mod === null) {
await message.react('❌');
await message.reply(
'Moderator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const warn = await this.warn(user.id, mod.id, reason, guild);
if (!warn.success) {
await message.react('❌');
}
// await message.react('✅');
}
private async warn(
userId: Snowflake,
modId: Snowflake,
reason: string,
guild: Guild,
) {
const info = {
message: '',
success: false,
};
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod!';
return info;
}
// Check if mod is in database
await updateUser(mod);
// Gets guildMember
let member = guild.members.cache.get(userId);
if (member === undefined) {
member = await guild.members.fetch(userId).catch(() => undefined);
}
if (member === undefined) {
info.message = 'User is not on this server';
return info;
}
await addExistingUser(member);
await addWarn(userId, modId, reason);
info.message = `Warned ${member}`;
info.success = true;
return info;
}
}

View File

@@ -1,186 +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 { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, TextChannel } from 'discord.js';
import type { Message, Guild, User } from 'discord.js';
import IDs from '#utils/ids';
import {
deleteWarning,
fetchWarning,
} from '#utils/database/moderation/warnings';
import { checkStaff } from '#utils/checker';
export class DeleteWarningCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'deletewarning',
aliases: ['delwarn', 'removewarning'],
description: 'Deletes a warning',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addIntegerOption((option) =>
option
.setName('id')
.setDescription('ID for the warning')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const warningId = interaction.options.getInteger('id', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
const staffChannel = checkStaff(interaction.channel);
await interaction.deferReply({ ephemeral: !staffChannel });
const info = await this.deleteWarning(warningId, mod, guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
// Non Application Command method for removing a warning
public async messageRun(message: Message, args: Args) {
// Get arguments
let warningId: number;
try {
warningId = await args.pick('integer');
} catch {
await message.react('❌');
await message.react('Correct warning ID not provided!');
return;
}
const mod = message.author;
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await this.deleteWarning(warningId, mod, guild);
await message.reply({ content: info.message, embeds: info.embeds });
if (!info.success) {
await message.react('❌');
}
}
private async deleteWarning(warningId: number, mod: User, guild: Guild) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
success: false,
};
const warning = await fetchWarning(warningId);
if (warning === null) {
info.message = `Warning ID \`${warningId}\` not found!`;
return info;
}
await deleteWarning(warningId);
info.success = true;
const userId = warning.userId;
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = `Deleted warning ID \`${warningId}\`, but the user could not be found!`;
return info;
}
}
// Log the warnings deletion
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(
'Delete Warning Error: Could not fetch log channel',
);
info.message =
`Deleted warning for ${user} (Warning ID: ${warningId} but ` +
'could not find the log channel.';
return info;
}
}
const message = new EmbedBuilder()
.setColor('#28A745')
.setAuthor({
name: `Removed warning for ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Warning ID', value: `${warningId}`, inline: true },
)
.setTimestamp()
.setFooter({ text: `ID: ${userId}` });
await logChannel.send({ embeds: [message] });
info.message = `Deleted warning for ${user}`;
return info;
}
}

View File

@@ -1,226 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023, 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 {
Args,
Command,
container,
RegisterBehavior,
} from '@sapphire/framework';
import type { User, Message, Snowflake, Guild, TextChannel } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import { addWarn } from '#utils/database/moderation/warnings';
import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids';
export class WarnCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'warn',
description: 'Warns a user',
preconditions: [['CoordinatorOnly', 'ModOnly']],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to warn')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('Reason for the warning')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply();
const info = await this.warn(user.id, mod.id, reason, guild);
await interaction.editReply({
content: info.message,
});
}
// Non Application Command method for warning a user
public async messageRun(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 reason = args.finished ? null : await args.rest('string');
const mod = message.member;
if (reason === null) {
await message.react('❌');
await message.reply('Warn reason was not provided!');
return;
}
if (mod === null) {
await message.react('❌');
await message.reply(
'Moderator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const warn = await this.warn(user.id, mod.id, reason, guild);
if (!warn.success) {
await message.react('❌');
return;
}
await message.react('✅');
}
private async warn(
userId: Snowflake,
modId: Snowflake,
reason: string,
guild: Guild,
) {
const info = {
message: '',
success: false,
};
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod!';
return info;
}
// Check if mod is in database
await updateUser(mod);
// Gets User for person being restricted
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = 'Error fetching user';
return info;
}
}
await addWarn(userId, modId, reason);
info.message = `Warned ${user}`;
info.success = true;
// DM the reason
const dmEmbed = new EmbedBuilder()
.setColor('#FF6700')
.setAuthor({
name: "You've been warned!",
iconURL: `${user.displayAvatarURL()}`,
})
.addFields({ name: 'Reason', value: reason })
.setTimestamp();
await user.send({ embeds: [dmEmbed] }).catch(() => {});
// Log the ban
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) {
container.logger.error('Warn Error: Could not fetch log channel');
info.message = `Warned ${user} but could not find the log channel. This has been logged to the database.`;
return info;
}
}
const message = new EmbedBuilder()
.setColor('#FF6700')
.setAuthor({
name: `Warned ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Reason', value: reason },
)
.setTimestamp()
.setFooter({ text: `ID: ${userId}` });
await logChannel.send({ embeds: [message] });
return info;
}
}

View File

@@ -1,161 +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 { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { ChannelType, EmbedBuilder } from 'discord.js';
import type { Message, Guild, User } from 'discord.js';
import IDs from '#utils/ids';
import { fetchWarnings } from '#utils/database/moderation/warnings';
import { checkStaff } from '#utils/checker';
import { createWarningsEmbed } from '#utils/embeds';
export class WarningsCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'warnings',
aliases: ['warninglog', 'warnlog'],
description: 'Shows all the warnings for the user',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to check the warnings for')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
const staffChannel = checkStaff(interaction.channel);
await interaction.deferReply({ ephemeral: !staffChannel });
const info = await this.warnings(user, guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
// Non Application Command method for fetching warnings
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User | undefined;
try {
user = await args.pick('user');
} catch {
user = undefined;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
if (user === undefined) {
const { channel } = message;
if (channel.type !== ChannelType.GuildText) {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
let topic: string[];
if (channel.parentId === IDs.categories.modMail) {
// Checks if the channel topic has the user's snowflake
if (channel.topic !== null) {
topic = channel.topic.split(' ');
// eslint-disable-next-line prefer-destructuring
const userId = topic[2];
user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
}
}
}
}
if (user === undefined) {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const info = await this.warnings(user, guild);
await message.reply({ content: info.message, embeds: info.embeds });
}
private async warnings(user: User, guild: Guild) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
};
const warnings = await fetchWarnings(user.id);
if (warnings.length === 0) {
info.message = `${user} user has no warnings.`;
return info;
}
// Creates an embed to display the warnings
const embed = createWarningsEmbed(warnings, user, guild);
info.embeds.push(embed);
return info;
}
}

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 { ChannelType, PermissionsBitField, Snowflake } from 'discord.js'; import type { Snowflake } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser'; import { updateUser } from '#utils/database/dbExistingUser';
import { import {
addStatUser, addStatUser,
@@ -38,10 +38,7 @@ import IDs from '#utils/ids';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
export class OutreachCommand extends Subcommand { export class OutreachCommand extends Subcommand {
public constructor( public constructor(context: Subcommand.Context, options: Subcommand.Options) {
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, { super(context, {
...options, ...options,
name: 'outreach', name: 'outreach',
@@ -66,6 +63,7 @@ export class OutreachCommand extends Subcommand {
], ],
}, },
], ],
preconditions: ['ModOnly'],
}); });
} }
@@ -199,15 +197,15 @@ export class OutreachCommand extends Subcommand {
if (mod === undefined) { if (mod === undefined) {
await interaction.reply({ await interaction.reply({
content: 'Outreach Leader was not found!', content: 'Mod was not found!',
ephemeral: true, ephemeral: true,
}); });
return; return;
} }
if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) { if (!mod.roles.cache.has(IDs.roles.staff.outreachCoordinator)) {
await interaction.reply({ await interaction.reply({
content: 'You need to be an Outreach Leader to run this command!', content: 'You need to be an Outreach Coordinator to run this command!',
ephemeral: true, ephemeral: true,
}); });
return; return;
@@ -253,9 +251,9 @@ export class OutreachCommand extends Subcommand {
return; return;
} }
if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) { if (!mod.roles.cache.has(IDs.roles.staff.outreachCoordinator)) {
await interaction.reply({ await interaction.reply({
content: 'You need to be an Outreach Leader to run this command!', content: 'You need to be an Outreach Coordinator to run this command!',
ephemeral: true, ephemeral: true,
}); });
return; return;
@@ -274,8 +272,7 @@ export class OutreachCommand extends Subcommand {
stat.forEach(({ role }) => { stat.forEach(({ role }) => {
if (role !== null) { if (role !== null) {
guild.roles.delete(role.roleId); // Delete role guild.roles.delete(role.roleId);
guild.channels.delete(role.channelId); // Delete VC
} }
}); });
@@ -388,66 +385,14 @@ 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,
}); });
// Create a voice channel for group await createStat(event.id, leader.id, role.id);
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}`,
}); });
@@ -494,7 +439,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.outreachLeader) !leaderMember.roles.cache.has(IDs.roles.staff.outreachCoordinator)
) { ) {
await interaction.editReply({ await interaction.editReply({
content: `You are not the leader for ${group}`, content: `You are not the leader for ${group}`,

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class BookClubCommand extends Command { export class BookClubCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'bookclub', name: 'bookclub',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class DebateHostCommand extends Command { export class DebateHostCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'debatehost', name: 'debatehost',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class GameNightHostCommand extends Command { export class GameNightHostCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'gamenight', name: 'gamenight',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class GuestCommand extends Command { export class GuestCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'guest', name: 'guest',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class MentorCommand extends Command { export class MentorCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'mentor', name: 'mentor',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class ModCommand extends Command { export class ModCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'mod', name: 'mod',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class RestrictedAccessCommand extends Command { export class RestrictedAccessCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'restrictedaccess', name: 'restrictedaccess',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class StageHostCommand extends Command { export class StageHostCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'stagehost', name: 'stagehost',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class TrialModCommand extends Command { export class TrialModCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'trialmod', name: 'trialmod',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class TrialVerifierCommand extends Command { export class TrialVerifierCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'trialverifier', name: 'trialverifier',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class VerifierCommand extends Command { export class VerifierCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'verifier', name: 'verifier',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class ActivistCommand extends Command { export class ActivistCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'activist', name: 'activist',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class ARAVeganCommand extends Command { export class ARAVeganCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'aravegan', name: 'aravegan',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class ConvincedCommand extends Command { export class ConvincedCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'convinced', name: 'convinced',

View File

@@ -23,13 +23,13 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class PlusCommand extends Command { export class PlusCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'plus', name: 'plus',
aliases: ['+'], aliases: ['+'],
description: 'Give/remove the plus role', description: 'Give/remove the plus role',
preconditions: [['CoordinatorOnly', 'VerifierOnly', 'ModOnly']], preconditions: [['CoordinatorOnly', 'ModOnly']],
}); });
} }
@@ -138,14 +138,6 @@ 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

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class TrustedCommand extends Command { export class TrustedCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'trusted', name: 'trusted',
@@ -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, especially by **not** posting anything **NSFW**, and **no animal products or consumption of animal products**.' + "\nMake sure that you follow the rules, and don't post anything NSFW, anything objectifying animals and follow Discord's ToS." +
`\n\nNot following these rules will result in the **immediate removal** of the ${trusted.name} role.`, `\nNot following these rules can result in the removal of the ${trusted.name} role.`,
) )
.catch(() => {}); .catch(() => {});
info.success = true; info.success = true;

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class VeganCommand extends Command { export class VeganCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'vegan', name: 'vegan',

View File

@@ -23,7 +23,7 @@ import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role'; import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class VegCuriousCommand extends Command { export class VegCuriousCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'vegcurious', name: 'vegcurious',

View File

@@ -21,7 +21,7 @@ import { Command, RegisterBehavior } from '@sapphire/framework';
import type { Message } from 'discord.js'; import type { Message } from 'discord.js';
export class ApplyCommand extends Command { export class ApplyCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'apply', name: 'apply',

View File

@@ -22,7 +22,7 @@ import type { Message } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class RenameUserCommand extends Command { export class RenameUserCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'count', name: 'count',

View File

@@ -19,7 +19,7 @@ import { EmbedBuilder } from 'discord.js';
import type { Message } from 'discord.js'; import type { Message } from 'discord.js';
export class HelpCommand extends Command { export class HelpCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'help', name: 'help',

View File

@@ -18,10 +18,9 @@
*/ */
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.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'info', name: 'info',
@@ -95,9 +94,7 @@ 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,11 +17,12 @@
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 { Message, MessageFlagsBitField } from 'discord.js'; import type { Message } 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.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'ping', name: 'ping',
@@ -40,13 +41,12 @@ 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?',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
withResponse: true, fetchReply: true,
}); });
if (msg.resource !== null && msg.resource.message !== null) { if (isMessageInstance(msg)) {
const diff = const diff = msg.createdTimestamp - interaction.createdTimestamp;
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,11 +57,6 @@ 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

@@ -27,7 +27,7 @@ import {
import { manualVerification } from '#utils/database/verification'; import { manualVerification } from '#utils/database/verification';
export class VerifyCommand extends Command { export class VerifyCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'verify', name: 'verify',

View File

@@ -22,7 +22,7 @@ import IDs from '#utils/ids';
import { checkVerificationFinish } from '#utils/database/verification'; import { checkVerificationFinish } from '#utils/database/verification';
export class VerifyTimeoutRemoveCommand extends Command { export class VerifyTimeoutRemoveCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'verifytimeoutremove', name: 'verifytimeoutremove',

View File

@@ -20,10 +20,10 @@
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/fun/xp'; import { getRank, xpToNextLevel } from '#utils/database/xp';
export class RankCommand extends Command { export class RankCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'rank', name: 'rank',

View File

@@ -25,11 +25,8 @@ 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 { Redis } from 'ioredis'; import { createClient } from 'redis';
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({
@@ -53,10 +50,7 @@ const client = new SapphireClient({
tasks: { tasks: {
bull: { bull: {
connection: { connection: {
host: process.env.REDIS_HOST, host: process.env.BULLMQ_URL,
username: process.env.REDIS_USER,
password: process.env.REDIS_PASSWORD,
port: REDIS_PORT,
}, },
}, },
}, },
@@ -69,14 +63,11 @@ const main = async () => {
client.logger.info('Logging in'); client.logger.info('Logging in');
// Create databases // Create databases
container.database = new PrismaClient(); container.database = await new PrismaClient();
container.redis = new Redis({ container.redis = createClient({
host: process.env.REDIS_HOST, url: process.env.REDIS_URL,
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);
@@ -92,7 +83,7 @@ const main = async () => {
declare module '@sapphire/pieces' { declare module '@sapphire/pieces' {
interface Container { interface Container {
database: PrismaClient; database: PrismaClient;
redis: Redis; redis: RedisClientType;
} }
} }

View File

@@ -21,14 +21,12 @@ import {
InteractionHandler, InteractionHandler,
InteractionHandlerTypes, InteractionHandlerTypes,
} from '@sapphire/framework'; } from '@sapphire/framework';
import type { PieceContext } from '@sapphire/framework';
import type { ButtonInteraction, GuildMember } from 'discord.js'; import type { ButtonInteraction, GuildMember } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class NonVeganAccessButtonHandler extends InteractionHandler { export class NonVeganAccessButtonHandler extends InteractionHandler {
public constructor( public constructor(ctx: PieceContext, options: InteractionHandler.Options) {
ctx: InteractionHandler.LoaderContext,
options: InteractionHandler.Options,
) {
super(ctx, { super(ctx, {
...options, ...options,
interactionHandlerType: InteractionHandlerTypes.Button, interactionHandlerType: InteractionHandlerTypes.Button,

View File

@@ -21,20 +21,12 @@ import {
InteractionHandler, InteractionHandler,
InteractionHandlerTypes, InteractionHandlerTypes,
} from '@sapphire/framework'; } from '@sapphire/framework';
import { import type { PieceContext } from '@sapphire/framework';
ButtonInteraction, import type { ButtonInteraction, GuildMember, TextChannel } from 'discord.js';
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(ctx: PieceContext, options: InteractionHandler.Options) {
ctx: InteractionHandler.LoaderContext,
options: InteractionHandler.Options,
) {
super(ctx, { super(ctx, {
...options, ...options,
interactionHandlerType: InteractionHandlerTypes.Button, interactionHandlerType: InteractionHandlerTypes.Button,
@@ -60,7 +52,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.',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
}); });
return; return;
} }
@@ -68,25 +60,14 @@ 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 discussions and debates.` + `and remember to follow the <#${IDs.channels.information.conduct}> and to respect ongoing discussion and debates.` +
`\n\nIf you are vegan, you can join the 'Verification' voice channel, or use \`/apply\` with the Appy bot in <#${IDs.channels.nonVegan.vcText}>, ` + "\n\nIf you would like to be verified as a vegan, join the 'Verification' voice channel.",
'to be verified and gain access to more channels.',
); );
return; return;
} }
@@ -94,13 +75,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.",
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
}); });
} 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.',
flags: MessageFlagsBitField.Flags.Ephemeral, ephemeral: true,
}); });
} }
} }

View File

@@ -20,15 +20,12 @@
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/moderation/ban'; import { addBan, checkBan } from '#utils/database/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';
export class BanListener extends Listener { export class BanListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'guildBanAdd', event: 'guildBanAdd',

View File

@@ -19,14 +19,11 @@
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/moderation/ban'; import { checkBan, getBanReason } from '#utils/database/ban';
import { checkTempBan } from '#utils/database/moderation/tempBan'; import { checkTempBan } from '#utils/database/tempBan';
export class BanJoinListener extends Listener { export class BanJoinListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'guildMemberAdd', event: 'guildMemberAdd',

View File

@@ -20,15 +20,12 @@
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/moderation/ban'; import { addBan, checkBan, removeBan } from '#utils/database/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';
export class UnbanListener extends Listener { export class UnbanListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'guildBanRemove', event: 'guildBanRemove',

View File

@@ -21,10 +21,7 @@ import { Listener } from '@sapphire/framework';
import type { Message } from 'discord.js'; import type { Message } from 'discord.js';
export class BotAppreciationListener extends Listener { export class BotAppreciationListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'messageCreate', event: 'messageCreate',

View File

@@ -24,10 +24,7 @@ import type {
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
export class OldCommandDeniedListener extends Listener { export class OldCommandDeniedListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'messageCommandDenied', event: 'messageCommandDenied',

View File

@@ -21,14 +21,11 @@
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/fun/counting'; import { getLastCount, addCount } from '#utils/database/counting';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class XpListener extends Listener { export class XpListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'messageCreate', event: 'messageCreate',
@@ -49,11 +46,6 @@ 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!',
); );
@@ -68,11 +60,6 @@ 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

@@ -27,10 +27,7 @@ import {
} from '#utils/database/dbExistingUser'; } from '#utils/database/dbExistingUser';
export class DbLeaveServerListener extends Listener { export class DbLeaveServerListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'guildMemberRemove', event: 'guildMemberRemove',

View File

@@ -24,10 +24,7 @@ import type {
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
export class CommandDeniedListener extends Listener { export class CommandDeniedListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'chatInputCommandDenied', event: 'chatInputCommandDenied',

View File

@@ -24,10 +24,7 @@ import { Listener } from '@sapphire/framework';
import { setupTypes } from '#utils/database/outreach'; import { setupTypes } from '#utils/database/outreach';
export class EventTypesReadyListener extends Listener { export class EventTypesReadyListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
once: true, once: true,

View File

@@ -1,199 +0,0 @@
// 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 { Client } from 'discord.js';
import IDs from '#utils/ids';
import { fetchRoles } from '#utils/database/dbExistingUser';
import { checkActive } from '#utils/database/moderation/restriction';
import { getUser } from '#utils/database/fun/xp';
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: true,
});
}
public async run(client: Client) {
this.container.logger.info(
'FixRolesOnReady: Preparation before starting to fix the roles for nonvegans...',
);
// 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('1329152627312824320')
.catch(() => null);
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 not 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 % 50 === 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 already has vegan or non-vegan role
if (
member.roles.cache.has(IDs.roles.vegan.vegan) ||
member.roles.cache.has(IDs.roles.nonvegan.nonvegan)
) {
await this.delay(1000);
count++;
continue;
}
// Checks if the user is restricted, and skips over them if they are
const restricted = await checkActive(userId);
if (
restricted ||
member.roles.cache.has(IDs.roles.restrictions.restricted1) ||
member.roles.cache.has(IDs.roles.restrictions.restricted2) ||
member.roles.cache.has(IDs.roles.restrictions.restrictedVegan)
) {
count++;
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));
if (!roles.includes(IDs.roles.nonvegan.nonvegan)) {
const xp = await getUser(userId);
if (xp !== null && xp.xp > 0) {
roles.push(IDs.roles.nonvegan.nonvegan);
} else {
count++;
continue;
}
}
// 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}.`,
);
// Add a delay so that there's around 4 users processed a second
await this.delay(5000);
}
// 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);
}
}
private delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View File

@@ -18,27 +18,15 @@
*/ */
import { Listener } from '@sapphire/framework'; import { Listener } from '@sapphire/framework';
import { ChannelType } from 'discord.js'; import { ChannelType, EmbedBuilder } from 'discord.js';
import type { GuildChannel, EmbedBuilder } from 'discord.js'; import type { GuildChannel } from 'discord.js';
import { setTimeout } from 'timers/promises'; import { setTimeout } from 'timers/promises';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { import { checkActive, getRestrictions } from '#utils/database/restriction';
checkActive, import { findNotes } from '#utils/database/sus';
getRestrictions,
} from '#utils/database/moderation/restriction';
import { findNotes } from '#utils/database/moderation/sus';
import {
createRestrictLogEmbed,
createSusLogEmbed,
createWarningsEmbed,
} from '#utils/embeds';
import { fetchWarnings } from '#utils/database/moderation/warnings';
export class ModMailCreateListener extends Listener { export class ModMailCreateListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'channelCreate', event: 'channelCreate',
@@ -63,16 +51,6 @@ export class ModMailCreateListener extends Listener {
// Get the user's ID // Get the user's ID
const userId = topic[2]; const userId = topic[2];
// Gets user who created ModMail
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
return;
}
}
// Check if the user is currently restricted on the database // Check if the user is currently restricted on the database
if (!(await checkActive(userId))) return; if (!(await checkActive(userId))) return;
@@ -82,21 +60,81 @@ export class ModMailCreateListener extends Listener {
// Creation of embeds // Creation of embeds
// Restriction Logs // Restriction Logs
const embeds: EmbedBuilder[] = []; const restrictEmbed = new EmbedBuilder()
embeds.push(createRestrictLogEmbed(restrictions, user, guild)); .setColor('#FF6700')
.setTitle(`${restrictions.length} restrictions`)
.setFooter({ text: `ID: ${userId}` });
// Warnings // Add up to 10 of the latest restrictions to the embed
const warnings = await fetchWarnings(userId); for (
let i = restrictions.length > 10 ? restrictions.length - 10 : 0;
i < restrictions.length;
i += 1
) {
// Get mod names
let restMod = restrictions[i].modId;
const restModMember = guild.members.cache.get(restMod);
if (restModMember !== undefined) {
restMod = restModMember.displayName;
}
let endRestMod = restrictions[i].endModId;
if (endRestMod !== null) {
const endRestModMember = guild.members.cache.get(endRestMod);
if (endRestModMember !== undefined) {
endRestMod = endRestModMember.displayName;
}
}
embeds.push(createWarningsEmbed(warnings, user, guild)); let restTitle = `Restriction: ${i + 1} | Restricted by: ${restMod} | `;
if (endRestMod !== null) {
restTitle += `Unrestricted by: ${endRestMod} | `;
} else {
restTitle += 'Currently Restricted | ';
}
restTitle += `Date: <t:${Math.floor(
restrictions[i].startTime.getTime() / 1000,
)}>`;
restrictEmbed.addFields({
name: restTitle,
value: restrictions[i].reason,
});
}
// Sus Notes // Sus Notes
const notes = await findNotes(userId, true); const notes = await findNotes(userId, true);
embeds.push(createSusLogEmbed(notes, user, guild)); const susEmbed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(`${notes.length} sus notes`);
// Add up to 10 of the latest sus notes to the embed
for (
let i = notes.length > 10 ? notes.length - 10 : 0;
i < notes.length;
i += 1
) {
// Get mod name
const modGuildMember = guild.members.cache.get(notes[i].modId);
let mod = notes[i].modId;
if (modGuildMember !== undefined) {
mod = modGuildMember.displayName;
}
// Add sus note to embed
susEmbed.addFields({
name: `Sus ID: ${
notes[i].id
} | Moderator: ${mod} | Date: <t:${Math.floor(
notes[i].time.getTime() / 1000,
)}>`,
value: notes[i].note,
});
}
// Set a timeout for 1 second and then send the 2 embeds // Set a timeout for 1 second and then send the 2 embeds
await setTimeout(1000); await setTimeout(1000);
await channel.send({ embeds: embeds }); await channel.send({ embeds: [restrictEmbed, susEmbed] });
} }
} }

View File

@@ -24,10 +24,7 @@ import type { Client, TextChannel } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class NonVeganAccessReady extends Listener { export class NonVeganAccessReady extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
once: true, once: true,

View File

@@ -22,13 +22,9 @@
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(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
once: true, once: true,
@@ -36,24 +32,8 @@ export class ReadyListener extends Listener {
}); });
} }
public async run(client: Client) { public 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,18 +28,12 @@ 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 { import { checkActive, getSection } from '#utils/database/restriction';
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';
export class RolesJoinServerListener extends Listener { export class RolesJoinServerListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'guildMemberAdd', event: 'guildMemberAdd',

View File

@@ -23,10 +23,7 @@ import type { Message } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class Suggestions extends Listener { export class Suggestions extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'messageCreate', event: 'messageCreate',
@@ -83,11 +80,6 @@ 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,13 +19,10 @@
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/moderation/vcMute'; import { checkActive, removeMute } from '#utils/database/vcMute';
export class VCMuteListener extends Listener { export class VCMuteListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'voiceStateUpdate', event: 'voiceStateUpdate',

View File

@@ -50,16 +50,13 @@ import {
startVerification, startVerification,
finishVerification, finishVerification,
} from '#utils/database/verification'; } from '#utils/database/verification';
import { findNotes } from '#utils/database/moderation/sus'; import { findNotes } from '#utils/database/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';
export class VerificationJoinVCListener extends Listener { export class VerificationJoinVCListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'voiceStateUpdate', event: 'voiceStateUpdate',
@@ -154,16 +151,14 @@ export class VerificationJoinVCListener extends Listener {
]); ]);
// Start 15-minute timer if verifier does not join // Start 15-minute timer if verifier does not join
await this.container.tasks.create( this.container.tasks.create(
'verifyTimeout',
{ {
name: 'verifyTimeout', channelId: channel.id,
payload: { userId: member.id,
channelId: channel.id,
userId: member.id,
},
}, },
900_000, // 15 minutes 900_000,
); ); // 15 minutes
} }
// Check how many voice channels there are // Check how many voice channels there are
@@ -511,13 +506,11 @@ export class VerificationJoinVCListener extends Listener {
await giveVerificationRoles(user, info.roles); await giveVerificationRoles(user, info.roles);
// Add timeout if they do not have activist role // Add timeout if they do not have activist role
if (!info.roles.activist) { if (!info.roles.activist) {
await this.container.tasks.create( this.container.tasks.create(
'verifyUnblock',
{ {
name: 'verifyUnblock', userId: user.id,
payload: { guildId: guild.id,
userId: user.id,
guildId: guild.id,
},
}, },
info.roles.vegan || info.roles.convinced ? 604800000 : 1814400000, info.roles.vegan || info.roles.convinced ? 604800000 : 1814400000,
); );

View File

@@ -37,10 +37,7 @@ import { fibonacci } from '#utils/maths';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class VerificationLeaveVCListener extends Listener { export class VerificationLeaveVCListener extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'voiceStateUpdate', event: 'voiceStateUpdate',
@@ -106,13 +103,11 @@ export class VerificationLeaveVCListener extends Listener {
// Creates the length of the time for the ban // Creates the length of the time for the ban
const banLength = fibonacci(incompleteCount) * 3600_000; const banLength = fibonacci(incompleteCount) * 3600_000;
await this.container.tasks.create( this.container.tasks.create(
'verifyUnblock',
{ {
name: 'verifyUnblock', userId: user.id,
payload: { guildId: guild.id,
userId: user.id,
guildId: guild.id,
},
}, },
banLength, banLength,
); );
@@ -148,10 +143,7 @@ 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 ( if (textChannel.topic!.includes(userSnowflake!)) {
textChannel.topic !== null &&
textChannel.topic.includes(userSnowflake!)
) {
textChannel.delete(); textChannel.delete();
} }
}); });

View File

@@ -22,10 +22,7 @@ import type { Message } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
export class VerificationMessageCounter extends Listener { export class VerificationMessageCounter extends Listener {
public constructor( public constructor(context: Listener.Context, options: Listener.Options) {
context: Listener.LoaderContext,
options: Listener.Options,
) {
super(context, { super(context, {
...options, ...options,
event: 'messageCreate', event: 'messageCreate',

Some files were not shown because too many files have changed in this diff Show More