15 Commits

Author SHA1 Message Date
Anthony Berg
c76a514a2c feat(db): add table for logging clear commands 2024-08-06 20:48:19 +02:00
Anthony Berg
f77758c039 feat(arabot): allow mods to run. add a requirement needing delete perms 2024-08-06 20:47:31 +02:00
Anthony Berg
325dc0d0d0 refactor(arabot): run prettier 2024-08-04 20:14:44 +02:00
Anthony Berg
71a065d3ca feat(arabot): mention that vegans can access extra channels if verified 2024-08-04 20:05:24 +02:00
Anthony Berg
613f53491b build: update pnpm deps 2024-08-04 20:04:40 +02:00
Anthony
19721c10ea build(arabot): update deps 2024-06-27 11:23:09 +02:00
Anthony Berg
bd87a8b6c6 ci: add tsconfig and pnpm-lock to .prettierignore 2024-03-12 21:37:52 +00:00
Anthony Berg
46ef2fd8e2 fix(arabot): adding sus note to user who left server 2024-03-12 21:35:19 +00:00
Anthony Berg
d8c91fd39b refactor: run prettier 2024-03-12 21:34:51 +00:00
Anthony Berg
fabd381051 deps: update packages 2024-03-12 21:34:18 +00:00
Anthony Berg
a5758dc6ef feat(arabot): add vegan check to plus command 2024-02-16 21:28:18 +00:00
Anthony Berg
9ff5b78aff feat(arabot): allow verifiers to use plus command 2024-02-15 19:57:51 +00:00
Anthony Berg
f4655829e2 feat(arabot): make vcs when creating groups in outreach 2024-02-13 21:36:41 +00:00
Anthony Berg
c82d256be4 refactor(arabot): change outreach command for outreach leader only 2024-02-13 19:31:49 +00:00
Anthony Berg
a9039572d1 build: update deps 2024-02-07 20:39:39 +00:00
18 changed files with 1564 additions and 1236 deletions

View File

@@ -12,7 +12,6 @@ POSTGRES_DB=DB
# 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

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

View File

@@ -1,4 +1,4 @@
FROM node:20 AS base
FROM node:22 AS base
# PNPM
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

View File

@@ -31,33 +31,33 @@
"homepage": "https://github.com/veganhacktivists/arabot#readme",
"engines": {
"node": ">=20",
"pnpm": ">=8"
"pnpm": ">=9"
},
"packageManager": "pnpm@9.6.0",
"dependencies": {
"@prisma/client": "^5.9.1",
"@sapphire/discord.js-utilities": "^7.1.6",
"@sapphire/framework": "^5.0.7",
"@prisma/client": "^5.17.0",
"@sapphire/discord.js-utilities": "^7.3.0",
"@sapphire/framework": "^5.2.1",
"@sapphire/plugin-logger": "^4.0.2",
"@sapphire/plugin-scheduled-tasks": "^10.0.1",
"@sapphire/plugin-subcommands": "^6.0.3",
"@sapphire/stopwatch": "^1.5.2",
"@sapphire/time-utilities": "^1.7.12",
"@sapphire/ts-config": "^5.0.0",
"@sapphire/utilities": "^3.15.3",
"@types/node": "^20.11.16",
"bullmq": "^5.1.8",
"discord.js": "^14.14.1",
"redis": "^4.6.12",
"@sapphire/ts-config": "^5.0.1",
"@sapphire/utilities": "^3.17.0",
"bullmq": "^5.12.0",
"discord.js": "^14.15.3",
"ioredis": "^5.4.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "~5.4.5"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"@types/node": "^20.14.14",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "8.56.0",
"eslint-config-prettier": "^9.1.0",
"ioredis": "^5.3.2",
"prettier": "3.2.4",
"prisma": "^5.9.1"
"prisma": "^5.17.0"
}
}

2535
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -66,6 +66,7 @@ model User {
TempBanEndMod TempBan[] @relation("endTbanMod")
VCMuteUser VCMute[] @relation("vcMuteUser")
VCMuteMod VCMute[] @relation("vcMuteMod")
ClearCommandMod ClearCommand[]
}
model Verify {
@@ -222,9 +223,10 @@ model Stat {
}
model StatRole {
stat Stat @relation(fields: [statId], references: [id])
statId Int @id
roleId String
stat Stat @relation(fields: [statId], references: [id])
statId Int @id
roleId String
channelId String
}
model ParticipantStat {
@@ -311,3 +313,11 @@ model VCMute {
endTime DateTime?
reason String?
}
model ClearCommand {
id Int @id @default(autoincrement())
mod User @relation(fields: [modId], references: [id])
modId String
messages Int
time DateTime @default(now())
}

View File

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

View File

@@ -359,7 +359,7 @@ export class PrivateCommand extends Subcommand {
} 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)) {
} else if (user.roles.cache.has(IDs.roles.staff.hrCoordinator)) {
name = 'hr';
id = IDs.roles.staff.hrCoordinator;
} else {

View File

@@ -219,26 +219,11 @@ export class SusCommand extends Subcommand {
success: false,
};
// 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);
if (member === undefined) {
info.message = 'Error fetching user';
return info;
}
}
// Add the data to the database
await addSusNoteDB(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);
}
// Gives the sus role to the user
await this.addSusRole(user, guild);
info.message = `Added the sus note for ${user}: ${note}`;
info.success = true;
@@ -278,6 +263,26 @@ export class SusCommand extends Subcommand {
return info;
}
private async addSusRole(user: User, guild: Guild) {
// Get GuildMember for user to add a sus note for
let member = guild.members.cache.get(user.id);
// Checks if Member was not found in cache
if (member === undefined) {
// Fetches Member from API call to Discord
member = await guild.members.fetch(user.id).catch(() => undefined);
}
if (member === undefined) {
return;
}
// Give the user the sus role they don't already have the sus note
if (!member.roles.cache.has(IDs.roles.restrictions.sus)) {
await member.roles.add(IDs.roles.restrictions.sus);
}
}
public async listNote(interaction: Subcommand.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);

View File

@@ -19,7 +19,7 @@
import { Subcommand } from '@sapphire/plugin-subcommands';
import { RegisterBehavior } from '@sapphire/framework';
import type { Snowflake } from 'discord.js';
import { ChannelType, PermissionsBitField, Snowflake } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import {
addStatUser,
@@ -66,7 +66,6 @@ export class OutreachCommand extends Subcommand {
],
},
],
preconditions: ['ModOnly'],
});
}
@@ -200,15 +199,15 @@ export class OutreachCommand extends Subcommand {
if (mod === undefined) {
await interaction.reply({
content: 'Mod was not found!',
content: 'Outreach Leader was not found!',
ephemeral: true,
});
return;
}
if (!mod.roles.cache.has(IDs.roles.staff.outreachCoordinator)) {
if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) {
await interaction.reply({
content: 'You need to be an Outreach Coordinator to run this command!',
content: 'You need to be an Outreach Leader to run this command!',
ephemeral: true,
});
return;
@@ -254,9 +253,9 @@ export class OutreachCommand extends Subcommand {
return;
}
if (!mod.roles.cache.has(IDs.roles.staff.outreachCoordinator)) {
if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) {
await interaction.reply({
content: 'You need to be an Outreach Coordinator to run this command!',
content: 'You need to be an Outreach Leader to run this command!',
ephemeral: true,
});
return;
@@ -275,7 +274,8 @@ export class OutreachCommand extends Subcommand {
stat.forEach(({ role }) => {
if (role !== null) {
guild.roles.delete(role.roleId);
guild.roles.delete(role.roleId); // Delete role
guild.channels.delete(role.channelId); // Delete VC
}
});
@@ -388,14 +388,66 @@ export class OutreachCommand extends Subcommand {
await updateUser(leaderMember);
// Create role for group
const role = await guild.roles.create({
name: `Outreach Group ${groupNo}`,
mentionable: true,
});
await createStat(event.id, leader.id, role.id);
// Create a voice channel for group
const channel = await guild.channels.create({
name: `Outreach Group ${groupNo}`,
type: ChannelType.GuildVoice,
parent: IDs.categories.activism,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
PermissionsBitField.Flags.ViewChannel,
],
},
{
id: IDs.roles.vegan.activist,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: role.id, // Permissions for the specific group
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
],
},
{
id: IDs.roles.staff.outreachLeader,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
],
},
],
});
// Create stats in database
await createStat(event.id, leader.id, role.id, channel.id);
// Give group leader role
await leaderMember.roles.add(role);
// 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}`,
});
@@ -442,7 +494,7 @@ export class OutreachCommand extends Subcommand {
if (
leader.id !== stat.stat.leaderId &&
!leaderMember.roles.cache.has(IDs.roles.staff.outreachCoordinator)
!leaderMember.roles.cache.has(IDs.roles.staff.outreachLeader)
) {
await interaction.editReply({
content: `You are not the leader for ${group}`,

View File

@@ -29,7 +29,7 @@ export class PlusCommand extends Command {
name: 'plus',
aliases: ['+'],
description: 'Give/remove the plus role',
preconditions: [['CoordinatorOnly', 'ModOnly']],
preconditions: [['CoordinatorOnly', 'VerifierOnly', 'ModOnly']],
});
}
@@ -138,6 +138,14 @@ export class PlusCommand extends Command {
info.success = true;
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

@@ -25,8 +25,7 @@ import { LogLevel, SapphireClient, container } from '@sapphire/framework';
import '@sapphire/plugin-scheduled-tasks/register';
import '@sapphire/plugin-logger/register';
import { PrismaClient } from '@prisma/client';
import { createClient } from 'redis';
import type { RedisClientType } from 'redis';
import { Redis } from 'ioredis';
// Setting up the Sapphire client
const client = new SapphireClient({
@@ -50,7 +49,7 @@ const client = new SapphireClient({
tasks: {
bull: {
connection: {
host: process.env.BULLMQ_URL,
host: process.env.REDIS_URL,
},
},
},
@@ -64,10 +63,10 @@ const main = async () => {
// Create databases
container.database = await new PrismaClient();
container.redis = createClient({
url: process.env.REDIS_URL,
container.redis = new Redis({
host: process.env.REDIS_URL,
db: 1,
});
await container.redis.connect();
// Log the bot in to Discord
await client.login(token);
@@ -83,7 +82,7 @@ const main = async () => {
declare module '@sapphire/pieces' {
interface Container {
database: PrismaClient;
redis: RedisClientType;
redis: Redis;
}
}

View File

@@ -69,7 +69,7 @@ export class WelcomeButtonHandler extends InteractionHandler {
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 discussions and debates.` +
"\n\nIf you would like to be verified as a vegan, join the 'Verification' voice channel.",
"\n\nIf you are vegan, you can join the 'Verification' voice channel to be verified and gain access to more channels.",
);
return;
}

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2022 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 { AllFlowsPrecondition } from '@sapphire/framework';
import type {
CommandInteraction,
ContextMenuCommandInteraction,
Message,
GuildMember,
} from 'discord.js';
import IDs from '#utils/ids';
export class OutreachLeaderOnlyPrecondition extends AllFlowsPrecondition {
public override async messageRun(message: Message) {
// for message command
return this.checkCoordinator(message.member!);
}
public override async chatInputRun(interaction: CommandInteraction) {
// for slash command
return this.checkCoordinator(interaction.member! as GuildMember);
}
public override async contextMenuRun(
interaction: ContextMenuCommandInteraction,
) {
// for context menu command
return this.checkCoordinator(interaction.member! as GuildMember);
}
private async checkCoordinator(user: GuildMember) {
return user.roles.cache.has(IDs.roles.staff.outreachLeader)
? this.ok()
: this.error({ message: 'Only outreach leaders can run this command!' });
}
}
declare module '@sapphire/framework' {
interface Preconditions {
OutreachLeaderOnly: never;
}
}

View File

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

View File

@@ -60,6 +60,7 @@ const devIDs = {
outreachCoordinator: '999431675140382807',
mediaCoordinator: '1204801056404676618',
hrCoordinator: '1204795893480431657',
outreachLeader: '999431675123597409',
restricted: '999431675123597407',
moderator: '999431675123597408',
trialModerator: '999431675123597404',
@@ -132,6 +133,7 @@ const devIDs = {
staff: '999431676058927253',
modMail: '1095453371411996762',
verification: '999431677006860409',
activism: '999431677795389549',
diversity: '999431679053660185',
private: '999431679527628818',
restricted: '999431679812845654',

View File

@@ -62,6 +62,7 @@ let IDs = {
outreachCoordinator: '954804769476730890',
mediaCoordinator: '1203778509449723914',
hrCoordinator: '1203802120180989993',
outreachLeader: '730915698544607232',
restricted: '851624392928264222',
moderator: '826157475815489598',
trialModerator: '982074555596152904',
@@ -134,6 +135,7 @@ let IDs = {
staff: '768685283583328257',
modMail: '867077297664426006',
verification: '797505409073676299',
activism: '873918877019545640',
diversity: '933078380394459146',
private: '992581296901599302',
restricted: '809765577236283472',