Anthony Berg 132e3cc62b
Some checks failed
Prettier / Run prettier scanning (push) Successful in 2m35s
ESLint / Run eslint scanning (push) Failing after 24s
CodeQL / Analyze (javascript) (push) Failing after 58s
refactor(arabot): create separate functions for fetching and checking types of users, channels, etc
2025-01-19 16:56:07 +01:00

662 lines
20 KiB
TypeScript

// 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 { RegisterBehavior, Args } from '@sapphire/framework';
import { Subcommand } from '@sapphire/plugin-subcommands';
import {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
User,
Guild,
Snowflake,
MessageFlagsBitField,
} from 'discord.js';
import type { Message } from 'discord.js';
import {
addSusNoteDB,
findNotes,
getNote,
deactivateNote,
deactivateAllNotes,
} from '#utils/database/moderation/sus';
import { checkStaff } from '#utils/checker';
import IDs from '#utils/ids';
import { createSusLogEmbed } from '#utils/embeds';
import { getGuildMember, getTextBasedChannel, getUser } from '#utils/fetcher';
import { isUser } from '#utils/typeChecking';
import {
isGuildMember,
isTextBasedChannel,
isTextChannel,
} from '@sapphire/discord.js-utilities';
// 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,
) {
super(context, {
...options,
name: 'sus',
subcommands: [
{
name: 'add',
default: true,
chatInputRun: 'addNoteChatInput',
messageRun: 'addNoteMessage',
},
{
name: 'view',
chatInputRun: 'listNote',
},
{
name: 'remove',
chatInputRun: 'removeNote',
},
{
name: 'purge',
chatInputRun: 'removeAllNotes',
},
],
description: 'Notes about users that are sus',
preconditions: [['VerifierOnly', 'ModOnly']],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Subcommand.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
// Subcommand to add a sus note
.addSubcommand((command) =>
command
.setName('add')
.setDescription('Add a sus note about a user')
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to add the note')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('note')
.setDescription('Note about the user')
.setRequired(true),
),
)
// Subcommand to list sus notes
.addSubcommand((command) =>
command
.setName('view')
.setDescription('View a sus note for a user')
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to view the note of')
.setRequired(true),
),
)
// Subcommand to remove a specific sus note
.addSubcommand((command) =>
command
.setName('remove')
.setDescription('Remove a specific sus note')
.addIntegerOption((option) =>
option
.setName('id')
.setDescription('Sus note ID')
.setRequired(true),
),
)
// Subcommand to remove all sus notes
.addSubcommand((command) =>
command
.setName('purge')
.setDescription('Remove all sus notes from a user')
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to remove the note from')
.setRequired(true),
),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Subcommand to add sus note
public async addNoteChatInput(
interaction: Subcommand.ChatInputCommandInteraction,
) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const note = interaction.options.getString('note', 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!',
flags: MessageFlagsBitField.Flags.Ephemeral,
});
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);
info.message = `Added the sus note for ${user}: ${note}`;
info.success = true;
// Log the sus note
const logChannel = await getTextBasedChannel(IDs.channels.logs.sus);
if (!isTextBasedChannel(logChannel)) {
this.container.logger.error('Sus: 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;
} else if (!logChannel.isSendable()) {
this.container.logger.error(
'Sus: Does not have permission to message in the log channel.',
);
info.message = `Added a sus note for ${user} but could not send in the logs 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
const member = await getGuildMember(user.id, guild);
if (!isGuildMember(member)) {
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);
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
flags: MessageFlagsBitField.Flags.Ephemeral,
});
return;
}
const staffChannel = checkStaff(interaction.channel);
// Gets the sus notes from the database
const notes = await findNotes(user.id, true);
// Checks if there are no notes on the user
if (notes.length === 0) {
await interaction.reply({
content: `${user} has no sus notes!`,
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
// Creates the embed to display the sus note
const noteEmbed = createSusLogEmbed(notes, user, guild);
// Sends the notes to the user
await interaction.reply({
embeds: [noteEmbed],
flags: staffChannel ? undefined : MessageFlagsBitField.Flags.Ephemeral,
withResponse: 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 || !isTextBasedChannel(channel)) {
await interaction.reply({
content: 'Error fetching guild or channel!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
// Get the note to be deleted
const note = await getNote(noteId);
// Checks if managed to fetch the note
if (note === null) {
await interaction.reply({
content: 'Error fetching note from database!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
const userId = note.userId;
const modId = note.modId;
// Get user GuildMembers for user and mod and person who ran command
const user = await getUser(userId);
if (!isUser(user)) {
await interaction.reply({
content: 'Error fetching user!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
const modCreator = await getUser(modId);
const modCreatorDisplay = isUser(modCreator)
? modCreator.displayName
: modId;
// Create an embed for the note
const noteEmbed = new EmbedBuilder()
.setColor('#ff0000')
.setTitle(`Sus note for ${user.tag}`)
.setThumbnail(user.displayAvatarURL())
.addFields({
name: `ID: ${noteId} | Moderator: ${modCreatorDisplay} | Date: <t:${Math.floor(
note.time.getTime() / 1000,
)}>`,
value: note.note,
});
// Create buttons to delete or cancel the deletion
const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`delete${noteId}`)
.setLabel('Delete')
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`cancel${noteId}`)
.setLabel('Cancel')
.setStyle(ButtonStyle.Secondary),
);
// Sends the note to verify this note is to be deleted
const message = await interaction.reply({
embeds: [noteEmbed],
components: [buttons],
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
// Checks if the message is not an APIMessage
if (message.resource === null) {
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
time: 15000, // 15 seconds
});
// Button pressed
collector.on('collect', async (button: ButtonInteraction) => {
if (button.customId === `delete${noteId}`) {
await deactivateNote(noteId);
await interaction.editReply({
content: `${user}'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);
// Checks if there are no notes on the user and if there's none, remove the sus role
if (notes.length === 0) {
const member = guild.members.cache.get(userId);
if (isGuildMember(member)) {
await member.roles.remove(IDs.roles.restrictions.sus);
}
}
// Logs the removal of the sus note
await this.deleteNoteLogger(userId, mod, noteId);
}
});
// Remove the buttons after they have been clicked
collector.on('end', async () => {
await interaction.editReply({
components: [],
});
});
}
// Logs removal of 1 sus note
private async deleteNoteLogger(userId: Snowflake, mod: User, noteId: number) {
// Find user
const user = await getUser(userId);
if (!isUser(user)) return;
// Log the sus note
const logChannel = await getTextBasedChannel(IDs.channels.logs.sus);
if (!isTextBasedChannel(logChannel)) {
this.container.logger.error('Sus: Could not fetch log channel.');
return;
} else if (!logChannel.isSendable()) {
this.container.logger.error(
'Sus: The bot does not have permission to send in the 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 || !isTextBasedChannel(channel)) {
await interaction.reply({
content: 'Error fetching guild or channel!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
const member = await getGuildMember(user.id, guild);
// Checks if managed to find GuildMember for the user
if (!isGuildMember(member)) {
await interaction.reply({
content: 'Error fetching user!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
// Check if the user had sus notes before trying to remove them
// Gets the sus notes from the database
const notes = await findNotes(user.id, true);
// Checks if there are no notes on the user
if (notes.length === 0) {
await interaction.reply({
content: `${user} had no notes!`,
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
// Creates the embed to display the sus note
const noteEmbed = new EmbedBuilder()
.setColor('#ff0000')
.setTitle(`Delete ${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 modGuildMember = await getGuildMember(mod, guild);
if (isGuildMember(modGuildMember)) {
mod = modGuildMember.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,
});
}
// Create buttons to delete or cancel the deletion
const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`delete${user.id}`)
.setLabel('Delete')
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId(`cancel${user.id}`)
.setLabel('Cancel')
.setStyle(ButtonStyle.Secondary),
);
// Sends the note to verify this note is to be deleted
const message = await interaction.reply({
embeds: [noteEmbed],
components: [buttons],
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
// Checks if the message is not an APIMessage
if (message.resource === null) {
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
time: 15000, // 15 seconds
});
// Button pressed
collector.on('collect', async (button: ButtonInteraction) => {
if (button.customId === `delete${user.id}`) {
// Remove sus note from database
await deactivateAllNotes(user.id);
await interaction.editReply({
content: `Removed all of ${member}'s sus notes successfully`,
embeds: [],
});
}
await this.deleteAllNotesLogger(user, mod);
});
// Remove the buttons after they have been clicked
collector.on('end', async () => {
await interaction.editReply({
components: [],
});
});
// Remove sus role from the user
await member.roles.remove(IDs.roles.restrictions.sus);
}
// Logs removal of 1 sus note
private async deleteAllNotesLogger(user: User, mod: User) {
// Log the sus note
const logChannel = await getTextBasedChannel(IDs.channels.logs.sus);
if (!isTextChannel(logChannel)) {
this.container.logger.error('Sus: Could not fetch log channel.');
return;
} else if (!logChannel.isSendable()) {
this.container.logger.error(
'Sus: Could not not send in the log channel.',
);
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}` });
await logChannel.send({ embeds: [embed] });
}
}