aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands/fun/minesweeper.ts2
-rw-r--r--src/commands/info/help.ts2
-rw-r--r--src/commands/moderation/role.ts1
-rw-r--r--src/lib/common/AutoMod.ts175
-rw-r--r--src/lib/common/ButtonPaginator.ts13
-rw-r--r--src/lib/extensions/discord.js/BushMessage.ts2
-rw-r--r--src/lib/models/Guild.ts4
-rw-r--r--src/tasks/cpuUsage.ts1
-rw-r--r--src/tasks/removeExpiredPunishements.ts1
-rw-r--r--src/tasks/updateCache.ts1
-rw-r--r--src/tasks/updateStats.ts1
-rw-r--r--src/tasks/updateSuperUsers.ts1
12 files changed, 184 insertions, 20 deletions
diff --git a/src/commands/fun/minesweeper.ts b/src/commands/fun/minesweeper.ts
index 7ef1de7..16352ce 100644
--- a/src/commands/fun/minesweeper.ts
+++ b/src/commands/fun/minesweeper.ts
@@ -1,5 +1,5 @@
import { BushCommand, type BushMessage, type BushSlashMessage } from '#lib';
-import Minesweeper from '@notenoughupdates/discord.js-minesweeper';
+import { Minesweeper } from '@notenoughupdates/discord.js-minesweeper';
export default class MinesweeperCommand extends BushCommand {
public constructor() {
diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts
index 8b6720b..455ad5f 100644
--- a/src/commands/info/help.ts
+++ b/src/commands/info/help.ts
@@ -139,7 +139,7 @@ export default class HelpCommand extends BushCommand {
})
);
}
- if (packageDotJSON)
+ if (packageDotJSON?.repository)
row.addComponents(
new MessageButton({
style: 'LINK',
diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts
index 7ca0a5d..275db38 100644
--- a/src/commands/moderation/role.ts
+++ b/src/commands/moderation/role.ts
@@ -105,6 +105,7 @@ export default class RoleCommand extends BushCommand {
message: BushMessage | BushSlashMessage,
args: { action: 'add' | 'remove'; member: BushGuildMember; role: BushRole; duration?: number | null; force?: boolean }
) {
+ if (!args.role) return await message.util.reply(`${util.emojis.error} You must specify a role.`);
if (args.duration === null) args.duration = 0;
if (
!message.member!.permissions.has('MANAGE_ROLES') &&
diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts
index 5fd5d2d..c52754a 100644
--- a/src/lib/common/AutoMod.ts
+++ b/src/lib/common/AutoMod.ts
@@ -4,18 +4,36 @@ import badLinksSecretArray from '../badlinks-secret.js'; // I cannot make this p
import badLinksArray from '../badlinks.js';
import badWords from '../badwords.js';
+/**
+ * Handles auto moderation functionality.
+ */
export class AutoMod {
+ /**
+ * The message to check for blacklisted phrases on
+ */
private message: BushMessage;
+ /**
+ * Whether or not a punishment has already been given to the user
+ */
+ private punished = false;
+
+ /**
+ * @param message The message to check and potentially perform automod actions to
+ */
public constructor(message: BushMessage) {
this.message = message;
if (message.author.id === client.user?.id) return;
void this.handle();
}
+ /**
+ * Handles the auto moderation
+ */
private async handle() {
if (this.message.channel.type === 'DM' || !this.message.guild) return;
- if (!(await this.message.guild.hasFeature('automod'))) return;
+ const hasFeature = this.message.guild.hasFeature;
+ if (!(await hasFeature('automod'))) return;
const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? {};
const badLinks: BadWords = {};
@@ -34,8 +52,8 @@ export class AutoMod {
const result = {
...this.checkWords(customAutomodPhrases),
- ...this.checkWords((await this.message.guild.hasFeature('excludeDefaultAutomod')) ? {} : badWords),
- ...this.checkWords((await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? {} : badLinks)
+ ...this.checkWords((await hasFeature('excludeDefaultAutomod')) ? {} : badWords),
+ ...this.checkWords((await hasFeature('excludeAutomodScamLinks')) ? {} : badLinks)
};
if (Object.keys(result).length === 0) return;
@@ -44,7 +62,7 @@ export class AutoMod {
.map(([key, value]) => ({ word: key, ...value }))
.sort((a, b) => b.severity - a.severity)[0];
- if (highestOffence.severity === undefined || highestOffence.severity === null)
+ if (highestOffence.severity === undefined || highestOffence.severity === null) {
void this.message.guild.sendLogChannel('error', {
embeds: [
{
@@ -54,12 +72,19 @@ export class AutoMod {
}
]
});
- else {
+ } else {
const color = this.punish(highestOffence);
void this.log(highestOffence, color, result);
}
+
+ if (!this.punished && (await hasFeature('delScamMentions'))) void this.checkScamMentions();
}
+ /**
+ * Checks if any of the words provided are in the message
+ * @param words The words to check for
+ * @returns The blacklisted words found in the message
+ */
private checkWords(words: BadWords): BadWords {
if (Object.keys(words).length === 0) return {};
@@ -79,17 +104,81 @@ export class AutoMod {
return matchedWords;
}
+ /**
+ * If the message contains '@everyone' or '@here' and it contains a common scam phrase, it will be deleted
+ * @returns
+ */
+ private async checkScamMentions() {
+ const includes = this.message.content.toLocaleLowerCase().includes;
+ if (!includes('@everyone' || !includes('@here'))) return;
+ // It would be bad if we deleted a message that actually pinged @everyone or @here
+ if (this.message.member?.permissionsIn(this.message.channelId).has('MENTION_EVERYONE') || this.message.mentions.everyone)
+ return;
+
+ if (
+ includes('steam') ||
+ includes('www.youtube.com') ||
+ includes('youtu.be') ||
+ includes('nitro') ||
+ includes('1 month') ||
+ includes('3 months') ||
+ includes('personalize your profile') ||
+ includes('even more') ||
+ includes('xbox and discord') ||
+ includes('left over') ||
+ includes('check this lol') ||
+ includes('airdrop')
+ ) {
+ const color = this.punish({ severity: Severity.TEMP_MUTE, reason: 'everyone mention and scam phrase' } as HighestOffence);
+ void this.message.guild!.sendLogChannel('automod', {
+ embeds: [
+ new MessageEmbed()
+ .setTitle(`[Severity ${Severity.TEMP_MUTE}] Mention Scam Deleted`)
+ .setDescription(
+ `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From**: <#${this.message.channel.id}> [Jump to context](${this.message.url})`
+ )
+ .addField('Message Content', `${await util.codeblock(this.message.content, 1024)}`)
+ .setColor(color)
+ .setTimestamp()
+ ],
+ components:
+ Severity.TEMP_MUTE >= 2
+ ? [
+ new MessageActionRow().addComponents(
+ new MessageButton()
+ .setStyle('DANGER')
+ .setLabel('Ban User')
+ .setCustomId(`automod;ban;${this.message.author.id};everyone mention and scam phrase`)
+ )
+ ]
+ : undefined
+ });
+ }
+ }
+
+ /**
+ * Format a string according to the word options
+ * @param string The string to format
+ * @param wordOptions The word options to format with
+ * @returns The formatted string
+ */
private format(string: string, wordOptions: BadWordDetails) {
const temp = wordOptions.ignoreCapitalization ? string.toLowerCase() : string;
return wordOptions.ignoreSpaces ? temp.replace(/ /g, '') : temp;
}
- private punish(highestOffence: BadWordDetails & { word: string }) {
+ /**
+ * Punishes the user based on the severity of the offence
+ * @param highestOffence The highest offence to punish the user for
+ * @returns The color of the embed that the log should, based on the severity of the offence
+ */
+ private punish(highestOffence: HighestOffence) {
let color;
switch (highestOffence.severity) {
case Severity.DELETE: {
color = util.colors.lightGray;
void this.message.delete().catch((e) => deleteError.bind(this, e));
+ this.punished = true;
break;
}
case Severity.WARN: {
@@ -99,6 +188,7 @@ export class AutoMod {
moderator: this.message.guild!.me!,
reason: `[AutoMod] ${highestOffence.reason}`
});
+ this.punished = true;
break;
}
case Severity.TEMP_MUTE: {
@@ -109,6 +199,7 @@ export class AutoMod {
reason: `[AutoMod] ${highestOffence.reason}`,
duration: 900_000 // 15 minutes
});
+ this.punished = true;
break;
}
case Severity.PERM_MUTE: {
@@ -119,6 +210,7 @@ export class AutoMod {
reason: `[AutoMod] ${highestOffence.reason}`,
duration: 0 // permanent
});
+ this.punished = true;
break;
}
default: {
@@ -142,7 +234,13 @@ export class AutoMod {
}
}
- private async log(highestOffence: BadWordDetails & { word: string }, color: `#${string}`, offences: BadWords) {
+ /**
+ * Log an automod infraction to the guild's specified automod log channel
+ * @param highestOffence The highest severity word found in the message
+ * @param color The color that the log embed should be (based on the severity)
+ * @param offences The other offences that were also matched in the message
+ */
+ private async log(highestOffence: HighestOffence, color: `#${string}`, offences: BadWords) {
void client.console.info(
'autoMod',
`Severity <<${highestOffence.severity}>> action performed on <<${this.message.author.tag}>> (<<${
@@ -150,7 +248,7 @@ export class AutoMod {
}>>) in <<#${(this.message.channel as TextChannel).name}>> in <<${this.message.guild!.name}>>`
);
- return await this.message.guild!.sendLogChannel('automod', {
+ await this.message.guild!.sendLogChannel('automod', {
embeds: [
new MessageEmbed()
.setTitle(`[Severity ${highestOffence.severity}] Automod Action Performed`)
@@ -179,6 +277,10 @@ export class AutoMod {
});
}
+ /**
+ * Handles the ban button in the automod log.
+ * @param interaction The button interaction.
+ */
public static async handleInteraction(interaction: BushButtonInteraction) {
if (!interaction.memberPermissions?.has('BAN_MEMBERS'))
return interaction.reply({
@@ -228,25 +330,74 @@ export class AutoMod {
}
}
+/**
+ * The severity of the blacklisted word
+ */
export const enum Severity {
- /** Delete message */
+ /**
+ * Delete message
+ */
DELETE,
- /** Delete message and warn user */
+
+ /**
+ * Delete message and warn user
+ */
WARN,
- /** Delete message and mute user for 15 minutes */
+
+ /**
+ * Delete message and mute user for 15 minutes
+ */
TEMP_MUTE,
- /** Delete message and mute user permanently */
+
+ /**
+ * Delete message and mute user permanently
+ */
PERM_MUTE
}
+/**
+ * Details about a blacklisted word
+ */
interface BadWordDetails {
+ /**
+ * The severity of the word
+ */
severity: Severity;
+
+ /**
+ * Whether or not to ignore spaces when checking for the word
+ */
ignoreSpaces: boolean;
+
+ /**
+ * Whether or not to ignore case when checking for the word
+ */
ignoreCapitalization: boolean;
+
+ /**
+ * The reason that this word is blacklisted (used for the punishment reason)
+ */
reason: string;
+
+ /**
+ * Whether or not the word is regex
+ */
regex: boolean;
}
+interface HighestOffence extends BadWordDetails {
+ /**
+ * The word that is blacklisted
+ */
+ word: string;
+}
+
+/**
+ * Blacklisted words mapped to their details
+ */
export interface BadWords {
+ /**
+ * The blacklisted word
+ */
[key: string]: BadWordDetails;
}
diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts
index b8ae249..983eb56 100644
--- a/src/lib/common/ButtonPaginator.ts
+++ b/src/lib/common/ButtonPaginator.ts
@@ -18,11 +18,11 @@ export class ButtonPaginator {
/**
* Sends multiple embeds with controls to switch between them
- * @param message - The message to respond to
- * @param embeds - The embeds to switch between
- * @param text - The text send with the embeds (optional)
- * @param deleteOnExit - Whether to delete the message when the exit button is clicked (defaults to true)
- * @param startOn - The page to start from (**not** the index)
+ * @param message The message to respond to
+ * @param embeds The embeds to switch between
+ * @param text The text send with the embeds (optional)
+ * @param deleteOnExit Whether to delete the message when the exit button is clicked (defaults to true)
+ * @param startOn The page to start from (**not** the index)
*/
public static async send(
message: BushMessage | BushSlashMessage,
@@ -37,6 +37,9 @@ export class ButtonPaginator {
return await new ButtonPaginator(message, embeds, text, deleteOnExit, startOn).send();
}
+ /**
+ * The number of pages in the paginator
+ */
protected get numPages(): number {
return this.embeds.length;
}
diff --git a/src/lib/extensions/discord.js/BushMessage.ts b/src/lib/extensions/discord.js/BushMessage.ts
index c722f3d..9f6d422 100644
--- a/src/lib/extensions/discord.js/BushMessage.ts
+++ b/src/lib/extensions/discord.js/BushMessage.ts
@@ -18,7 +18,7 @@ export type PartialBushMessage = Partialize<
export class BushMessage<Cached extends boolean = boolean> extends Message<Cached> {
public declare readonly client: BushClient;
public declare util: BushCommandUtil<BushMessage<true>>;
- public declare readonly guild: BushGuild | null;
+ public declare readonly guild: If<Cached, BushGuild>;
public declare readonly member: BushGuildMember | null;
public declare author: BushUser;
public declare readonly channel: If<Cached, BushGuildTextBasedChannel, BushTextBasedChannels>;
diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts
index 02f487b..50113bf 100644
--- a/src/lib/models/Guild.ts
+++ b/src/lib/models/Guild.ts
@@ -280,6 +280,10 @@ export const guildFeaturesObj = asGuildFeature({
name: 'Exclude Automod Scam Links',
description: 'Opt out of having automod delete scam links.'
},
+ delScamMentions: {
+ name: 'Delete Scam Mentions',
+ description: 'Deletes messages with @everyone and @here mentions that have common scam phrases.'
+ },
autoPublish: {
name: 'Auto Publish',
description: 'Publishes messages in configured announcement channels.'
diff --git a/src/tasks/cpuUsage.ts b/src/tasks/cpuUsage.ts
index e597b31..882d660 100644
--- a/src/tasks/cpuUsage.ts
+++ b/src/tasks/cpuUsage.ts
@@ -8,6 +8,7 @@ export default class CpuUsageTask extends BushTask {
runOnStart: true
});
}
+
public override async exec() {
const cpu = await osu.cpu.usage(client.stats.cpu === undefined ? 100 : 60_000);
client.stats.cpu = cpu;
diff --git a/src/tasks/removeExpiredPunishements.ts b/src/tasks/removeExpiredPunishements.ts
index 6662292..8197cc5 100644
--- a/src/tasks/removeExpiredPunishements.ts
+++ b/src/tasks/removeExpiredPunishements.ts
@@ -8,6 +8,7 @@ export default class RemoveExpiredPunishmentsTask extends BushTask {
runOnStart: true
});
}
+
public override async exec() {
const expiredEntries = await ActivePunishment.findAll({
where: {
diff --git a/src/tasks/updateCache.ts b/src/tasks/updateCache.ts
index 16683f0..8bf92d5 100644
--- a/src/tasks/updateCache.ts
+++ b/src/tasks/updateCache.ts
@@ -9,6 +9,7 @@ export default class UpdateCacheTask extends BushTask {
runOnStart: false // done in preinit task
});
}
+
public override async exec() {
await UpdateCacheTask.updateGlobalCache(client);
await UpdateCacheTask.#updateGuildCache(client);
diff --git a/src/tasks/updateStats.ts b/src/tasks/updateStats.ts
index d6cabaa..8813343 100644
--- a/src/tasks/updateStats.ts
+++ b/src/tasks/updateStats.ts
@@ -8,6 +8,7 @@ export default class UpdateStatsTask extends BushTask {
runOnStart: true
});
}
+
public override async exec() {
const row =
(await Stat.findByPk(client.config.environment)) ?? (await Stat.create({ environment: client.config.environment }));
diff --git a/src/tasks/updateSuperUsers.ts b/src/tasks/updateSuperUsers.ts
index ffbf550..ba3e90c 100644
--- a/src/tasks/updateSuperUsers.ts
+++ b/src/tasks/updateSuperUsers.ts
@@ -8,6 +8,7 @@ export default class UpdateSuperUsersTask extends BushTask {
runOnStart: true
});
}
+
public override async exec() {
const superUsers = client.guilds.cache
.get(client.config.supportGuild.id)