Merge pull request #68 from veganhacktivists/verification

feat(arabot): new verification system
This commit is contained in:
Anthony Berg 2022-10-20 22:38:20 +01:00 committed by GitHub
commit 57b17d6e28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2531 additions and 666 deletions

View File

@ -1,4 +1,4 @@
FROM node:18-buster FROM node:18
WORKDIR /opt/app WORKDIR /opt/app
@ -11,6 +11,8 @@ RUN npm install
COPY . . COPY . .
RUN npx prisma generate
RUN npm run build RUN npm run build
RUN chown node:node /opt/app/ RUN chown node:node /opt/app/

949
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,31 +26,31 @@
}, },
"homepage": "https://github.com/veganhacktivists/arabot#readme", "homepage": "https://github.com/veganhacktivists/arabot#readme",
"dependencies": { "dependencies": {
"@discordjs/builders": "^1.2.0", "@discordjs/builders": "^1.3.0",
"@prisma/client": "^4.0.0", "@prisma/client": "^4.0.0",
"@sapphire/discord.js-utilities": "^5.0.0", "@sapphire/discord.js-utilities": "^5.0.0",
"@sapphire/framework": "^3.1.1", "@sapphire/framework": "^3.1.3",
"@sapphire/plugin-scheduled-tasks": "^4.0.0", "@sapphire/plugin-scheduled-tasks": "^4.0.0",
"@sapphire/plugin-subcommands": "^3.0.0", "@sapphire/plugin-subcommands": "^3.0.0",
"@sapphire/stopwatch": "^1.4.1", "@sapphire/stopwatch": "^1.4.1",
"@sapphire/ts-config": "^3.3.4",
"@sapphire/utilities": "^3.9.2", "@sapphire/utilities": "^3.9.2",
"@types/node": "^18.0.3", "@types/node": "^18.0.3",
"bullmq": "^1.89.1", "bullmq": "^1.89.1",
"discord-api-types": "^0.33.3", "discord-api-types": "^0.33.3",
"discord.js": "^13.10.3", "discord.js": "^13.12.0",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"prisma": "^4.2.1",
"ts-node": "^10.8.2", "ts-node": "^10.8.2",
"typescript": "^4.7.4" "typescript": "^4.7.4"
}, },
"devDependencies": { "devDependencies": {
"@sapphire/ts-config": "^3.3.4",
"@types/ioredis": "^4.28.10", "@types/ioredis": "^4.28.10",
"@typescript-eslint/eslint-plugin": "^5.30.7", "@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^5.30.7", "@typescript-eslint/parser": "^5.30.7",
"eslint": "8.22.0", "eslint": "8.22.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-airbnb-typescript": "^17.0.0",
"eslint-plugin-import": "^2.26.0" "eslint-plugin-import": "^2.26.0",
"prisma": "^4.4.0"
} }
} }

View File

@ -0,0 +1,14 @@
/*
Warnings:
- You are about to drop the column `balance` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `lastDaily` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `level` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `xp` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "balance",
DROP COLUMN "lastDaily",
DROP COLUMN "level",
DROP COLUMN "xp";

View File

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

View File

@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `time` on the `VerifyUnblock` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Verify" DROP COLUMN "time",
ADD COLUMN "finishTime" TIMESTAMP(3),
ADD COLUMN "startTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Verify" ADD COLUMN "joinTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN "startTime" DROP NOT NULL;

View File

@ -0,0 +1,14 @@
/*
Warnings:
- The primary key for the `VerifyUnblock` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `channelId` on the `VerifyUnblock` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Verify" DROP CONSTRAINT "Verify_pkey",
DROP COLUMN "channelId",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "Verify_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Verify_id_seq";

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Verify" ALTER COLUMN "startTime" DROP DEFAULT;

View File

@ -0,0 +1,10 @@
-- AlterTable
ALTER TABLE "Verify" ADD COLUMN "activist" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "food" INTEGER,
ADD COLUMN "length" INTEGER,
ADD COLUMN "life" INTEGER,
ADD COLUMN "reason" INTEGER,
ADD COLUMN "reasoning" INTEGER,
ADD COLUMN "trusted" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "vegCurious" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "where" INTEGER;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Verify" ADD COLUMN "convinced" BOOLEAN NOT NULL DEFAULT false;

View File

@ -27,10 +27,6 @@ datasource db {
model User { model User {
id String @id @db.VarChar(255) id String @id @db.VarChar(255)
level Int @default(0)
xp Int @default(0)
balance Int @default(0)
lastDaily DateTime?
vegan Boolean @default(false) vegan Boolean @default(false)
trusted Boolean @default(false) trusted Boolean @default(false)
activist Boolean @default(false) activist Boolean @default(false)
@ -52,16 +48,31 @@ model User {
} }
model Verify { model Verify {
id Int @id @default(autoincrement()) id String @id
user User @relation("verUser", fields: [userId], references: [id]) user User @relation("verUser", fields: [userId], references: [id])
userId String userId String
verifier User? @relation("verVerifier", fields: [verifierId], references: [id]) verifier User? @relation("verVerifier", fields: [verifierId], references: [id])
verifierId String? verifierId String?
time DateTime @default(now()) joinTime DateTime @default(now())
timedOut Boolean @default(false) // If they got kicked out of verification because they timed out startTime DateTime?
vegan Boolean @default(false) // If they got verified as a vegan finishTime DateTime?
text Boolean @default(false) // If they used text verification timedOut Boolean @default(false) // If they got kicked out of verification because they timed out
serverVegan Boolean @default(false) // People that went vegan on the server //complete Boolean @default(false) // If the verification was incomplete
// Roles they got from verification
vegan Boolean @default(false) // If they got verified as a vegan
activist Boolean @default(false) // If they got the activist role when they verified
trusted Boolean @default(false) // If they got the trusted role when they verified
vegCurious Boolean @default(false) // If they got the Veg Curious role
convinced Boolean @default(false)
text Boolean @default(false) // If they used text verification
serverVegan Boolean @default(false) // People that went vegan on the server
// Stats on verification
reason Int?
where Int?
length Int?
reasoning Int?
life Int?
food Int?
notes String? notes String?
} }

View File

@ -21,113 +21,19 @@ import { Command, RegisterBehavior, Args } from '@sapphire/framework';
import { import {
MessageEmbed, MessageActionRow, MessageButton, Constants, ButtonInteraction, MessageEmbed, MessageActionRow, MessageButton, Constants, ButtonInteraction,
} from 'discord.js'; } from 'discord.js';
import type { Message, GuildMember } from 'discord.js';
import { PrismaClient } from '@prisma/client';
import { isMessageInstance } from '@sapphire/discord.js-utilities'; import { isMessageInstance } from '@sapphire/discord.js-utilities';
import { addExistingUser, userExists } from '../../utils/dbExistingUser'; import { addExistingUser, userExists } from '../../utils/database/dbExistingUser';
import {
addToDatabase,
findNotes,
getNote,
deactivateNote,
deactivateAllNotes,
} from '../../utils/database/sus';
import IDs from '../../utils/ids'; import IDs from '../../utils/ids';
// TODO add a check when they join the server to give the user the sus role again // TODO add a check when they join the server to give the user the sus role again
async function addToDatabase(userId: string, modId: string, message: string) {
// Initialise the database connection
const prisma = new PrismaClient();
// Add the user to the database
await prisma.sus.create({
data: {
user: {
connect: {
id: userId,
},
},
mod: {
connect: {
id: modId,
},
},
note: message,
},
});
// Close the database connection
await prisma.$disconnect();
}
// Get a list of sus notes from the user
async function findNotes(userId: string, active: boolean) {
// Initialise the database connection
const prisma = new PrismaClient();
// Query to get the specific user's sus notes
const note = await prisma.sus.findMany({
where: {
userId,
active,
},
});
// Close the database connection
await prisma.$disconnect();
return note;
}
// Get one note from the id
async function getNote(noteId: number) {
// Initialise the database connection
const prisma = new PrismaClient();
// Query to get the specific user's sus notes
const note = await prisma.sus.findUnique({
where: {
id: noteId,
},
});
// Close the database connection
await prisma.$disconnect();
return note;
}
async function deactivateNote(noteId: number) {
// Initialise the database connection
const prisma = new PrismaClient();
// Query to deactivate the specific sus note
await prisma.sus.update({
where: {
id: noteId,
},
data: {
active: false,
},
});
// Close the database connection
await prisma.$disconnect();
}
async function deactivateAllNotes(userId: string) {
// Initialise the database connection
const prisma = new PrismaClient();
// Query to deactivate the specific user's sus notes
await prisma.sus.updateMany({
where: {
userId: {
contains: userId,
},
},
data: {
active: false,
},
});
// Close the database connection
await prisma.$disconnect();
}
// Main command
class SusCommand extends Command { class SusCommand extends Command {
public constructor(context: Command.Context) { public constructor(context: Command.Context) {
super(context, { super(context, {

View File

@ -0,0 +1,50 @@
// 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 { Listener } from '@sapphire/framework';
import type { GuildMember } from 'discord.js';
import { fetchRoles } from '../../utils/database/dbExistingUser';
import IDs from '../../utils/ids';
import { blockTime } from '../../utils/database/verification';
class VerificationReady extends Listener {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
once: true,
event: 'guildMemberAdd',
});
}
public async run(user: GuildMember) {
// Add basic roles
const roles = await fetchRoles(user.id);
// Check if the user has a verification block
const timeout = await blockTime(user.id);
if (timeout > 0) {
roles.push(IDs.roles.verifyBlock);
}
// Add roles if they don't have verification block
await user.roles.add(roles);
}
}
export default VerificationReady;

View File

@ -0,0 +1,720 @@
// 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 { container, Listener } from '@sapphire/framework';
import type {
CategoryChannel,
ColorResolvable,
TextChannel,
VoiceChannel,
VoiceState,
GuildMember,
Guild,
User,
} from 'discord.js';
import {
ButtonInteraction,
Constants,
MessageActionRow,
MessageButton,
MessageEmbed,
} from 'discord.js';
import { time } from '@discordjs/builders';
import { maxVCs, questionInfo, serverFind } from '../../utils/verificationConfig';
import { joinVerification, startVerification, finishVerification } from '../../utils/database/verification';
import { findNotes } from '../../utils/database/sus';
import { userExists, addExistingUser } from '../../utils/database/dbExistingUser';
import { rolesToString } from '../../utils/formatter';
import IDs from '../../utils/ids';
class VerificationJoinVCListener extends Listener {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
event: 'voiceStateUpdate',
});
}
public async run(oldState: VoiceState, newState: VoiceState) {
// If the event was not a user joining the channel
if (oldState.channel?.parent?.id === IDs.categories.verification
|| newState.channel?.parent?.id !== IDs.categories.verification
) {
return;
}
// Variable if this channel is a Verifiers only VC
let verifier = false;
// Checks for not null
const { channel } = newState;
const { member } = newState;
const { client } = container;
const guild = client.guilds.cache.get(newState.guild.id);
if (channel === null || member === null || guild === undefined) {
console.error('Verification channel not found');
return;
}
// Get current category and channel
const categoryGuild = guild.channels.cache.get(IDs.categories.verification);
const currentChannelGuild = guild.channels.cache.get(channel.id);
if (currentChannelGuild === undefined || categoryGuild === undefined) {
console.error('Verification channel not found');
return;
}
const currentChannel = currentChannelGuild as VoiceChannel;
const category = categoryGuild as CategoryChannel;
const roles = rolesToString(member.roles.cache.map((r) => r.id));
// Checks if a verifier has joined
if (channel.members.size === 2) {
await newState.channel!.permissionOverwrites.set([
{
id: guild.roles.everyone,
allow: ['SEND_MESSAGES'],
},
]);
return;
}
// Check if a verifier joined a verification VC and update database
if (channel.members.size === 2) {
if (!channel.name.includes(' - Verification')) {
return;
}
await startVerification(channel.id);
return;
}
// Checks if there is more than one person who has joined or if the channel has members
if (channel.members.size !== 1
|| !channel.members.has(member.id)) {
return;
}
// Check if the user has the verifiers role
if (member.roles.cache.has(IDs.roles.staff.verifier)
|| member.roles.cache.has(IDs.roles.staff.trialVerifier)) {
await channel.setName('Verifier Meeting');
verifier = true;
} else {
await channel.setName(`${member.displayName} - Verification`);
await currentChannel.send(`Hiya ${member.user}, please be patient as a verifier has been called out to verify you.\n\n`
+ 'If you leave this voice channel, you will automatically be given the non-vegan role where you gain access to this server and if you\'d like to verify as a vegan again, you\'d have to contact a Mod, which could be done via ModMail.');
// Adds to the database that the user joined verification
await joinVerification(channel.id, member);
// Remove all roles from the user
await member.roles.remove([
IDs.roles.vegan.vegan,
IDs.roles.trusted,
IDs.roles.nonvegan.nonvegan,
IDs.roles.nonvegan.convinced,
IDs.roles.nonvegan.vegCurious,
]);
// Start 15-minute timer if verifier does not join
// @ts-ignore
this.container.tasks.create('verifyTimeout', {
channelId: channel.id,
userId: member.id,
}, 900_000); // 15 minutes
}
// Check how many voice channels there are
const listVoiceChannels = category.children.filter((c) => c.type === 'GUILD_VOICE');
// Create a text channel for verifiers only
// Checks if there are more than 10 voice channels
if (!verifier) {
const verificationText = await guild.channels.create(`✅┃${member.displayName}-verification`, {
type: 'GUILD_TEXT',
topic: `Channel for verifiers only. ${member.id} ${channel.id} (Please do not change this)`,
parent: category.id,
userLimit: 1,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
{
id: IDs.roles.verifyBlock,
deny: ['VIEW_CHANNEL', 'SEND_MESSAGES'],
},
{
id: IDs.roles.staff.verifier,
allow: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
],
});
// Send a message that someone wants to be verified
const userInfoEmbed = await this.getUserInfo(member, roles);
const susNotes = await this.getSus(member, guild);
await verificationText.send({
content: `${member.user} wants to be verified in ${channel}
\n<@&${IDs.roles.staff.verifier}> <@&${IDs.roles.staff.trialVerifier}>`,
embeds: [userInfoEmbed, susNotes],
});
await this.verificationProcess(verificationText, channel.id, member, guild);
}
// Create a new channel for others to join
// Checks if there are more than 10 voice channels
if (listVoiceChannels.size > maxVCs - 1) {
await guild.channels.create('Verification', {
type: 'GUILD_VOICE',
parent: category.id,
userLimit: 1,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
{
id: IDs.roles.verifyBlock,
deny: ['VIEW_CHANNEL', 'CONNECT', 'SEND_MESSAGES'],
},
{
id: IDs.roles.nonvegan.nonvegan,
allow: ['VIEW_CHANNEL'],
deny: ['CONNECT'],
},
{
id: IDs.roles.vegan.vegan,
allow: ['VIEW_CHANNEL'],
deny: ['CONNECT'],
},
{
id: IDs.roles.vegan.activist,
deny: ['VIEW_CHANNEL', 'CONNECT'],
},
{
id: IDs.roles.staff.verifier,
allow: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
],
});
} else {
await guild.channels.create('Verification', {
type: 'GUILD_VOICE',
parent: category.id,
userLimit: 1,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
{
id: IDs.roles.verifyBlock,
deny: ['VIEW_CHANNEL', 'CONNECT', 'SEND_MESSAGES'],
},
{
id: IDs.roles.nonvegan.nonvegan,
allow: ['VIEW_CHANNEL'],
},
{
id: IDs.roles.vegan.vegan,
allow: ['VIEW_CHANNEL'],
},
{
id: IDs.roles.vegan.activist,
deny: ['VIEW_CHANNEL', 'CONNECT'],
},
{
id: IDs.roles.staff.verifier,
allow: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
],
});
}
// Change permissions to join the current channel
await currentChannel.permissionOverwrites.set([
{
id: guild.roles.everyone,
deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
{
id: IDs.roles.nonvegan.nonvegan,
deny: ['VIEW_CHANNEL'],
},
{
id: IDs.roles.vegan.vegan,
deny: ['VIEW_CHANNEL'],
},
{
id: member.id,
allow: ['VIEW_CHANNEL'],
},
]);
await currentChannel.setUserLimit(0);
}
// Creates an embed for information about the user
private async getUserInfo(user: GuildMember, roles: string) {
const joinTime = time(user.joinedAt!);
const registerTime = time(user.user.createdAt);
const embed = new MessageEmbed()
.setColor(user.displayHexColor)
.setTitle(`Information on ${user.user.username}`)
.setThumbnail(user.user.avatarURL()!)
.addFields(
{ name: 'Joined:', value: `${joinTime}`, inline: true },
{ name: 'Created:', value: `${registerTime}`, inline: true },
{ name: 'Roles:', value: roles },
);
return embed;
}
// Creates the embed to display the sus note
private async getSus(user: GuildMember, guild: Guild) {
const notes = await findNotes(user.id, true);
const embed = new MessageEmbed()
.setColor(user.displayHexColor)
.setTitle(`${notes.length} sus notes for ${user.user.username}`);
// 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
embed.addFields({
name: `Sus ID: ${notes[i].id} | Moderator: ${mod} | Date: <t:${Math.floor(notes[i].time.getTime() / 1000)}>`,
value: notes[i].note,
});
}
return embed;
}
private async verificationProcess(
channel: TextChannel,
verId: string,
user: GuildMember,
guild: Guild,
) {
const embedColor = '#0099ff';
const info = {
page: 0,
find: {
reason: 0,
where: 0,
},
length: 0,
reasoning: 0,
life: 0,
food: 0,
roles: {
vegan: false,
activist: false,
trusted: false,
vegCurious: false,
convinced: false,
},
};
// TODO add a variable that tells if each order has a reversed value, e.g. 0-3 or 3-0
const questionLength = questionInfo.length;
let embed = await this.createEmbed(questionInfo[0].question, embedColor);
let buttons = await this.createButtons(questionInfo[0].buttons);
// Sends the note to verify this note is to be deleted
const message = await channel.send({
embeds: [embed],
components: buttons,
});
// Listen for the button presses
const collector = channel.createMessageComponentCollector({
// max: 2, // Maximum of 1 button press
});
// Button pressed
collector.on('collect', async (button: ButtonInteraction) => {
// Select roles
if (button.customId.includes('button')) {
await button.deferUpdate();
// Get the button choice
const buttonChoice = this.getButtonValue(button.customId);
if (Number.isNaN(buttonChoice)) {
return;
}
// Set the value of the button choice to the page the question was on
switch (info.page) {
case 0: {
info.find.reason = buttonChoice;
if (buttonChoice !== 0 && info.find.reason === 0) {
embed = await this.createEmbed(serverFind[info.page].question, embedColor);
buttons = await this.createButtons(serverFind[info.page].buttons);
await message.edit({
embeds: [embed],
components: buttons,
});
return;
}
if (info.find.reason !== 0) {
info.find.where = buttonChoice;
}
break;
}
case 1: {
info.length = buttonChoice;
break;
}
case 2: {
info.reasoning = buttonChoice;
break;
}
case 3: {
info.life = buttonChoice;
break;
}
case 4: {
info.food = buttonChoice;
break;
}
// If they are definitely vegan or not
case 5: {
if (buttonChoice === 0) {
info.roles.vegan = true;
info.roles.trusted = true;
} else {
info.page += 1;
}
break;
}
// If they are vegan but should get activist role
case 6: {
if (buttonChoice === 0) {
info.roles.activist = true;
}
info.page += 1;
break;
}
// If they should get vegan, convinced or non-vegan
case 7: {
if (buttonChoice === 0) {
info.roles.vegan = true;
} else if (buttonChoice === 1) {
info.roles.convinced = true;
}
break;
}
case 8: {
if (buttonChoice === 0) {
info.roles.vegCurious = true;
}
break;
}
default: {
console.error('Button clicked out of range');
return;
}
}
info.page += 1;
// Checks if finished all the questions
if (info.page < questionLength) {
embed = await this.createEmbed(questionInfo[info.page].question, embedColor);
buttons = await this.createButtons(questionInfo[info.page].buttons);
await message.edit({
embeds: [embed],
components: buttons,
});
}
// Confirmation to give roles to the user being verified
if (info.page === questionLength) {
// Create embed with all the roles the user has
embed = new MessageEmbed()
.setColor(embedColor)
.setTitle(`Give these roles to ${user.displayName}?`)
.setThumbnail(user.avatarURL()!)
.addFields(
{ name: 'Roles:', value: this.getTextRoles(info.roles) },
);
// Create buttons for input
buttons = [new MessageActionRow<MessageButton>()
.addComponents(
new MessageButton()
.setCustomId('confirm')
.setLabel('Yes')
.setStyle(Constants.MessageButtonStyles.SUCCESS),
new MessageButton()
.setCustomId('cancel')
.setLabel('No')
.setStyle(Constants.MessageButtonStyles.DANGER),
)];
await message.edit({
embeds: [embed],
components: buttons,
});
}
}
// Confirming and finishing the verification
if (button.customId === 'confirm' && info.page >= questionLength) {
// Check verifier is on the database
const verifierGuildMember = await guild.members.cache.get(button.user.id);
if (verifierGuildMember === undefined) {
await message.edit({ content: 'Verifier not found!' });
return;
}
// Add verifier to database if they're not on the database
if (!(await userExists(verifierGuildMember))) {
await addExistingUser(verifierGuildMember);
}
// Add verification data to database
await finishVerification(verId, button.user.id, info);
// Give roles on Discord
await this.giveRoles(user, info.roles);
// Add timeout if they do not have activist role
if (!info.roles.activist) {
// @ts-ignore
this.container.tasks.create('verifyUnblock', {
userId: user.id,
guildId: guild.id,
}, (info.roles.vegan || info.roles.convinced) ? 604800000 : 1814400000);
}
// Add embed saying verification completed
embed = new MessageEmbed()
.setColor('#34c000')
.setTitle(`Successfully verified ${user.displayName}!`)
.setThumbnail(user.user.avatarURL()!)
.addFields(
{ name: 'Roles:', value: this.getTextRoles(info.roles) },
);
await message.edit({
embeds: [embed],
components: [],
});
// Send welcome message after verification
await this.finishMessages(user.user, info.roles);
}
if (button.customId === 'cancel' && info.page >= questionLength) {
info.page = 5;
info.roles.vegan = false;
info.roles.activist = false;
info.roles.trusted = false;
info.roles.vegCurious = false;
info.roles.convinced = false;
embed = await this.createEmbed(questionInfo[info.page].question, embedColor);
buttons = await this.createButtons(questionInfo[info.page].buttons);
await message.edit({
embeds: [embed],
components: buttons,
});
await button.deferUpdate();
}
});
}
private async createEmbed(title: string, color: ColorResolvable) {
return new MessageEmbed()
.setColor(color)
.setTitle(title);
}
private async createButtons(buttons: string[]) {
const buttonActions = [];
for (let i = 0; i < buttons.length; i += 1) {
// Check if it exceeds the maximum buttons in a ActionRow
if (i % 5 === 0) {
buttonActions.push(new MessageActionRow<MessageButton>());
}
buttonActions[Math.floor(i / 5)]
.addComponents(
new MessageButton()
.setCustomId(`button${i}`)
.setLabel(buttons[i])
.setStyle(Constants.MessageButtonStyles.SECONDARY),
);
}
return buttonActions;
}
// Finds the value of the choice in the button
private getButtonValue(button: string) {
const buttonChoice = button.at(button.length - 1);
if (buttonChoice === undefined) {
return NaN;
}
return parseInt(buttonChoice, 10);
}
private getTextRoles(
roles: {
vegan: boolean,
activist: boolean,
trusted: boolean,
vegCurious: boolean,
convinced: boolean
},
) {
let rolesText = '';
if (roles.convinced) {
rolesText += `<@&${IDs.roles.nonvegan.convinced}>`;
}
if (roles.vegan) {
rolesText += `<@&${IDs.roles.vegan.vegan}>`;
} else {
rolesText += `<@&${IDs.roles.nonvegan.nonvegan}>`;
}
if (roles.activist) {
rolesText += `<@&${IDs.roles.vegan.activist}>`;
}
if (roles.trusted) {
rolesText += `<@&${IDs.roles.trusted}>`;
}
if (roles.vegCurious) {
rolesText += `<@&${IDs.roles.nonvegan.vegCurious}>`;
}
return rolesText;
}
private async giveRoles(
user: GuildMember,
roles: {
vegan: boolean,
activist: boolean,
trusted: boolean,
vegCurious: boolean,
convinced: boolean
},
) {
const rolesAdd = [];
if (roles.convinced) {
rolesAdd.push(IDs.roles.nonvegan.convinced);
}
if (roles.vegan) {
rolesAdd.push(IDs.roles.vegan.vegan);
} else {
rolesAdd.push(IDs.roles.nonvegan.nonvegan);
}
if (roles.activist) {
rolesAdd.push(IDs.roles.vegan.activist);
} else {
rolesAdd.push(IDs.roles.verifyBlock);
}
if (roles.trusted) {
rolesAdd.push(IDs.roles.trusted);
}
if (roles.vegCurious) {
rolesAdd.push(IDs.roles.nonvegan.vegCurious);
}
await user.roles.add(rolesAdd);
}
// Messages after verifying the user
private async finishMessages(user: User, roles: {
vegan: boolean,
activist: boolean,
trusted: boolean,
vegCurious: boolean,
convinced: boolean
}) {
// Send a DM with when their verification is finished
await this.finishDM(user, roles)
.catch(() => console.error('Verification: Closed DMs'));
// Not vegan
if (!roles.vegan) {
const general = this.container.client.channels.cache.get(IDs.channels.nonVegan.general) as TextChannel | undefined;
if (general === undefined) {
return;
}
let msg = `${user}, you have been verified! Please check <#${IDs.channels.information.roles}> `
+ `and remember to follow the <#${IDs.channels.information.conduct}> and to respect ongoing discussion and debates.`;
// Add extra info if the user got veg curious or convinced.
if (roles.vegCurious || roles.convinced) {
msg += `\n\nYou also have access to <#${IDs.channels.dietSupport.main}> for help on going vegan.`;
}
await general.send(msg);
return;
}
// Vegan
const general = this.container.client.channels.cache.get(IDs.channels.vegan.general) as TextChannel | undefined;
if (general === undefined) {
return;
}
const msg = `Welcome ${user}! Please check out <#${IDs.channels.information.roles}> :)`;
await general.send(msg);
// Activist role
if (roles.activist) {
const activist = this.container.client.channels.cache.get(IDs.channels.activism.activism) as TextChannel | undefined;
if (activist === undefined) {
return;
}
const activistMsg = `${user} you have been given the activist role! This means that if you'd wish to engage with non-vegans in `
+ `<#${IDs.channels.nonVegan.general}>, you should follow these rules:\n\n`
+ '1. Try to move conversations with non-vegans towards veganism/animal ethics\n'
+ '2. Don\'t discuss social topics while activism is happening\n'
+ '3. Have evidence for claims you make. "I don\'t know" is an acceptable answer. Chances are someone here knows or you can take time to find out\n'
+ '4. Don\'t advocate for baby steps towards veganism. Participation in exploitation can stop today\n'
+ '5. Differences in opinion between activists should be resolved in vegan spaces, not in the chat with non-vegans';
await activist.send(activistMsg);
}
}
// Messages after verifying the user
private async finishDM(user: User, roles: {
vegan: boolean,
activist: boolean,
trusted: boolean,
vegCurious: boolean,
convinced: boolean
}) {
if (!roles.vegan && !roles.convinced) {
const message = 'You\'ve been verified as non-vegan!'
+ `\n\nYou can next verify on ${time(Math.round(Date.now() / 1000) + 1814400)}`;
await user.send(message);
} else if (roles.convinced) {
const message = 'You\'ve been verified as convinced!'
+ `\n\nYou can next verify on ${time(Math.round(Date.now() / 1000) + 604800)}`;
await user.send(message);
} else if (roles.vegan && !roles.activist) {
const message = 'You\'ve been verified as a vegan!'
+ `\n\nYou can next get verified on ${time(Math.round(Date.now() / 1000) + 604800)} if you would wish to have the activist role.`;
await user.send(message);
}
}
}
export default VerificationJoinVCListener;

View File

@ -0,0 +1,183 @@
// 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 { container, Listener } from '@sapphire/framework';
import type {
VoiceState, CategoryChannel, VoiceChannel, TextChannel,
} from 'discord.js';
import { time } from '@discordjs/builders';
import { maxVCs, leaveBan } from '../../utils/verificationConfig';
import { getUser, checkFinish, countIncomplete } from '../../utils/database/verification';
import { fetchRoles } from '../../utils/database/dbExistingUser';
import { fibonacci } from '../../utils/mathsSeries';
import IDs from '../../utils/ids';
class VerificationLeaveVCListener extends Listener {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
event: 'voiceStateUpdate',
});
}
public async run(oldState: VoiceState, newState: VoiceState) {
// If the event was not a user joining the channel
if (oldState.channel?.parent?.id !== IDs.categories.verification
|| newState.channel?.parent?.id === IDs.categories.verification
|| oldState.channel.members.size > 0
) {
return;
}
let verifier = false;
// Check for undefined variables
const { client } = container;
const { channel } = oldState;
const guild = client.guilds.cache.get(newState.guild.id);
if (channel === null || guild === undefined) {
console.error('Verification channel not found');
return;
}
// Get the category
const categoryGuild = guild.channels.cache.get(IDs.categories.verification);
if (categoryGuild === null) {
console.error('Verification channel not found');
return;
}
const category = categoryGuild as CategoryChannel;
// Get the user that was being verified
const userSnowflake = await getUser(channel.id);
if (userSnowflake === null) {
verifier = true;
}
// Allow more people to join VC if there are less than 10 VCs
if (!verifier) {
const user = guild.members.cache.get(userSnowflake!)!;
// Remove verify as vegan and give non vegan role
if (!await checkFinish(channel.id)) {
await user.roles.remove(IDs.roles.verifyingAsVegan);
// Get roles to give back to the user
const roles = await fetchRoles(user.id);
roles.push(IDs.roles.verifyBlock);
await user.roles.add(roles);
// Create timeout block for user
// Counts the recent times they have incomplete verifications
const incompleteCount = await countIncomplete(user.id) % (leaveBan + 1);
// Creates the length of the time for the ban
const banLength = fibonacci(incompleteCount) * 3600_000;
// @ts-ignore
this.container.tasks.create('verifyUnblock', {
userId: user.id,
guildId: guild.id,
}, banLength);
await user.user.send('You have been timed out as a verifier had not joined for 15 minutes or you disconnected from verification.\n\n'
+ `You can verify again at: ${time(Math.round(Date.now() / 1000) + (banLength / 1000))}`)
.catch(() => console.error('Verification: Closed DMs'));
}
}
// Check how many voice channels there are
const listVoiceChannels = category.children.filter((c) => c.type === 'GUILD_VOICE');
// Check that it is not deleting the 'Verification' channel (in case bot crashes)
if (channel.name !== 'Verification') {
// Delete the channel
await channel.delete();
}
// Delete text channel
if (!verifier) {
// Gets a list of all the text channels in the verification category
const listTextChannels = category.children.filter((c) => c.type === 'GUILD_TEXT');
listTextChannels.forEach((c) => {
const textChannel = c as TextChannel;
// Checks if the channel topic has the user's snowflake
if (textChannel.topic!.includes(userSnowflake!)) {
textChannel.delete();
}
});
}
// If there are no VCs left in verification after having the channel deleted
if (listVoiceChannels.size === 0) {
// Create a verification channel
await guild.channels.create('Verification', {
type: 'GUILD_VOICE',
parent: IDs.categories.verification,
userLimit: 1,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
{
id: IDs.roles.verifyBlock,
deny: ['VIEW_CHANNEL', 'CONNECT', 'SEND_MESSAGES'],
},
{
id: IDs.roles.nonvegan.nonvegan,
allow: ['VIEW_CHANNEL'],
},
{
id: IDs.roles.vegan.vegan,
allow: ['VIEW_CHANNEL'],
},
{
id: IDs.roles.vegan.activist,
deny: ['VIEW_CHANNEL', 'CONNECT'],
},
{
id: IDs.roles.staff.verifier,
allow: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
],
});
}
// If there are less than 10, stop
if (listVoiceChannels.size < maxVCs) {
return;
}
const verification = listVoiceChannels.last() as VoiceChannel;
await verification!.permissionOverwrites.set([
{
id: IDs.roles.nonvegan.nonvegan,
allow: ['VIEW_CHANNEL'],
},
{
id: IDs.roles.vegan.vegan,
allow: ['VIEW_CHANNEL'],
},
]);
}
}
export default VerificationLeaveVCListener;

View File

@ -0,0 +1,111 @@
// 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 { Listener } from '@sapphire/framework';
import type {
Client,
CategoryChannel,
TextChannel,
VoiceChannel,
} from 'discord.js';
import IDs from '../../utils/ids';
class VerificationReady extends Listener {
public constructor(context: Listener.Context, options: Listener.Options) {
super(context, {
...options,
once: true,
event: 'ready',
});
}
public async run(client: Client) {
// Get verification category
let category = client.channels.cache.get(IDs.categories.verification) as CategoryChannel | undefined;
if (category === undefined) {
category = await client.channels.fetch(IDs.categories.verification) as CategoryChannel | undefined;
if (category === undefined) {
console.error('verifyStart: Channel not found');
return;
}
}
// Check how many voice channels there are
let voiceChannels = category.children.filter((c) => c.type === 'GUILD_VOICE');
const emptyVC: string[] = [];
// Delete voice channels
voiceChannels.forEach((c) => {
const voiceChannel = c as VoiceChannel;
if (voiceChannel.members.size === 0) {
emptyVC.push(voiceChannel.id);
voiceChannel.delete();
}
});
// Delete text channels
const textChannels = category.children.filter((c) => c.type === 'GUILD_TEXT');
textChannels.forEach((c) => {
const textChannel = c as TextChannel;
// Checks if the channel topic has the user's snowflake
emptyVC.forEach((snowflake) => {
if (textChannel.topic!.includes(snowflake)) {
textChannel.delete();
}
});
});
// Check if there is no voice channels, create verification
voiceChannels = category.children.filter((c) => c.type === 'GUILD_VOICE');
if (voiceChannels.size === emptyVC.length) {
await category.guild.channels.create('Verification', {
type: 'GUILD_VOICE',
parent: IDs.categories.verification,
userLimit: 1,
permissionOverwrites: [
{
id: category.guild.roles.everyone,
deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
{
id: IDs.roles.verifyBlock,
deny: ['VIEW_CHANNEL', 'CONNECT', 'SEND_MESSAGES'],
},
{
id: IDs.roles.nonvegan.nonvegan,
allow: ['VIEW_CHANNEL'],
},
{
id: IDs.roles.vegan.vegan,
allow: ['VIEW_CHANNEL'],
},
{
id: IDs.roles.vegan.activist,
deny: ['VIEW_CHANNEL', 'CONNECT'],
},
{
id: IDs.roles.staff.verifier,
allow: ['SEND_MESSAGES', 'VIEW_CHANNEL'],
},
],
});
}
}
}
export default VerificationReady;

View File

@ -0,0 +1,56 @@
// 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 type { VoiceChannel } from 'discord.js';
import { ScheduledTask } from '@sapphire/plugin-scheduled-tasks';
export class VerifyTimeout extends ScheduledTask {
public constructor(context: ScheduledTask.Context, options: ScheduledTask.Options) {
super(context, options);
}
public async run(payload: { channelId: string, userId: string }) {
// Get the guild where the user is in
let channel = this.container.client.channels.cache.get(payload.channelId) as VoiceChannel | undefined;
if (channel === undefined) {
channel = await this.container.client.channels.fetch(payload.channelId) as VoiceChannel | undefined;
if (channel === undefined) {
console.error('verifyTimeout: Channel not found!');
return;
}
}
if (channel.members.size < 2 && channel.members.has(payload.userId)) {
const user = channel.members.get(payload.userId);
if (user === undefined) {
console.error('verifyTimeout: GuildMember not found!');
return;
}
await user.voice.disconnect();
}
}
}
declare module '@sapphire/plugin-scheduled-tasks' {
interface ScheduledTasks {
verifyUnblock: never;
}
}
export default VerifyTimeout;

View File

@ -0,0 +1,60 @@
// 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 { ScheduledTask } from '@sapphire/plugin-scheduled-tasks';
import IDs from '../utils/ids';
export class VerifyUnblock extends ScheduledTask {
public constructor(context: ScheduledTask.Context, options: ScheduledTask.Options) {
super(context, options);
}
public async run(payload: { userId: string, guildId: string }) {
// Get the guild where the user is in
let guild = this.container.client.guilds.cache.get(payload.guildId);
if (guild === undefined) {
guild = await this.container.client.guilds.fetch(payload.guildId);
if (guild === undefined) {
console.error('verifyUnblock: Guild not found!');
return;
}
}
// Find GuildMember for the user
let user = guild.members.cache.get(payload.userId);
if (user === undefined) {
user = await guild.members.fetch(payload.userId);
if (user === undefined) {
console.error('verifyUnblock: GuildMember not found!');
return;
}
}
// Remove the 'verify block' role
await user.roles.remove(IDs.roles.verifyBlock);
}
}
declare module '@sapphire/plugin-scheduled-tasks' {
interface ScheduledTasks {
verifyUnblock: never;
}
}
export default VerifyUnblock;

View File

@ -0,0 +1,185 @@
// 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 type { GuildMember, GuildMemberRoleManager } from 'discord.js';
import { PrismaClient } from '@prisma/client';
import IDs from '../ids';
// Checks if the user exists on the database
export async function userExists(user: GuildMember) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// Counts if the user is on the database by their snowflake
const userQuery = await prisma.user.count({
where: {
id: user.id,
},
});
// Close the database connection
await prisma.$disconnect();
// If the user is found on the database, then return true, otherwise, false.
return userQuery > 0;
}
function getRoles(roles: GuildMemberRoleManager) {
// Checks what roles the user has
const rolesDict = {
vegan: roles.cache.has(IDs.roles.vegan.vegan),
activist: roles.cache.has(IDs.roles.vegan.activist),
plus: roles.cache.has(IDs.roles.vegan.plus),
notVegan: roles.cache.has(IDs.roles.nonvegan.nonvegan),
vegCurious: roles.cache.has(IDs.roles.nonvegan.vegCurious),
convinced: roles.cache.has(IDs.roles.nonvegan.convinced),
trusted: roles.cache.has(IDs.roles.trusted),
muted: roles.cache.has(IDs.roles.restrictions.muted),
};
return rolesDict;
}
// Adds the user to the database if they were already on the server before the bot/database
export async function addExistingUser(user: GuildMember) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// Counts if the user is on the database by their snowflake
const userQuery = await prisma.user.count({
where: {
id: user.id,
},
});
// If the user is already in the database
if (userQuery > 0) {
return;
}
// Parse all the roles into a dictionary
const roles = getRoles(user.roles);
// Create the user in the database
await prisma.user.create({
data: {
id: user.id,
vegan: roles.vegan,
trusted: roles.trusted,
activist: roles.activist,
plus: roles.plus,
notVegan: roles.notVegan,
vegCurious: roles.vegCurious,
convinced: roles.convinced,
muted: roles.muted,
},
});
// Close the database connection
await prisma.$disconnect();
}
export async function updateUser(user: GuildMember) {
// Check if the user is already on the database
if (!(await userExists(user))) {
await addExistingUser(user);
return;
}
// Parse all the roles into a dictionary
const roles = getRoles(user.roles);
// Initialises Prisma Client
const prisma = new PrismaClient();
await prisma.user.update({
where: {
id: user.id,
},
data: {
id: user.id,
vegan: roles.vegan,
trusted: roles.trusted,
activist: roles.activist,
plus: roles.plus,
notVegan: roles.notVegan,
vegCurious: roles.vegCurious,
convinced: roles.convinced,
muted: roles.muted,
},
});
// Close the database connection
await prisma.$disconnect();
}
export async function fetchRoles(user: string) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// Get the user's roles
const roleQuery = await prisma.user.findUnique({
where: {
id: user,
},
select: {
vegan: true,
trusted: true,
activist: true,
plus: true,
notVegan: true,
vegCurious: true,
convinced: true,
},
});
// Close the database connection
await prisma.$disconnect();
// Assign roles to role snowflakes
const roles = [];
if (roleQuery === null) {
roles.push('');
return roles;
}
if (roleQuery.vegan) {
roles.push(IDs.roles.vegan.vegan);
}
if (roleQuery.trusted) {
roles.push(IDs.roles.trusted);
}
if (roleQuery.activist) {
roles.push(IDs.roles.vegan.activist);
}
if (roleQuery.plus) {
roles.push(IDs.roles.vegan.plus);
}
if (roleQuery.notVegan) {
roles.push(IDs.roles.nonvegan.nonvegan);
}
if (roleQuery.vegCurious) {
roles.push(IDs.roles.nonvegan.vegCurious);
}
if (roleQuery.convinced) {
roles.push(IDs.roles.nonvegan.convinced);
}
return roles;
}

99
src/utils/database/sus.ts Normal file
View File

@ -0,0 +1,99 @@
import { PrismaClient } from '@prisma/client';
export async function addToDatabase(userId: string, modId: string, message: string) {
// Initialise the database connection
const prisma = new PrismaClient();
// Add the user to the database
await prisma.sus.create({
data: {
user: {
connect: {
id: userId,
},
},
mod: {
connect: {
id: modId,
},
},
note: message,
},
});
// Close the database connection
await prisma.$disconnect();
}
// Get a list of sus notes from the user
export async function findNotes(userId: string, active: boolean) {
// Initialise the database connection
const prisma = new PrismaClient();
// Query to get the specific user's sus notes
const note = await prisma.sus.findMany({
where: {
userId,
active,
},
});
// Close the database connection
await prisma.$disconnect();
return note;
}
// Get one note from the id
export async function getNote(noteId: number) {
// Initialise the database connection
const prisma = new PrismaClient();
// Query to get the specific user's sus notes
const note = await prisma.sus.findUnique({
where: {
id: noteId,
},
});
// Close the database connection
await prisma.$disconnect();
return note;
}
export async function deactivateNote(noteId: number) {
// Initialise the database connection
const prisma = new PrismaClient();
// Query to deactivate the specific sus note
await prisma.sus.update({
where: {
id: noteId,
},
data: {
active: false,
},
});
// Close the database connection
await prisma.$disconnect();
}
export async function deactivateAllNotes(userId: string) {
// Initialise the database connection
const prisma = new PrismaClient();
// Query to deactivate the specific user's sus notes
await prisma.sus.updateMany({
where: {
userId: {
contains: userId,
},
},
data: {
active: false,
},
});
// Close the database connection
await prisma.$disconnect();
}

View File

@ -0,0 +1,230 @@
// 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 type { GuildMember } from 'discord.js';
import { PrismaClient } from '@prisma/client';
import { updateUser } from './dbExistingUser';
import { leaveBan } from '../verificationConfig';
import { fibonacci } from '../mathsSeries';
export async function joinVerification(channelId: string, user: GuildMember) {
// Update the user on the database with the current roles they have
await updateUser(user);
// Initialises Prisma Client
const prisma = new PrismaClient();
await prisma.verify.create({
data: {
id: channelId,
user: {
connect: {
id: user.id,
},
},
},
});
// Close database connection
await prisma.$disconnect();
}
export async function startVerification(channelId: string) {
// Initialises Prisma Client
const prisma = new PrismaClient();
await prisma.verify.update({
where: {
id: channelId,
},
data: {
startTime: new Date(),
},
});
// Close database connection
await prisma.$disconnect();
}
export async function getUser(channelId: string) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// Get the snowflake of the user verifying
const user = await prisma.verify.findUnique({
where: {
id: channelId,
},
select: {
userId: true,
},
});
// Close database connection
await prisma.$disconnect();
// Check the user could be found
if (user === null) {
return null;
}
// Return the user's snowflake
return user.userId;
}
export async function finishVerification(
channelId: string,
verifierId: string,
info: {
page: number,
find: {
reason: number,
where: number
},
length: number,
reasoning: number,
life: number,
food: number,
roles: {
vegan: boolean,
activist: boolean,
trusted: boolean,
vegCurious: boolean,
convinced: boolean
} },
) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// TODO potentially add an incomplete tracker?
await prisma.verify.update({
where: {
id: channelId,
},
data: {
verifier: {
connect: {
id: verifierId,
},
},
finishTime: new Date(),
// Roles
vegan: info.roles.vegan,
activist: info.roles.activist,
trusted: info.roles.trusted,
vegCurious: info.roles.vegCurious,
convinced: info.roles.convinced,
// Statistics
reason: info.find.reason,
where: info.find.where,
length: info.length,
reasoning: info.reasoning,
life: info.life,
food: info.food,
},
});
// Close database connection
await prisma.$disconnect();
}
// Checks if verification was complete
export async function checkFinish(channelId: string) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// Get the snowflake of the user verifying
const finish = await prisma.verify.findUnique({
where: {
id: channelId,
},
select: {
finishTime: true,
},
});
// Close database connection
await prisma.$disconnect();
// Checks if query returned is null
if (finish === null) {
return false;
}
// Return if a finish time has been set meaning verification is complete
return finish.finishTime !== null;
}
// Counts how many times the user has not had a verifier join their VC before leaving
export async function countIncomplete(userId: string) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// Count how many times the user has not completed a verification
const incompleteCount = await prisma.verify.count({
where: {
userId,
finishTime: null,
},
});
// Close the database connection
await prisma.$disconnect();
return incompleteCount;
}
// Gets the amount of time left on the block
export async function blockTime(userId: string) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// Count how many times the user has not completed a verification
const verification = await prisma.verify.findFirst({
where: {
userId,
},
orderBy: {
id: 'desc',
},
});
// Close the database connection
await prisma.$disconnect();
if (verification === null) {
return 0;
}
// If user finished verification
if (verification.finishTime !== null) {
// Activist role
if (verification.activist) {
return 0;
}
const timeOff = new Date().getTime() - verification.finishTime.getTime();
return ((verification.vegan || verification.convinced) ? 604800000 : 1814400000) - timeOff;
}
// Timeouts
const count = await countIncomplete(verification.userId) % (leaveBan + 1);
const timeOff = new Date().getTime() - verification.joinTime.getTime();
// Creates the length of the time for the ban
return (fibonacci(count) * 3600_000) - timeOff;
}

View File

@ -1,87 +0,0 @@
// 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 type { GuildMember } from 'discord.js';
import { PrismaClient } from '@prisma/client';
import IDs from './ids';
// Checks if the user exists on the database
export async function userExists(user: GuildMember) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// Counts if the user is on the database by their snowflake
const userQuery = await prisma.user.count({
where: {
id: user.id,
},
});
// Close the database connection
await prisma.$disconnect();
// If the user is found on the database, then return true, otherwise, false.
return userQuery > 0;
}
// Adds the user to the database if they were already on the server before the bot/database
export async function addExistingUser(user: GuildMember) {
// Initialises Prisma Client
const prisma = new PrismaClient();
// Counts if the user is on the database by their snowflake
const userQuery = await prisma.user.count({
where: {
id: user.id,
},
});
// If the user is already in the database
if (userQuery > 0) {
return;
}
// Checks what roles the user has
const hasVegan = user.roles.cache.has(IDs.roles.vegan.vegan);
const hasActivist = user.roles.cache.has(IDs.roles.vegan.activist);
const hasPlus = user.roles.cache.has(IDs.roles.vegan.plus);
const hasNotVegan = user.roles.cache.has(IDs.roles.nonvegan.nonvegan);
const hasVegCurious = user.roles.cache.has(IDs.roles.nonvegan.vegCurious);
const hasConvinced = user.roles.cache.has(IDs.roles.nonvegan.convinced);
const hasTrusted = user.roles.cache.has(IDs.roles.trusted);
const hasMuted = user.roles.cache.has(IDs.roles.restrictions.muted);
// Create the user in the database
await prisma.user.create({
data: {
id: user.id,
vegan: hasVegan,
trusted: hasTrusted,
activist: hasActivist,
plus: hasPlus,
notVegan: hasNotVegan,
vegCurious: hasVegCurious,
convinced: hasConvinced,
muted: hasMuted,
},
});
// Close the database connection
await prisma.$disconnect();
}

View File

@ -60,15 +60,28 @@ const devIDs = {
channels: { channels: {
information: { information: {
news: '999431676058927247', news: '999431676058927247',
conduct: '999431676058927248',
roles: '999431676058927250',
}, },
staff: { staff: {
coordinators: '999431676058927254', coordinators: '999431676058927254',
standup: '999431676289622183', standup: '999431676289622183',
verifiers: '999431677006860411', verifiers: '999431677006860411',
}, },
dietSupport: {
info: '999431677006860417',
introduction: '999431677325615184',
main: '999431677325615185',
},
nonVegan: { nonVegan: {
general: '999431677325615189', general: '999431677325615189',
}, },
vegan: {
general: '999431677535338575',
},
activism: {
activism: '999431678214807604',
},
diversity: { diversity: {
women: '999431679053660187', women: '999431679053660187',
lgbtqia: '999431679053660188', lgbtqia: '999431679053660188',

34
src/utils/formatter.ts Normal file
View File

@ -0,0 +1,34 @@
// 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 type { Snowflake } from 'discord-api-types/globals';
export function rolesToString(roles: Snowflake[]) {
let output = '';
roles.forEach((role) => {
output += `<@&${role}>`;
});
if (output.length === 0) {
output = 'None';
}
return output;
}

View File

@ -52,23 +52,39 @@ let IDs = {
moderator: '826157475815489598', moderator: '826157475815489598',
trialModerator: '982074555596152904', trialModerator: '982074555596152904',
verifier: '871802735031373856', verifier: '871802735031373856',
trialVerifier: '982635638010572850',
}, },
stageHost: '854893757593419786', stageHost: '854893757593419786',
patron: '765370219207852055', patron: '765370219207852055',
patreon: '993848684640997406', patreon: '993848684640997406',
verifyingAsVegan: '854725899576279060', verifyingAsVegan: '854725899576279060',
verifyBlock: '1032765019269640203',
}, },
channels: { channels: {
information: { information: {
news: '866000393259319306', news: '866000393259319306',
conduct: '990728521531920385',
roles: '990761562199457813',
}, },
staff: { staff: {
coordinators: '1006240682505142354', coordinators: '1006240682505142354',
standup: '996009201237233684', standup: '996009201237233684',
verifiers: '873215538627756072',
},
dietSupport: {
info: '993891104346873888',
introduction: '993272252743286874',
main: '822665615612837918',
}, },
nonVegan: { nonVegan: {
general: '798967615636504657', general: '798967615636504657',
}, },
vegan: {
general: '787738272616808509',
},
activism: {
activism: '730907954877956179',
},
diversity: { diversity: {
women: '938808963544285324', women: '938808963544285324',
lgbtqia: '956224226556272670', lgbtqia: '956224226556272670',
@ -77,6 +93,7 @@ let IDs = {
}, },
}, },
categories: { categories: {
verification: '797505409073676299',
diversity: '933078380394459146', diversity: '933078380394459146',
}, },
}; };

34
src/utils/mathsSeries.ts Normal file
View File

@ -0,0 +1,34 @@
// 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/>.
*/
// Created because Stove loves Fibonacci sequences
// A fibonacci sequence where n = 0 => 1
export function fibonacci(position: number) {
let previous = 0;
let next = 1;
let tempNext;
for (let i = 0; i < position; i += 1) {
tempNext = next + previous;
previous = next;
next = tempNext;
}
return next;
}

View File

@ -0,0 +1,141 @@
// 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/>.
*/
// The maximum amount of verification VCs there can be
export const maxVCs = 10;
// The maximum amount of leaving bans before time resets
export const leaveBan = 8;
export const questionInfo = [
{
question: 'Welcome to Animal Rights Advocates! How did you find the server?',
buttons: [
'Search',
'Friend',
'YouTube',
'Another Server',
'Vegan Org',
],
},
{
question: 'How long have you been vegan?',
buttons: [
'<1 month',
'1-2 months',
'3-6 months',
'6 months - 1 year',
'1-2 years',
'2+ years',
],
},
{
question: 'Ask the user why they went vegan and to define veganism.\n'
+ 'Do they cite ethical concerns and abstinence from at least meat, dairy, eggs, leather, and fur?',
buttons: [
'Yes',
'Yes with prompting',
'No',
],
},
{
question: 'Ask the user about their life as a vegan, including things like watching documentaries or social media content and interactions with family and friends. What are their stories like?',
buttons: [
'Believable',
'Unbelievable',
'Short',
],
},
{
question: 'Ask the user about food and nutrition. Do they seem to know how to live as a vegan?',
buttons: [
'Dietitian / Chef',
'Acceptable',
'Salads / Smoothies',
'No clue',
],
},
{
question: 'Do you think this user is definitely vegan?',
buttons: [
'Yes',
'No',
],
},
{
question: 'Offer to ask questions for Activist. Do you think they should get it?',
buttons: [
'Yes',
'No',
],
},
{
question: 'Do some activism, asking Activist questions. Now which role should they get?',
buttons: [
'Vegan',
'Convinced',
'Non-vegan',
],
},
{
question: 'Should this user get Veg Curious?',
buttons: [
'Yes',
'No',
],
},
];
export const serverFind = [
// From a friend
{
question: 'Ask for username and indicate',
buttons: [
'Vegan',
'Non-Vegan',
'Unknown',
],
},
// From a video
{
question: 'Ask what video',
buttons: [
'Troll video',
'Our content',
'Other',
],
},
// From another server
{
question: 'Ask which server',
buttons: [
'Vegan',
'Debate',
'Other',
],
},
// From a vegan organisation
{
question: 'Ask which one',
buttons: [
'Vegan Hacktivists',
'Other',
],
},
];

View File

@ -12,7 +12,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
@ -101,7 +101,7 @@
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ // "skipLibCheck": true /* Skip type checking all .d.ts files. */
}, },
"include": ["src"] "include": ["src"]
} }