Merge pull request #173 from veganhacktivists/feat/warnings

feat(arabot): add warning commands
This commit is contained in:
Anthony Berg 2024-01-04 22:12:46 +00:00 committed by GitHub
commit d00fddd51a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 821 additions and 250 deletions

View File

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

View File

@ -256,7 +256,6 @@ model Warning {
mod User @relation("warnMod", fields: [modId], references: [id]) mod User @relation("warnMod", fields: [modId], references: [id])
modId String modId String
time DateTime @default(now()) time DateTime @default(now())
active Boolean @default(true)
note String note String
} }

View File

@ -29,7 +29,7 @@ export class RestrictLogsCommand extends Command {
super(context, { super(context, {
...options, ...options,
name: 'restrictlogs', name: 'restrictlogs',
description: 'Unrestricts a user', description: 'Shows restriction history for a user',
preconditions: ['ModOnly'], preconditions: ['ModOnly'],
}); });
} }
@ -75,10 +75,9 @@ export class RestrictLogsCommand extends Command {
userId = user.id; userId = user.id;
} }
let staffChannel = false; const staffChannel = checkStaff(channel);
if (channel.type === ChannelType.GuildText) { if (staffChannel) {
channel = channel as TextChannel; channel = channel as TextChannel;
staffChannel = checkStaff(channel);
if (userId === null) { if (userId === null) {
let topic: string[]; let topic: string[];

View File

@ -20,14 +20,13 @@
import { RegisterBehavior, Args } from '@sapphire/framework'; import { RegisterBehavior, Args } from '@sapphire/framework';
import { Subcommand } from '@sapphire/plugin-subcommands'; import { Subcommand } from '@sapphire/plugin-subcommands';
import { import {
ChannelType,
EmbedBuilder, EmbedBuilder,
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
ButtonInteraction, ButtonInteraction,
ButtonStyle, ButtonStyle,
} from 'discord.js'; } from 'discord.js';
import type { Message, GuildMember, TextChannel } from 'discord.js'; import type { Message, GuildMember } from 'discord.js';
import { isMessageInstance } from '@sapphire/discord.js-utilities'; import { isMessageInstance } from '@sapphire/discord.js-utilities';
import { addExistingUser } from '#utils/database/dbExistingUser'; import { addExistingUser } from '#utils/database/dbExistingUser';
import { import {
@ -39,11 +38,15 @@ import {
} from '#utils/database/sus'; } from '#utils/database/sus';
import { checkStaff } from '#utils/checker'; import { checkStaff } from '#utils/checker';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { createSusLogEmbed } from '#utils/embeds';
// TODO add a check when they join the server to give the user the sus role again // TODO add a check when they join the server to give the user the sus role again
export class SusCommand extends Subcommand { export class SusCommand extends Subcommand {
public constructor(context: Subcommand.LoaderContext, options: Subcommand.Options) { public constructor(
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, { super(context, {
...options, ...options,
name: 'sus', name: 'sus',
@ -201,14 +204,7 @@ export class SusCommand extends Subcommand {
return; return;
} }
let staffChannel = false; const staffChannel = checkStaff(interaction.channel);
let { channel } = interaction;
if (channel !== null) {
if (channel.type === ChannelType.GuildText) {
channel = channel as TextChannel;
staffChannel = checkStaff(channel);
}
}
// Gets the sus notes from the database // Gets the sus notes from the database
const notes = await findNotes(user.id, true); const notes = await findNotes(user.id, true);
@ -224,34 +220,7 @@ export class SusCommand extends Subcommand {
} }
// Creates the embed to display the sus note // Creates the embed to display the sus note
const noteEmbed = new EmbedBuilder() const noteEmbed = createSusLogEmbed(notes, user, guild);
.setColor('#0099ff')
.setTitle(`${notes.length} sus notes for ${user.username}`)
.setThumbnail(user.displayAvatarURL());
// Add up to 10 of the latest sus notes to the embed
for (
let i = notes.length > 10 ? notes.length - 10 : 0;
i < notes.length;
i += 1
) {
// Get mod name
let mod = notes[i].modId;
const modMember = guild.members.cache.get(mod);
if (modMember !== undefined) {
mod = modMember.displayName;
}
// Add sus note to embed
noteEmbed.addFields({
name: `Sus ID: ${
notes[i].id
} | Moderator: ${mod} | Date: <t:${Math.floor(
notes[i].time.getTime() / 1000,
)}>`,
value: notes[i].note,
});
}
// Sends the notes to the user // Sends the notes to the user
await interaction.reply({ await interaction.reply({
@ -554,11 +523,6 @@ export class SusCommand extends Subcommand {
await user.roles.add(IDs.roles.restrictions.sus); await user.roles.add(IDs.roles.restrictions.sus);
} }
// Checks if the user is xlevra to send a very kind message
if (mod.id === '259624904746467329') {
await message.reply('Fuck you for making me add this feature 🤬');
}
await message.react('✅'); await message.react('✅');
} }
} }

View File

@ -1,127 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command } from '@sapphire/framework';
import type { User, Message, Snowflake, Guild } from 'discord.js';
import { addExistingUser, updateUser } from '#utils/database/dbExistingUser';
import { addWarn } from '#utils/database/warnings';
/*
This command is not intended to be functional for now, this is purely to log
warnings onto a database, so if we were to switch purely to ARA Bot, it would
mean we would have a lot of the warns already in the database.
*/
export class WarnCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'warn',
description: 'Warns a user (only used for logging to a database for now)',
preconditions: [['CoordinatorOnly', 'ModOnly']],
});
}
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const reason = args.finished ? null : await args.rest('string');
const mod = message.member;
if (reason === null) {
await message.react('❌');
await message.reply('Warn reason was not provided!');
return;
}
if (mod === null) {
await message.react('❌');
await message.reply(
'Moderator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const warn = await this.warn(user.id, mod.id, reason, guild);
if (!warn.success) {
await message.react('❌');
}
// await message.react('✅');
}
private async warn(
userId: Snowflake,
modId: Snowflake,
reason: string,
guild: Guild,
) {
const info = {
message: '',
success: false,
};
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod!';
return info;
}
// Check if mod is in database
await updateUser(mod);
// Gets guildMember
let member = guild.members.cache.get(userId);
if (member === undefined) {
member = await guild.members.fetch(userId).catch(() => undefined);
}
if (member === undefined) {
info.message = 'User is not on this server';
return info;
}
await addExistingUser(member);
await addWarn(userId, modId, reason);
info.message = `Warned ${member}`;
info.success = true;
return info;
}
}

View File

@ -0,0 +1,183 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2024 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, TextChannel } from 'discord.js';
import type { Message, Guild, User } from 'discord.js';
import IDs from '#utils/ids';
import { deleteWarning, fetchWarning } from '#utils/database/warnings';
import { checkStaff } from '#utils/checker';
export class DeleteWarningCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'deletewarning',
aliases: ['delwarn', 'removewarning'],
description: 'Deletes a warning',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addIntegerOption((option) =>
option
.setName('id')
.setDescription('ID for the warning')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const warningId = interaction.options.getInteger('id', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
const staffChannel = checkStaff(interaction.channel);
await interaction.deferReply({ ephemeral: !staffChannel });
const info = await this.deleteWarning(warningId, mod, guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
// Non Application Command method for removing a warning
public async messageRun(message: Message, args: Args) {
// Get arguments
let warningId: number;
try {
warningId = await args.pick('integer');
} catch {
await message.react('❌');
await message.react('Correct warning ID not provided!');
return;
}
const mod = message.author;
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await this.deleteWarning(warningId, mod, guild);
await message.reply({ content: info.message, embeds: info.embeds });
if (!info.success) {
await message.react('❌');
}
}
private async deleteWarning(warningId: number, mod: User, guild: Guild) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
success: false,
};
const warning = await fetchWarning(warningId);
if (warning === null) {
info.message = `Warning ID \`${warningId}\` not found!`;
return info;
}
await deleteWarning(warningId);
info.success = true;
const userId = warning.userId;
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = `Deleted warning ID \`${warningId}\`, but the user could not be found!`;
return info;
}
}
// Log the warnings deletion
let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(IDs.channels.logs.sus)) as
| TextChannel
| undefined;
if (logChannel === undefined) {
this.container.logger.error(
'Delete Warning Error: Could not fetch log channel',
);
info.message =
`Deleted warning for ${user} (Warning ID: ${warningId} but ` +
'could not find the log channel.';
return info;
}
}
const message = new EmbedBuilder()
.setColor('#28A745')
.setAuthor({
name: `Removed warning for ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Warning ID', value: `${warningId}`, inline: true },
)
.setTimestamp()
.setFooter({ text: `ID: ${userId}` });
await logChannel.send({ embeds: [message] });
info.message = `Deleted warning for ${user}`;
return info;
}
}

View File

@ -0,0 +1,226 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023, 2024 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
Args,
Command,
container,
RegisterBehavior,
} from '@sapphire/framework';
import type { User, Message, Snowflake, Guild, TextChannel } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import { addWarn } from '#utils/database/warnings';
import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids';
export class WarnCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'warn',
description: 'Warns a user',
preconditions: [['CoordinatorOnly', 'ModOnly']],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to warn')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('Reason for the warning')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply();
const info = await this.warn(user.id, mod.id, reason, guild);
await interaction.editReply({
content: info.message,
});
}
// Non Application Command method for warning a user
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const reason = args.finished ? null : await args.rest('string');
const mod = message.member;
if (reason === null) {
await message.react('❌');
await message.reply('Warn reason was not provided!');
return;
}
if (mod === null) {
await message.react('❌');
await message.reply(
'Moderator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const warn = await this.warn(user.id, mod.id, reason, guild);
if (!warn.success) {
await message.react('❌');
return;
}
await message.react('✅');
}
private async warn(
userId: Snowflake,
modId: Snowflake,
reason: string,
guild: Guild,
) {
const info = {
message: '',
success: false,
};
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod!';
return info;
}
// Check if mod is in database
await updateUser(mod);
// Gets User for person being restricted
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = 'Error fetching user';
return info;
}
}
await addWarn(userId, modId, reason);
info.message = `Warned ${user}`;
info.success = true;
// DM the reason
const dmEmbed = new EmbedBuilder()
.setColor('#FF6700')
.setAuthor({
name: "You've been warned!",
iconURL: `${user.displayAvatarURL()}`,
})
.addFields({ name: 'Reason', value: reason })
.setTimestamp();
await user.send({ embeds: [dmEmbed] }).catch(() => {});
// Log the ban
let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(IDs.channels.logs.sus)) as
| TextChannel
| undefined;
if (logChannel === undefined) {
container.logger.error('Warn Error: Could not fetch log channel');
info.message = `Warned ${user} but could not find the log channel. This has been logged to the database.`;
return info;
}
}
const message = new EmbedBuilder()
.setColor('#FF6700')
.setAuthor({
name: `Warned ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Reason', value: reason },
)
.setTimestamp()
.setFooter({ text: `ID: ${userId}` });
await logChannel.send({ embeds: [message] });
return info;
}
}

View File

@ -0,0 +1,161 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2024 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { ChannelType, EmbedBuilder } from 'discord.js';
import type { Message, Guild, User } from 'discord.js';
import IDs from '#utils/ids';
import { fetchWarnings } from '#utils/database/warnings';
import { checkStaff } from '#utils/checker';
import { createWarningsEmbed } from '#utils/embeds';
export class WarningsCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'warnings',
aliases: ['warninglog', 'warnlog'],
description: 'Shows all the warnings for the user',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to check the warnings for')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
const staffChannel = checkStaff(interaction.channel);
await interaction.deferReply({ ephemeral: !staffChannel });
const info = await this.warnings(user, guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
// Non Application Command method for fetching warnings
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User | undefined;
try {
user = await args.pick('user');
} catch {
user = undefined;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
if (user === undefined) {
const { channel } = message;
if (channel.type !== ChannelType.GuildText) {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
let topic: string[];
if (channel.parentId === IDs.categories.modMail) {
// Checks if the channel topic has the user's snowflake
if (channel.topic !== null) {
topic = channel.topic.split(' ');
// eslint-disable-next-line prefer-destructuring
const userId = topic[2];
user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
}
}
}
}
if (user === undefined) {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const info = await this.warnings(user, guild);
await message.reply({ content: info.message, embeds: info.embeds });
}
private async warnings(user: User, guild: Guild) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
};
const warnings = await fetchWarnings(user.id);
if (warnings.length === 0) {
info.message = `${user} user has no warnings.`;
return info;
}
// Creates an embed to display the warnings
const embed = createWarningsEmbed(warnings, user, guild);
info.embeds.push(embed);
return info;
}
}

View File

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

View File

@ -17,8 +17,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { TextChannel } from 'discord.js'; import type { TextBasedChannel } from 'discord.js';
import IDs from '#utils/ids'; import IDs from '#utils/ids';
import { ChannelType } from 'discord.js';
/** /**
* Checks if the channel is in the staff category. * Checks if the channel is in the staff category.
@ -26,7 +27,15 @@ import IDs from '#utils/ids';
* @returns {boolean} true if is in staff channel * @returns {boolean} true if is in staff channel
*/ */
export function checkStaff(channel: TextChannel) { export function checkStaff(channel: TextBasedChannel | null) {
if (channel === null) {
return false;
}
if (channel.type !== ChannelType.GuildText) {
return false;
}
if (channel.parent === null) { if (channel.parent === null) {
return false; return false;
} }

View File

@ -1,5 +1,6 @@
import { container } from '@sapphire/framework'; import { container } from '@sapphire/framework';
import type { Snowflake } from 'discord.js'; import type { Snowflake } from 'discord.js';
import { Prisma } from '@prisma/client';
export async function restrict( export async function restrict(
userId: Snowflake, userId: Snowflake,
@ -71,6 +72,8 @@ export async function getRestrictions(userId: Snowflake) {
return restrictions; return restrictions;
} }
export type RestrictionLogs = Prisma.PromiseReturnType<typeof getRestrictions>;
export async function checkActive(userId: Snowflake) { export async function checkActive(userId: Snowflake) {
const restriction = await container.database.restrict.findFirst({ const restriction = await container.database.restrict.findFirst({
where: { where: {

View File

@ -1,4 +1,5 @@
import { container } from '@sapphire/framework'; import { container } from '@sapphire/framework';
import { Prisma } from '@prisma/client';
export async function addToDatabase( export async function addToDatabase(
userId: string, userId: string,
@ -39,6 +40,8 @@ export async function findNotes(userId: string, active: boolean) {
return note; return note;
} }
export type SusNotes = Prisma.PromiseReturnType<typeof findNotes>;
// Get one note from the id // Get one note from the id
export async function getNote(noteId: number) { export async function getNote(noteId: number) {
// Query to get the specific user's sus notes // Query to get the specific user's sus notes

View File

@ -1,5 +1,6 @@
import { container } from '@sapphire/framework'; import { container } from '@sapphire/framework';
import type { Snowflake } from 'discord.js'; import type { Snowflake } from 'discord.js';
import { Prisma } from '@prisma/client';
export async function addWarn( export async function addWarn(
userId: Snowflake, userId: Snowflake,
@ -9,8 +10,13 @@ export async function addWarn(
await container.database.warning.create({ await container.database.warning.create({
data: { data: {
user: { user: {
connect: { connectOrCreate: {
id: userId, where: {
id: userId,
},
create: {
id: userId,
},
}, },
}, },
mod: { mod: {
@ -22,3 +28,36 @@ export async function addWarn(
}, },
}); });
} }
export async function fetchWarning(warningId: number) {
const warning = await container.database.warning.findUnique({
where: {
id: warningId,
},
});
return warning;
}
export async function fetchWarnings(userId: Snowflake) {
const warnings = await container.database.warning.findMany({
where: {
userId,
},
orderBy: {
id: 'asc',
},
});
return warnings;
}
export type Warnings = Prisma.PromiseReturnType<typeof fetchWarnings>;
export async function deleteWarning(warningId: number) {
await container.database.warning.delete({
where: {
id: warningId,
},
});
}

View File

@ -122,6 +122,7 @@ const devIDs = {
logs: { logs: {
restricted: '999431681217937513', restricted: '999431681217937513',
economy: '999431681599623198', economy: '999431681599623198',
sus: '999431681599623199',
}, },
}, },
categories: { categories: {

143
src/utils/embeds.ts Normal file
View File

@ -0,0 +1,143 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2024 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { Guild, User } from 'discord.js';
import { EmbedBuilder } from 'discord.js';
import type { SusNotes } from '#utils/database/sus';
import { RestrictionLogs } from '#utils/database/restriction';
import { Warnings } from '#utils/database/warnings';
export function createSusLogEmbed(notes: SusNotes, user: User, guild: Guild) {
const embed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(`${notes.length} sus notes for ${user.username}`)
.setThumbnail(user.displayAvatarURL());
// Add up to 10 of the latest sus notes to the embed
for (
let i = notes.length > 10 ? notes.length - 10 : 0;
i < notes.length;
i += 1
) {
// Get mod name
let mod = notes[i].modId;
const modMember = guild.members.cache.get(mod);
if (modMember !== undefined) {
mod = modMember.displayName;
}
// Add sus note to embed
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;
}
export function createRestrictLogEmbed(
restrictions: RestrictionLogs,
user: User,
guild: Guild,
) {
const embed = new EmbedBuilder()
.setColor('#FF6700')
.setTitle(`${restrictions.length} restrictions for ${user.tag}`)
.setThumbnail(user.displayAvatarURL())
.setFooter({ text: `ID: ${user.id}` });
// Add up to 10 of the latest restrictions to the embed
for (
let i = restrictions.length > 10 ? restrictions.length - 10 : 0;
i < restrictions.length;
i += 1
) {
// Get mod names
let restMod = restrictions[i].modId;
const restModMember = guild.members.cache.get(restMod);
if (restModMember !== undefined) {
restMod = restModMember.displayName;
}
let endRestMod = restrictions[i].endModId;
if (endRestMod !== null) {
const endRestModMember = guild.members.cache.get(endRestMod);
if (endRestModMember !== undefined) {
endRestMod = endRestModMember.displayName;
}
}
let restTitle = `Restriction: ${i + 1} | Restricted by: ${restMod} | `;
if (endRestMod !== null) {
restTitle += `Unrestricted by: ${endRestMod} | `;
} else {
restTitle += 'Currently Restricted | ';
}
restTitle += `Date: <t:${Math.floor(
restrictions[i].startTime.getTime() / 1000,
)}>`;
embed.addFields({
name: restTitle,
value: restrictions[i].reason,
});
}
return embed;
}
export function createWarningsEmbed(
warnings: Warnings,
user: User,
guild: Guild,
) {
const embed = new EmbedBuilder()
.setColor('#FF6700')
.setTitle(`${warnings.length} warnings for ${user.tag}`)
.setThumbnail(user.displayAvatarURL())
.setFooter({ text: `ID: ${user.id}` });
// Add up to 10 of the latest warnings to the embed
for (
let i = warnings.length > 10 ? warnings.length - 10 : 0;
i < warnings.length;
i += 1
) {
// Get mod names
let mod = warnings[i].modId;
const modMember = guild.members.cache.get(mod);
if (modMember !== undefined) {
mod = modMember.displayName;
}
let warnTitle = `ID: ${warnings[i].id} | Moderator: ${mod} | `;
warnTitle += `Date: <t:${Math.floor(warnings[i].time.getTime() / 1000)}>`;
embed.addFields({
name: warnTitle,
value: warnings[i].note,
});
}
return embed;
}

View File

@ -124,6 +124,7 @@ let IDs = {
logs: { logs: {
restricted: '920993034462715925', restricted: '920993034462715925',
economy: '932050015034159174', economy: '932050015034159174',
sus: '872884989950324826',
}, },
}, },
categories: { categories: {