Skip to content

Commit

Permalink
feat: add clear command
Browse files Browse the repository at this point in the history
  • Loading branch information
virtual-designer committed Jul 19, 2023
1 parent 70bc6a0 commit b52070c
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 11 deletions.
118 changes: 118 additions & 0 deletions src/commands/moderation/ClearCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* This file is part of SudoBot.
*
* Copyright (C) 2021-2023 OSN Developers.
*
* SudoBot is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SudoBot 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
*/

import {
Channel,
ChatInputCommandInteraction,
GuildChannel,
Message,
PermissionsBitField,
TextChannel,
User
} from "discord.js";
import Command, {
AnyCommandContext,
ArgumentType,
CommandMessage,
CommandReturn,
ValidationRule
} from "../../core/Command";
import { isTextableChannel } from "../../utils/utils";
import { logError } from "../../utils/logger";

export default class ClearCommand extends Command {
public readonly name = "clear";
public readonly validationRules: ValidationRule[] = [
{
types: [ArgumentType.User, ArgumentType.Integer],
entityNotNull: true,
entityNotNullErrorMessage: "This user does not exist! If it's an ID, make sure it's correct!",
name: "countOrUser",
requiredErrorMessage: "You must specify the count of messages to delete or a user to delete messages from!",
typeErrorMessage: "Please either specify a message count or user at position 1!",
minValue: 0,
maxValue: 100,
minMaxErrorMessage: "The message count must be a number between 0 to 100"
},
{
types: [ArgumentType.Integer],
optional: true,
name: "count",
typeErrorMessage: "Please specify a valid message count at position 2!",
minValue: 0,
maxValue: 100,
minMaxErrorMessage: "The message count must be a number between 0 to 100"
},
{
types: [ArgumentType.Channel],
optional: true,
entityNotNull: true,
entityNotNullErrorMessage: "This channel does not exist! If it's an ID, make sure it's correct!",
name: "channel",
typeErrorMessage: "Please specify a valid text channel at position 3!",
}
];
public readonly permissions = [PermissionsBitField.Flags.ManageMessages];

async execute(message: CommandMessage, context: AnyCommandContext): Promise<CommandReturn> {
const count: number | undefined = !context.isLegacy ? context.options.getInteger('count') ?? undefined : (
typeof context.parsedNamedArgs.countOrUser === 'number' ? context.parsedNamedArgs.countOrUser : context.parsedNamedArgs.count
);

const user: User | undefined = !context.isLegacy ? context.options.getUser('user') ?? undefined : (
typeof context.parsedNamedArgs.countOrUser !== 'number' ? context.parsedNamedArgs.countOrUser : undefined
);

const channel: Channel | undefined = !context.isLegacy ? context.options.getChannel('channel') ?? undefined : context.parsedNamedArgs.channel ?? undefined;

if (channel && !isTextableChannel(channel)) {
return {
__reply: true,
content: `${this.emoji('error')} The given channel is not a text channel`
};
}

if ((!count && count !== 0) && !user) {
return {
__reply: true,
content: `${this.emoji('error')} Please specify a user or message count, otherwise the system cannot determine how many messages to delete!`
};
}

if (message instanceof Message) {
await message.delete().catch(logError);
}

await this.deferIfInteraction(message);

await this.client.infractionManager.bulkDeleteMessages({
user,
guild: message.guild!,
reason: undefined,
sendLog: true,
moderator: message.member!.user as User,
notifyUser: false,
messageChannel: message.channel! as TextChannel,
count
});

if (message instanceof ChatInputCommandInteraction)
await message.editReply(`${this.emoji('check')} Operation completed.`);
}
}
5 changes: 5 additions & 0 deletions src/core/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default class Client extends DiscordClient {

async loadCommands(directory = this.commandsDirectory) {
const files = await fs.readdir(directory);
const includeOnly = process.env.COMMANDS?.split(';');

for (const file of files) {
const filePath = path.join(directory, file);
Expand All @@ -101,6 +102,10 @@ export default class Client extends DiscordClient {
continue;
}

if (includeOnly && !includeOnly.includes(file.replace(/\.(ts|js)/gi, ""))) {
continue;
}

const { default: Command } = await import(filePath);
const command = new Command(this);
this.commands.set(command.name, command);
Expand Down
34 changes: 24 additions & 10 deletions src/services/InfractionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,25 +368,38 @@ export default class InfractionManager extends Service {
return { id, result };
}

async bulkDeleteMessages({ user, messagesToDelete, messageChannel, guild, moderator, reason, sendLog }: BulkDeleteMessagesOptions) {
async bulkDeleteMessages({ user, messagesToDelete, messageChannel, guild, moderator, reason, sendLog, count: messageCount }: BulkDeleteMessagesOptions) {
if (messageChannel && !(messagesToDelete && messagesToDelete.length === 0)) {
let messages: Collection<string, Message> | MessageResolvable[] | null = messagesToDelete ?? null;
let messages: MessageResolvable[] | null = messagesToDelete ?? null;

if (messages === null) {
log("The messagesToDelete was option not provided. Fetching messages manually.");

try {
messages = await messageChannel.messages.fetch({ limit: 100 });
messages = messages.filter((m) => m.author.id === user.id && Date.now() - m.createdAt.getTime() <= 1000 * 60 * 60 * 24 * 7 * 2);
const allMessages = await messageChannel.messages.fetch({ limit: 100 });
messages = [];

let i = 0;

for (const [id, m] of allMessages) {
if (messageCount && i >= messageCount) {
break;
}

if ((user ? m.author.id === user.id : true) && Date.now() - m.createdAt.getTime() <= 1000 * 60 * 60 * 24 * 7 * 2) {
messages.push(m);
i++;
}
}
} catch (e) {
logError(e);
messages = null;
}
}

const count = messages ? (messages instanceof Collection ? messages.size : messages.length) : 0;
const count = messages ? messages.length : 0;

const { id } = await this.client.prisma.infraction.create({
const { id } = user ? await this.client.prisma.infraction.create({
data: {
type: InfractionType.BULK_DELETE_MESSAGE,
guildId: guild.id,
Expand All @@ -397,15 +410,15 @@ export default class InfractionManager extends Service {
},
reason
}
});
}) : { id: 0 };

if (sendLog) {
this.client.logger
.logBulkDeleteMessages({
channel: messageChannel,
count,
guild,
id: `${id}`,
id: id === 0 ? undefined : `${id}`,
user,
moderator,
reason
Expand All @@ -417,7 +430,7 @@ export default class InfractionManager extends Service {
try {
await messageChannel.bulkDelete(messages);
const reply = await messageChannel.send(
`${getEmoji(this.client, "check")} Deleted ${count} messages from user **@${escapeMarkdown(user.username)}**`
`${getEmoji(this.client, "check")} Deleted ${count} messages${user ? ` from user **@${escapeMarkdown(user.username)}**` : ""}`
);

setTimeout(() => reply.delete().catch(logError), 5000);
Expand Down Expand Up @@ -800,9 +813,10 @@ export type CreateMemberMuteOptions = CommonOptions & {
};

export type BulkDeleteMessagesOptions = CommonOptions & {
user: User;
user?: User;
messagesToDelete?: MessageResolvable[];
messageChannel?: TextChannel;
count?: number;
};

export type ActionDoneName = "banned" | "muted" | "kicked" | "warned" | "unbanned" | "unmuted";
Expand Down
2 changes: 1 addition & 1 deletion src/services/LoggerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export default class LoggerService extends Service {
id,
count,
channel
}: CommonUserActionOptions & { user: User; reason?: string; count: number; channel: TextChannel }) {
}: Omit<CommonUserActionOptions, 'id'> & { user?: User; reason?: string; count: number; channel: TextChannel, id?: string }) {
this.sendLogEmbed(guild, {
user,
title: "Messages deleted in bulk",
Expand Down

1 comment on commit b52070c

@vercel
Copy link

@vercel vercel bot commented on b52070c Jul 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.