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
133 changed files with 5364 additions and 4277 deletions

View File

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

View File

@@ -3,18 +3,16 @@ DISCORD_TOKEN= # Bot token from: https://discord.com/developers/
# Configuration
DEFAULT_PREFIX= # Prefix used to run commands in Discord
DEVELOPMENT= # (true/false) Enables developer mode
DEVELOPMENT= # (true/false) Enables developer mode
# Docker
POSTGRES_USER=USERNAME
POSTGRES_PASSWORD=PASSWORD
POSTGRES_DB=DB
# Redis (if running everything within docker compose, use "redis" for the host and leave the rest empty)
REDIS_HOST= # URL to redis database
REDIS_USER= # redis database user
REDIS_PASSWORD= # redis database password
REDIS_PORT= # redis database port
# Redis
REDIS_URL= # URL to redis database (if running everything within docker compose, use "redis")
BULLMQ_URL # URL for redis database, but without redis:// and credentials
# Database URL (designed for Postgres, but designed on Prisma)
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.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# 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).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v2
# 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
@@ -68,4 +68,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- 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
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: eslint-results.sarif
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
node_modules
tsconfig.json
pnpm-lock.yaml

View File

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

View File

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

View File

@@ -14,6 +14,7 @@
- `/daily`/`?daily` - Gives you a daily reward 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
- `/topbalances`/`?topbalances` - Displays the richest server members
## 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",
"main": "dist/index.js",
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "tsc",
"cleanBuild": "rm -rf ./dist && tsc",
"start": "node dist/index.js",
"start:migrate": "prisma migrate deploy && pnpm run start"
"start:migrate": "prisma migrate deploy && npm run start"
},
"imports": {
"#utils/*": "./dist/utils/*.js"
@@ -29,35 +28,31 @@
"url": "https://github.com/veganhacktivists/arabot/issues"
},
"homepage": "https://github.com/veganhacktivists/arabot#readme",
"engines": {
"node": ">=20",
"pnpm": ">=9"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"@sapphire/discord.js-utilities": "^7.3.2",
"@sapphire/framework": "^5.3.2",
"@sapphire/plugin-logger": "^4.0.2",
"@sapphire/plugin-scheduled-tasks": "^10.0.2",
"@sapphire/plugin-subcommands": "^6.0.3",
"@sapphire/stopwatch": "^1.5.4",
"@sapphire/time-utilities": "^1.7.14",
"@sapphire/ts-config": "^5.0.1",
"@sapphire/utilities": "^3.18.1",
"bullmq": "^5.34.10",
"discord.js": "^14.17.3",
"ioredis": "^5.4.2",
"ts-node": "^10.9.2",
"typescript": "~5.4.5"
"@prisma/client": "^5.5.2",
"@sapphire/discord.js-utilities": "^7.0.2",
"@sapphire/framework": "^4.7.2",
"@sapphire/plugin-logger": "^3.0.6",
"@sapphire/plugin-scheduled-tasks": "^8.0.0",
"@sapphire/plugin-subcommands": "^5.0.0",
"@sapphire/stopwatch": "^1.5.0",
"@sapphire/time-utilities": "^1.7.10",
"@sapphire/ts-config": "^5.0.0",
"@sapphire/utilities": "^3.13.0",
"@types/node": "^20.8.9",
"bullmq": "^4.12.7",
"discord.js": "^14.13.0",
"redis": "^4.6.10",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
},
"devDependencies": {
"@types/node": "^20.17.13",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "8.56.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "3.2.4",
"prisma": "^5.22.0"
},
"packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"
"@types/ioredis": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"eslint": "8.54.0",
"eslint-config-prettier": "^9.0.0",
"prettier": "3.1.0",
"prisma": "^5.5.2"
}
}

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 {
stat Stat @relation(fields: [statId], references: [id])
statId Int @id
roleId String
channelId String
stat Stat @relation(fields: [statId], references: [id])
statId Int @id
roleId String
}
model ParticipantStat {
@@ -257,6 +256,7 @@ model Warning {
mod User @relation("warnMod", fields: [modId], references: [id])
modId String
time DateTime @default(now())
active Boolean @default(true)
note String
}

View File

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

View File

@@ -18,11 +18,11 @@
*/
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';
export class AnonymousCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'anonymous',
@@ -67,8 +67,8 @@ export class AnonymousCommand extends Command {
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
return;
}
@@ -77,17 +77,8 @@ export class AnonymousCommand extends Command {
if (interaction.channel === null) {
await interaction.reply({
content: 'Error getting the channel!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
if (!interaction.channel.isSendable()) {
await interaction.reply({
content: `I do not have sufficient permissions to send a message in ${interaction.channel}!`,
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
return;
}
@@ -95,8 +86,8 @@ export class AnonymousCommand extends Command {
await interaction.channel.send(message);
await interaction.reply({
content: 'Sent the message',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
return;
}
@@ -104,8 +95,8 @@ export class AnonymousCommand extends Command {
if (channel.type !== ChannelType.GuildText) {
await interaction.reply({
content: 'Could not send, unsupported text channel!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
}
@@ -114,8 +105,8 @@ export class AnonymousCommand extends Command {
await interaction.reply({
content: 'Sent the message',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
}
@@ -130,7 +121,7 @@ export class AnonymousCommand extends Command {
return;
}
if (channel.isSendable()) {
if (channel.isTextBased()) {
await channel.send(text);
} else {
await message.react('❌');

View File

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

View File

@@ -31,10 +31,7 @@ import {
import IDs from '#utils/ids';
export class PrivateCommand extends Subcommand {
public constructor(
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
public constructor(context: Subcommand.Context, options: Subcommand.Options) {
super(context, {
...options,
name: 'private',
@@ -356,12 +353,6 @@ export class PrivateCommand extends Subcommand {
} else if (user.roles.cache.has(IDs.roles.staff.eventCoordinator)) {
name = 'event';
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 {
name = 'coordinator';
id = IDs.roles.staff.coordinator;

View File

@@ -20,11 +20,11 @@
import { Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Guild, Message } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import { getBalance } from '#utils/database/fun/economy';
import { getBalance } from '#utils/database/economy';
import { EmbedBuilder } from 'discord.js';
export class BalanceCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'balance',

View File

@@ -21,12 +21,12 @@ import { Command, RegisterBehavior } from '@sapphire/framework';
import { Time } from '@sapphire/time-utilities';
import type { User, Guild, GuildMember, Message } from 'discord.js';
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 IDs from '#utils/ids';
export class DailyCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'daily',

View File

@@ -20,12 +20,12 @@
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Guild, Message } from 'discord.js';
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 IDs from '#utils/ids';
export class BalanceCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
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 { EmbedBuilder, GuildMember } from 'discord.js';
import { EmbedBuilder } from 'discord.js';
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 {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: '1984',
description: 'this is literally 1984',
preconditions: [['CoordinatorOnly', 'ModOnly']],
preconditions: ['ModOnly'],
});
}
@@ -45,26 +45,10 @@ export class N1984Command extends Command {
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the user
const { member } = interaction;
const { user } = interaction;
// Type checks
if (!(member instanceof GuildMember)) {
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!`;
}
await addFunLog(user.id, '1984');
const count = await countTotal(user.id, '1984');
// Creates the embed for the 1984 reaction
// 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)];
const n1984Embed = new EmbedBuilder()
.setColor('#ffffff')
.setTitle(`${member.displayName} is happy!`)
.setTitle(`${user.username} is happy!`)
.setImage(random1984)
.setFooter({ text: embedFooter });
.setFooter({ text: `${user.username}'s 1984 count: ${count}` });
// Send the embed
await interaction.reply({ embeds: [n1984Embed], fetchReply: true });

View File

@@ -18,12 +18,12 @@
*/
import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js';
import { EmbedBuilder } from 'discord.js';
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 {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'cringe',
@@ -44,34 +44,19 @@ export class CringeCommand extends Command {
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the user
const { member } = interaction;
// TODO exception handling
const { user } = interaction;
// Type check
if (!(member instanceof GuildMember)) {
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!`;
}
await addFunLog(user.id, 'cringe');
const count = await countTotal(user.id, 'cringe');
// Creates the embed for the cringe reaction
const randomCringe = Cringe[Math.floor(Math.random() * Cringe.length)];
const cringeEmbed = new EmbedBuilder()
.setColor('#001148')
.setTitle(`${member.displayName} feels immense cringe...`)
.setTitle(`${user.username} feels immense cringe...`)
.setImage(randomCringe)
.setFooter({ text: embedFooter });
.setFooter({ text: `${user.username}'s cringe count: ${count}` });
// Send the embed
await interaction.reply({ embeds: [cringeEmbed], fetchReply: true });

View File

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

View File

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

View File

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

View File

@@ -18,12 +18,12 @@
*/
import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js';
import { EmbedBuilder } from 'discord.js';
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 {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'poke',
@@ -54,42 +54,20 @@ export class PokeCommand extends Command {
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the users
const user = interaction.options.getUser('user', true)!;
const sender = interaction.member;
// Type checks
if (!(sender instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
const sender = interaction.user;
await addFunLog(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
const randomPoke = Poke[Math.floor(Math.random() * Poke.length)];
const pokeEmbed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(`Poke from ${sender.displayName}`)
.setTitle(`Poke from ${sender.username}`)
.setImage(randomPoke)
.setFooter({ text: embedFooter });
.setFooter({
text: `Amount of pokes from ${sender.username} to you: ${count}`,
});
// Send the poke
await interaction.reply({

View File

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

View File

@@ -18,15 +18,16 @@
*/
import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js';
import { EmbedBuilder } from 'discord.js';
import { Shrug } from '#utils/gifs';
export class ShrugCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'shrug',
description: 'Ugh... whatever... idk...',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
});
}
@@ -43,22 +44,15 @@ export class ShrugCommand extends Command {
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the user
const { member } = interaction;
// Type checks
if (!(member instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
// TODO exception handling
const member = interaction.member!.user;
const memberGuildMember = interaction.guild!.members.cache.get(member.id)!;
// Creates the embed for the shrug reaction
const randomShrug = Shrug[Math.floor(Math.random() * Shrug.length)];
const shrugEmbed = new EmbedBuilder()
.setColor('#001980')
.setTitle(`${member.displayName} shrugs`)
.setTitle(`${memberGuildMember.displayName} shrugs`)
.setImage(randomShrug);
// 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 { EmbedBuilder } from 'discord.js';
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 {
checkTempBan,
removeTempBan,
} from '#utils/database/moderation/tempBan';
import { checkTempBan, removeTempBan } from '#utils/database/tempBan';
export class BanCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
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 { EmbedBuilder, Message } from 'discord.js';
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';
export class TempBanCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'tempban',
@@ -258,13 +258,11 @@ export class TempBanCommand extends Command {
await addTempBan(userId, modId, time.fromNow, reason);
// Create scheduled task to unban
await this.container.tasks.create(
this.container.tasks.create(
'tempBan',
{
name: 'tempBan',
payload: {
userId: user.id,
guildId: guild.id,
},
userId: user.id,
guildId: guild.id,
},
time.offset,
);

View File

@@ -28,15 +28,12 @@ import type {
} from 'discord.js';
import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids';
import { removeBan, checkBan, addBan } from '#utils/database/moderation/ban';
import {
checkTempBan,
removeTempBan,
} from '#utils/database/moderation/tempBan';
import { removeBan, checkBan, addBan } from '#utils/database/ban';
import { checkTempBan, removeTempBan } from '#utils/database/tempBan';
import { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser';
export class UnbanCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'unban',

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,15 +21,15 @@ import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { ChannelType, EmbedBuilder } from 'discord.js';
import type { Message, TextChannel, Guild, Snowflake } from 'discord.js';
import IDs from '#utils/ids';
import { getRestrictions } from '#utils/database/moderation/restriction';
import { getRestrictions } from '#utils/database/restriction';
import { checkStaff } from '#utils/checker';
export class RestrictLogsCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'restrictlogs',
description: 'Shows restriction history for a user',
description: 'Unrestricts a user',
preconditions: ['ModOnly'],
});
}
@@ -75,9 +75,10 @@ export class RestrictLogsCommand extends Command {
userId = user.id;
}
const staffChannel = checkStaff(channel);
if (staffChannel) {
let staffChannel = false;
if (channel.type === ChannelType.GuildText) {
channel = channel as TextChannel;
staffChannel = checkStaff(channel);
if (userId === null) {
let topic: string[];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,37 +20,30 @@
import { RegisterBehavior, Args } from '@sapphire/framework';
import { Subcommand } from '@sapphire/plugin-subcommands';
import {
ChannelType,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
User,
Guild,
TextChannel,
GuildMember,
Snowflake,
MessageFlagsBitField,
} 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 {
addSusNoteDB,
addToDatabase,
findNotes,
getNote,
deactivateNote,
deactivateAllNotes,
} from '#utils/database/moderation/sus';
} from '#utils/database/sus';
import { checkStaff } from '#utils/checker';
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
export class SusCommand extends Subcommand {
public constructor(
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
public constructor(context: Subcommand.Context, options: Subcommand.Options) {
super(context, {
...options,
name: 'sus',
@@ -58,8 +51,8 @@ export class SusCommand extends Subcommand {
{
name: 'add',
default: true,
chatInputRun: 'addNoteChatInput',
messageRun: 'addNoteMessage',
chatInputRun: 'addNote',
messageRun: 'addMessage',
},
{
name: 'view',
@@ -147,9 +140,7 @@ export class SusCommand extends Subcommand {
}
// Subcommand to add sus note
public async addNoteChatInput(
interaction: Subcommand.ChatInputCommandInteraction,
) {
public async addNote(interaction: Subcommand.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const note = interaction.options.getString('note', true);
@@ -160,127 +151,40 @@ export class SusCommand extends Subcommand {
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
flags: MessageFlagsBitField.Flags.Ephemeral,
ephemeral: true,
});
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
await addSusNoteDB(user.id, mod.id, note);
// Gives the sus role to the user
await this.addSusRole(user, guild);
// Check if the user exists on the database
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}`;
info.success = true;
// 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');
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) {
if (member === undefined || modMember === undefined) {
await interaction.reply({
content: 'Error fetching users!',
ephemeral: true,
});
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
if (!member.roles.cache.has(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) {
@@ -292,12 +196,19 @@ export class SusCommand extends Subcommand {
if (guild == null) {
await interaction.reply({
content: 'Error fetching guild!',
flags: MessageFlagsBitField.Flags.Ephemeral,
ephemeral: true,
});
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
const notes = await findNotes(user.id, true);
@@ -306,35 +217,61 @@ export class SusCommand extends Subcommand {
if (notes.length === 0) {
await interaction.reply({
content: `${user} has no sus notes!`,
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
return;
}
// 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
await interaction.reply({
embeds: [noteEmbed],
flags: staffChannel ? undefined : MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: !staffChannel,
fetchReply: true,
});
}
public async removeNote(interaction: Subcommand.ChatInputCommandInteraction) {
// Get the arguments
const noteId = interaction.options.getInteger('id', true);
const mod = interaction.user;
const { guild, channel } = interaction;
// Checks if all the variables are of the right type
if (guild === null || channel === null) {
await interaction.reply({
content: 'Error fetching guild or channel!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
return;
}
@@ -346,46 +283,45 @@ export class SusCommand extends Subcommand {
if (note === null) {
await interaction.reply({
content: 'Error fetching note from database!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
return;
}
const userId = note.userId;
const modId = note.modId;
// Get user GuildMembers for user and mod and person who ran command
let user = guild.client.users.cache.get(userId);
if (!(user instanceof User)) {
user = await guild.client.users.fetch(userId).catch(() => undefined);
}
if (user === undefined) {
const member = await guild.members.cache.get(note.userId);
const mod = await guild.members.cache.get(note.modId);
// TODO fix if user left the server
if (member === undefined || mod === undefined) {
await interaction.reply({
content: 'Error fetching user!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
content: 'Error fetching users from Discord!',
ephemeral: true,
fetchReply: true,
});
return;
}
let modCreator = guild.client.users.cache.get(modId);
if (!(modCreator instanceof User)) {
modCreator = await guild.client.users.fetch(modId).catch(() => undefined);
// Get user's name
let userName = note.userId;
if (member !== undefined) {
userName = member.displayName;
}
let modCreatorDisplay = modId;
if (modCreator instanceof User) {
modCreatorDisplay = modCreator.displayName;
// Get mod name
let modName = note.modId;
if (mod !== undefined) {
modName = mod.displayName;
}
// Create an embed for the note
const noteEmbed = new EmbedBuilder()
.setColor('#ff0000')
.setTitle(`Sus note for ${user.tag}`)
.setThumbnail(user.displayAvatarURL())
.setTitle(`Sus note for ${userName}`)
.setThumbnail(member.displayAvatarURL())
.addFields({
name: `ID: ${noteId} | Moderator: ${modCreatorDisplay} | Date: <t:${Math.floor(
name: `ID: ${noteId} | Moderator: ${modName} | Date: <t:${Math.floor(
note.time.getTime() / 1000,
)}>`,
value: note.note,
@@ -407,21 +343,16 @@ export class SusCommand extends Subcommand {
const message = await interaction.reply({
embeds: [noteEmbed],
components: [buttons],
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
// Checks if the message is not an APIMessage
if (message.resource === null) {
if (!isMessageInstance(message)) {
await interaction.editReply('Failed to retrieve the message :(');
return;
}
if (!channel.isSendable()) {
await interaction.editReply('Cannot send messages in this channel!');
return;
}
// Listen for the button presses
const collector = channel.createMessageComponentCollector({
max: 1, // Maximum of 1 button press
@@ -433,28 +364,18 @@ export class SusCommand extends Subcommand {
if (button.customId === `delete${noteId}`) {
await deactivateNote(noteId);
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: [],
});
// 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
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
if (notes.length === 0) {
let member = guild.members.cache.get(userId);
if (!(member instanceof GuildMember)) {
member = await guild.members.fetch(userId).catch(() => undefined);
}
if (member instanceof GuildMember) {
await member.roles.remove(IDs.roles.restrictions.sus);
}
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(
interaction: Subcommand.ChatInputCommandInteraction,
) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const mod = interaction.user;
const { guild, channel } = interaction;
// Checks if all the variables are of the right type
if (guild === null || channel === null) {
await interaction.reply({
content: 'Error fetching guild or channel!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
return;
}
@@ -536,8 +410,8 @@ export class SusCommand extends Subcommand {
if (member === undefined) {
await interaction.reply({
content: 'Error fetching user!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
return;
}
@@ -550,8 +424,8 @@ export class SusCommand extends Subcommand {
if (notes.length === 0) {
await interaction.reply({
content: `${user} had no notes!`,
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
return;
}
@@ -601,21 +475,16 @@ export class SusCommand extends Subcommand {
const message = await interaction.reply({
embeds: [noteEmbed],
components: [buttons],
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
// Checks if the message is not an APIMessage
if (message.resource === null) {
if (!isMessageInstance(message)) {
await interaction.editReply('Failed to retrieve the message :(');
return;
}
if (!channel.isSendable()) {
await interaction.editReply('Cannot send messages in this channel!');
return;
}
// Listen for the button presses
const collector = channel.createMessageComponentCollector({
max: 1, // Maximum of 1 button press
@@ -632,8 +501,6 @@ export class SusCommand extends Subcommand {
embeds: [],
});
}
await this.deleteAllNotesLogger(user, mod, guild);
});
// 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);
}
// Logs removal of 1 sus note
private async deleteAllNotesLogger(user: User, mod: User, guild: Guild) {
// Log the sus note
let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as
| TextChannel
| undefined;
// Non Application Command method of adding a sus note
// xlevra begged me to add this... so I guess here it is
public async addMessage(message: Message, args: Args) {
// Get arguments
let user: GuildMember;
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) {
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;
}
if (note === null) {
await message.react('❌');
await message.reply('No sus note was provided!');
return;
}
const embed = new EmbedBuilder()
.setColor('#28A745')
.setAuthor({
name: `Purged all sus notes for ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
)
.setTimestamp()
.setFooter({ text: `ID: ${user.id}` });
if (mod === null) {
await message.react('❌');
await message.reply(
'Moderator not found! Try again or contact a developer!',
);
return;
}
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 type { GuildMember, Message } from 'discord.js';
import {
addMute,
removeMute,
checkActive,
} from '#utils/database/moderation/vcMute';
import { addMute, removeMute, checkActive } from '#utils/database/vcMute';
import { addExistingUser } from '#utils/database/dbExistingUser';
export class VCMuteCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
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 { RegisterBehavior } from '@sapphire/framework';
import { ChannelType, PermissionsBitField, Snowflake } from 'discord.js';
import type { Snowflake } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import {
addStatUser,
@@ -38,10 +38,7 @@ import IDs from '#utils/ids';
import { EmbedBuilder } from 'discord.js';
export class OutreachCommand extends Subcommand {
public constructor(
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
public constructor(context: Subcommand.Context, options: Subcommand.Options) {
super(context, {
...options,
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) {
await interaction.reply({
content: 'Outreach Leader was not found!',
content: 'Mod was not found!',
ephemeral: true,
});
return;
}
if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) {
if (!mod.roles.cache.has(IDs.roles.staff.outreachCoordinator)) {
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,
});
return;
@@ -253,9 +251,9 @@ export class OutreachCommand extends Subcommand {
return;
}
if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) {
if (!mod.roles.cache.has(IDs.roles.staff.outreachCoordinator)) {
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,
});
return;
@@ -274,8 +272,7 @@ export class OutreachCommand extends Subcommand {
stat.forEach(({ role }) => {
if (role !== null) {
guild.roles.delete(role.roleId); // Delete role
guild.channels.delete(role.channelId); // Delete VC
guild.roles.delete(role.roleId);
}
});
@@ -388,66 +385,14 @@ export class OutreachCommand extends Subcommand {
await updateUser(leaderMember);
// Create role for group
const role = await guild.roles.create({
name: `Outreach Group ${groupNo}`,
mentionable: true,
});
// Create a voice channel for group
const channel = await guild.channels.create({
name: `Outreach Group ${groupNo}`,
type: ChannelType.GuildVoice,
parent: IDs.categories.activism,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
PermissionsBitField.Flags.ViewChannel,
],
},
{
id: IDs.roles.vegan.activist,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: role.id, // Permissions for the specific group
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
],
},
{
id: IDs.roles.staff.outreachLeader,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
],
},
],
});
await createStat(event.id, leader.id, role.id);
// Create stats in database
await createStat(event.id, leader.id, role.id, channel.id);
// Give group leader 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({
content: `Created a group with the leader being ${leader}`,
});
@@ -494,7 +439,7 @@ export class OutreachCommand extends Subcommand {
if (
leader.id !== stat.stat.leaderId &&
!leaderMember.roles.cache.has(IDs.roles.staff.outreachLeader)
!leaderMember.roles.cache.has(IDs.roles.staff.outreachCoordinator)
) {
await interaction.editReply({
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';
export class BookClubCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'bookclub',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,10 +18,9 @@
*/
import { Command, RegisterBehavior } from '@sapphire/framework';
import IDs from '#utils/ids';
export class InfoCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'info',
@@ -95,9 +94,7 @@ export class InfoCommand extends Command {
message =
"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." +
"\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.';
"\n\nIf there aren't any verifiers available, you'll be disconnected, and you can rejoin later.";
break;
case 'modMail':
message =

View File

@@ -17,11 +17,12 @@
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 { Message, MessageFlagsBitField } from 'discord.js';
import type { Message } from 'discord.js';
export class PingCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'ping',
@@ -40,13 +41,12 @@ export class PingCommand extends Command {
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const msg = await interaction.reply({
content: 'Ping?',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
ephemeral: true,
fetchReply: true,
});
if (msg.resource !== null && msg.resource.message !== null) {
const diff =
msg.resource.message.createdTimestamp - interaction.createdTimestamp;
if (isMessageInstance(msg)) {
const diff = msg.createdTimestamp - interaction.createdTimestamp;
const ping = Math.round(this.container.client.ws.ping);
return interaction.editReply(
`Pong 🏓! (Round trip took: ${diff}ms. Heartbeat: ${ping}ms.)`,
@@ -57,11 +57,6 @@ export class PingCommand extends Command {
}
public async messageRun(message: Message) {
if (!message.channel.isSendable()) {
// TODO manage logging/errors properly
return;
}
const msg = await message.channel.send('Ping?');
const diff = msg.createdTimestamp - message.createdTimestamp;

View File

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

View File

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

View File

@@ -20,10 +20,10 @@
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Guild, Message } 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 {
public constructor(context: Command.LoaderContext, options: Command.Options) {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'rank',

View File

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

View File

@@ -21,18 +21,12 @@ import {
InteractionHandler,
InteractionHandlerTypes,
} from '@sapphire/framework';
import {
ButtonInteraction,
GuildMember,
MessageFlagsBitField,
} from 'discord.js';
import type { PieceContext } from '@sapphire/framework';
import type { ButtonInteraction, GuildMember } from 'discord.js';
import IDs from '#utils/ids';
export class NonVeganAccessButtonHandler extends InteractionHandler {
public constructor(
ctx: InteractionHandler.LoaderContext,
options: InteractionHandler.Options,
) {
public constructor(ctx: PieceContext, options: InteractionHandler.Options) {
super(ctx, {
...options,
interactionHandlerType: InteractionHandlerTypes.Button,
@@ -55,7 +49,7 @@ export class NonVeganAccessButtonHandler extends InteractionHandler {
if (member === null) {
await interaction.reply({
content: errorMessage,
flags: MessageFlagsBitField.Flags.Ephemeral,
ephemeral: true,
});
return;
}
@@ -66,7 +60,7 @@ export class NonVeganAccessButtonHandler extends InteractionHandler {
if (!member.roles.cache.has(IDs.roles.vegan.vegan)) {
await interaction.reply({
content: 'You need to be vegan to use this button!',
flags: MessageFlagsBitField.Flags.Ephemeral,
ephemeral: true,
});
return;
}
@@ -77,7 +71,7 @@ export class NonVeganAccessButtonHandler extends InteractionHandler {
content:
'Your access from the non vegan section has been removed. ' +
'If you want to gain access again, click this button again.',
flags: MessageFlagsBitField.Flags.Ephemeral,
ephemeral: true,
});
return;
}
@@ -87,13 +81,13 @@ export class NonVeganAccessButtonHandler extends InteractionHandler {
content:
'Your access to the non vegan section has been given back. ' +
'If you want to remove access again, click this button again.',
flags: MessageFlagsBitField.Flags.Ephemeral,
ephemeral: true,
});
} catch (error) {
this.container.logger.error(`Non Vegan Access Interaction: ${error}`);
await interaction.reply({
content: errorMessage,
flags: MessageFlagsBitField.Flags.Ephemeral,
ephemeral: true,
});
}
}

View File

@@ -21,20 +21,12 @@ import {
InteractionHandler,
InteractionHandlerTypes,
} from '@sapphire/framework';
import {
ButtonInteraction,
GuildMember,
MessageFlagsBitField,
} from 'discord.js';
import type { PieceContext } from '@sapphire/framework';
import type { ButtonInteraction, GuildMember, TextChannel } from 'discord.js';
import IDs from '#utils/ids';
import { checkActive } from '#utils/database/moderation/restriction';
import { addUser } from '#utils/database/dbExistingUser';
export class WelcomeButtonHandler extends InteractionHandler {
public constructor(
ctx: InteractionHandler.LoaderContext,
options: InteractionHandler.Options,
) {
public constructor(ctx: PieceContext, options: InteractionHandler.Options) {
super(ctx, {
...options,
interactionHandlerType: InteractionHandlerTypes.Button,
@@ -48,101 +40,49 @@ export class WelcomeButtonHandler extends InteractionHandler {
}
public async run(interaction: ButtonInteraction) {
const { member } = interaction;
let general = this.container.client.channels.cache.get(
let { member } = interaction;
const general = this.container.client.channels.cache.get(
IDs.channels.nonVegan.general,
);
// Messages that are used multiple times
const roleErrorMessage =
'There was an error giving you the role, please try again later or contact ModMail to be let into this server.';
const welcomeMessage =
`${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.` +
`\n\nIf you are vegan, you can join the 'Verification' voice channel, or use \`/apply\` with the Appy bot in <#${IDs.channels.nonVegan.vcText}>, ` +
'to be verified and gain access to more channels.';
// Checks if general is not in the cache
) as TextChannel | undefined;
if (general === undefined) {
// Sends an API request to get the channel
const generalFetch = await this.container.client.channels
.fetch(IDs.channels.nonVegan.general)
.catch(() => undefined);
return;
}
// If general does not exist
if (generalFetch === null || generalFetch === undefined) {
this.container.logger.error(
'WelcomeButtonHandler: Could not find and fetch the general channel!',
if (member === null) {
await interaction.reply({
content:
'There was an error giving you the role, please try again later or contact ModMail to be let into this server.',
ephemeral: true,
});
return;
}
try {
member = member as GuildMember;
// Give non-vegan role
if (!member.voice.channel) {
await member.roles.add(IDs.roles.nonvegan.nonvegan);
await general.send(
`${member} Welcome to ARA! :D Please check <#${IDs.channels.information.roles}> ` +
`and remember to follow the <#${IDs.channels.information.conduct}> and to respect ongoing discussion and debates.` +
"\n\nIf you would like to be verified as a vegan, join the 'Verification' voice channel.",
);
await interaction.reply({
content:
'Sorry there was a problem trying to give you access to the server. Please try again later.',
flags: MessageFlagsBitField.Flags.Ephemeral,
});
return;
}
// Replace fetched version of general with the cached version
general = generalFetch;
}
// If the member could not be found
if (!(member instanceof GuildMember)) {
await interaction.reply({
content: roleErrorMessage,
flags: MessageFlagsBitField.Flags.Ephemeral,
});
return;
}
// 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
if (member.voice.channel) {
await interaction.reply({
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.",
flags: MessageFlagsBitField.Flags.Ephemeral,
ephemeral: true,
});
return;
}
// Add the user to the database
await addUser(member.id);
// Give the role to the member
const role = await member.roles
.add(IDs.roles.nonvegan.nonvegan)
.catch(() => undefined);
// If the role could not be given
if (role === undefined) {
} catch (error) {
await interaction.reply({
content: roleErrorMessage,
flags: MessageFlagsBitField.Flags.Ephemeral,
content:
'There was an error giving you the role, please try again later or contact ModMail to be let into this server.',
ephemeral: true,
});
return;
}
if (general.isSendable()) {
await general.send(welcomeMessage);
} else {
this.container.logger.error(
'WelcomeButtonHandler: The bot does not have permission to send in general!',
);
await member.send(welcomeMessage);
}
}
}

View File

@@ -20,15 +20,12 @@
import { Listener } from '@sapphire/framework';
import type { GuildBan } 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 { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser';
export class BanListener extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
event: 'guildBanAdd',

View File

@@ -19,14 +19,11 @@
import { Listener } from '@sapphire/framework';
import type { GuildMember } from 'discord.js';
import { checkBan, getBanReason } from '#utils/database/moderation/ban';
import { checkTempBan } from '#utils/database/moderation/tempBan';
import { checkBan, getBanReason } from '#utils/database/ban';
import { checkTempBan } from '#utils/database/tempBan';
export class BanJoinListener extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
event: 'guildMemberAdd',

View File

@@ -20,15 +20,12 @@
import { Listener } from '@sapphire/framework';
import type { GuildBan } 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 { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser';
export class UnbanListener extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
event: 'guildBanRemove',

View File

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

View File

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

View File

@@ -21,14 +21,11 @@
import { Listener } from '@sapphire/framework';
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';
export class XpListener extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
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 (lastCount === null) {
if (this.container.client.id === null) {
if (!message.channel.isSendable()) {
// TODO manage logging/errors properly
return;
}
message.channel.send(
'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();
if (lastCount === null) {
if (!message.channel.isSendable()) {
// TODO manage logging/errors properly
return;
}
message.channel.send(
'An unexpected error occurred, please contact a developer!',
);

View File

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

View File

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

View File

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

View File

@@ -1,189 +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: false,
});
}
public async run(client: Client) {
this.container.logger.info(
'FixRolesOnReady: Preparation before starting to fix the roles for each user...',
);
// Fetching the Guild
const guild = await client.guilds.fetch(IDs.guild).catch(() => undefined);
if (guild === undefined) {
this.container.logger.error('FixRolesOnReady: Could not find the server');
return;
}
// Fetching the channel for the logs
// Leave the snowflake parameter empty for no logs
const logChannel = await client.channels.fetch('').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) {
// Update the counter for the total number of users processed
count += 1;
// Send a message with an update for every 50 completions
// Checks if `channelLog` has been set to null
// The RHS of the modulo should be around 100
if (sendLogs && count % 250 === 0) {
const currentTime = new Date().getTime();
const runningTime = currentTime - startTime;
const remaining = totalMembers - count;
// Basing this on the fact that
const eta = remaining * (runningTime / count);
const estimate = new DurationFormatter().format(eta);
logChannel.send(
`Given roles to ${count} out of ${totalMembers} members. Estimated time until completion: ${estimate}`,
);
}
// Checks if the user already has vegan or non-vegan role
// 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.restricted3) ||
member.roles.cache.has(IDs.roles.restrictions.restricted4) ||
member.roles.cache.has(IDs.roles.restrictions.restrictedVegan)
) {
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);
}
}
// Give the roles to the member
if (roles.length > 0) {
await member.roles.add(roles);
}
// Log the completion
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(1000);
}
// 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 { ChannelType } from 'discord.js';
import type { GuildChannel, EmbedBuilder } from 'discord.js';
import { ChannelType, EmbedBuilder } from 'discord.js';
import type { GuildChannel } from 'discord.js';
import { setTimeout } from 'timers/promises';
import IDs from '#utils/ids';
import {
checkActive,
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';
import { checkActive, getRestrictions } from '#utils/database/restriction';
import { findNotes } from '#utils/database/sus';
export class ModMailCreateListener extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
event: 'channelCreate',
@@ -63,16 +51,6 @@ export class ModMailCreateListener extends Listener {
// Get the user's ID
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
if (!(await checkActive(userId))) return;
@@ -82,21 +60,81 @@ export class ModMailCreateListener extends Listener {
// Creation of embeds
// Restriction Logs
const embeds: EmbedBuilder[] = [];
embeds.push(createRestrictLogEmbed(restrictions, user, guild));
const restrictEmbed = new EmbedBuilder()
.setColor('#FF6700')
.setTitle(`${restrictions.length} restrictions`)
.setFooter({ text: `ID: ${userId}` });
// Warnings
const warnings = await fetchWarnings(userId);
// Add up to 10 of the latest restrictions to the embed
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
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
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';
export class NonVeganAccessReady extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
once: true,

View File

@@ -22,13 +22,9 @@
import { Listener } from '@sapphire/framework';
import type { Client } from 'discord.js';
import IDs from '#utils/ids';
export class ReadyListener extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
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!;
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 { fetchRoles, getLeaveRoles } from '#utils/database/dbExistingUser';
import { blockTime } from '#utils/database/verification';
import {
checkActive,
getSection,
} from '#utils/database/moderation/restriction';
import { checkActive, getSection } from '#utils/database/restriction';
import { blockedRoles, blockedRolesAfterRestricted } from '#utils/blockedRoles';
import IDs from '#utils/ids';
export class RolesJoinServerListener extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
event: 'guildMemberAdd',

View File

@@ -23,10 +23,7 @@ import type { Message } from 'discord.js';
import IDs from '#utils/ids';
export class Suggestions extends Listener {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
event: 'messageCreate',
@@ -83,11 +80,6 @@ export class Suggestions extends Listener {
return;
}
if (!mailbox.isSendable()) {
// TODO manage logging/errors properly
return;
}
const sent = await mailbox.send({
embeds: [suggestion],
content: message.author.toString(),

View File

@@ -19,13 +19,10 @@
import { Listener } from '@sapphire/framework';
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 {
public constructor(
context: Listener.LoaderContext,
options: Listener.Options,
) {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
event: 'voiceStateUpdate',

View File

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

View File

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

View File

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

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