aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/commands/info/help.ts5
-rw-r--r--src/commands/utilities/highlight-!.ts150
-rw-r--r--src/commands/utilities/highlight-add.ts82
-rw-r--r--src/commands/utilities/highlight-block.ts69
-rw-r--r--src/commands/utilities/highlight-clear.ts39
-rw-r--r--src/commands/utilities/highlight-matches.ts0
-rw-r--r--src/commands/utilities/highlight-remove.ts57
-rw-r--r--src/commands/utilities/highlight-show.ts34
-rw-r--r--src/commands/utilities/highlight-unblock.ts69
-rw-r--r--src/lib/common/HighlightManager.ts71
-rw-r--r--src/lib/extensions/discord-akairo/BushClient.ts10
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts18
-rw-r--r--src/lib/index.ts1
-rw-r--r--src/lib/models/instance/Highlight.ts81
-rw-r--r--src/listeners/message/highlight.ts15
15 files changed, 699 insertions, 2 deletions
diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts
index e31153b..2383566 100644
--- a/src/commands/info/help.ts
+++ b/src/commands/info/help.ts
@@ -77,7 +77,10 @@ export default class HelpCommand extends BushCommand {
if (command.channel == 'guild' && !message.guild && !args.showHidden) return false;
if (command.ownerOnly && !isOwner) return false;
if (command.superUserOnly && !isSuperUser) return false;
- return !(command.restrictedGuilds?.includes(message.guild?.id ?? '') === false && !args.showHidden);
+ if (command.restrictedGuilds?.includes(message.guild?.id ?? '') === false && !args.showHidden) return false;
+ if (command.aliases.length === 0) return false;
+
+ return true;
});
const categoryNice = category.id
.replace(/(\b\w)/gi, (lc) => lc.toUpperCase())
diff --git a/src/commands/utilities/highlight-!.ts b/src/commands/utilities/highlight-!.ts
new file mode 100644
index 0000000..332af03
--- /dev/null
+++ b/src/commands/utilities/highlight-!.ts
@@ -0,0 +1,150 @@
+import { BushCommand, Highlight, HighlightWord, type BushSlashMessage } from '#lib';
+import { Flag, type ArgumentGeneratorReturn, type SlashOption } from 'discord-akairo';
+import { ApplicationCommandOptionType } from 'discord-api-types';
+import { ApplicationCommandSubCommandData, AutocompleteInteraction, CacheType } from 'discord.js';
+
+type Unpacked<T> = T extends (infer U)[] ? U : T;
+
+export const highlightCommandArgs: {
+ [Command in keyof typeof highlightSubcommands]: (Unpacked<Required<ApplicationCommandSubCommandData['options']>> & {
+ retry?: string;
+ })[];
+} = {
+ add: [
+ {
+ name: 'word',
+ description: 'What word do you want to highlight?',
+ retry: '{error} Enter a valid word.',
+ type: ApplicationCommandOptionType.String,
+ required: true
+ },
+ {
+ name: 'regex',
+ description: 'Should the word be matched using regular expression?',
+ type: ApplicationCommandOptionType.Boolean,
+ required: false
+ }
+ ],
+ remove: [
+ {
+ name: 'word',
+ description: 'Which word do you want to stop highlighting?',
+ retry: '{error} Enter a valid word.',
+ type: ApplicationCommandOptionType.String,
+ required: true,
+ autocomplete: true
+ }
+ ],
+ block: [
+ {
+ name: 'target',
+ description: 'What user/channel would you like to prevent from triggering your highlights?',
+ retry: '{error} Enter a valid user or channel.',
+ type: ApplicationCommandOptionType.Mentionable,
+ required: true
+ }
+ ],
+ unblock: [
+ {
+ name: 'target',
+ description: 'What user/channel would you like to allow triggering your highlights again?',
+ retry: '{error} Enter a valid user or channel.',
+ type: ApplicationCommandOptionType.Mentionable,
+ required: true
+ }
+ ],
+ show: [],
+ clear: [],
+ matches: [
+ {
+ name: 'phrase',
+ description: 'What phrase would you like to test your highlighted words against?',
+ retry: '{error} Enter a valid phrase to test.',
+ type: ApplicationCommandOptionType.String,
+ required: true
+ }
+ ]
+};
+
+export const highlightSubcommands = {
+ add: 'Add a word to highlight.',
+ remove: 'Stop highting a word.',
+ block: 'Block a user or channel from triggering your highlights.',
+ unblock: 'Re-allow a user or channel to triggering your highlights.',
+ show: 'List all your current highlighted words.',
+ clear: 'Remove all of your highlighted words.',
+ matches: 'Test a phrase to see if it matches your current highlighted words.'
+} as const;
+
+export default class HighlightCommand extends BushCommand {
+ public constructor() {
+ super('highlight', {
+ aliases: ['highlight', 'hl'],
+ category: 'utilities',
+ description: 'Command description.',
+ usage: ['template <requiredArg> [optionalArg]'],
+ examples: ['template 1 2'],
+ slashOptions: Object.entries(highlightSubcommands).map((args) => {
+ // typescript being annoying
+ const [subcommand, description] = args as [keyof typeof highlightSubcommands, typeof args[1]];
+
+ return {
+ name: subcommand,
+ description,
+ type: ApplicationCommandOptionType.Subcommand,
+ options: highlightCommandArgs[subcommand].map((arg) => ({
+ name: arg.name,
+ description: arg.description,
+ type: arg.type,
+ required: arg.required,
+ autocomplete: arg.autocomplete
+ }))
+ } as SlashOption;
+ }),
+ slash: true,
+ channel: 'guild',
+ clientPermissions: (m) => util.clientSendAndPermCheck(m),
+ userPermissions: [],
+ ownerOnly: true
+ });
+ }
+
+ public override *args(): ArgumentGeneratorReturn {
+ const subcommand: keyof typeof highlightSubcommands = yield {
+ id: 'subcommand',
+ type: Object.keys(highlightSubcommands),
+ prompt: {
+ start: 'What sub command would you like to use?',
+ retry: `{error} Valid subcommands are: ${Object.keys(highlightSubcommands)
+ .map((s) => `\`${s}\``)
+ .join()}.`
+ }
+ };
+
+ return Flag.continue(`highlight-${subcommand}`);
+ }
+
+ public override async execSlash(message: BushSlashMessage, args: { subcommand: keyof typeof highlightSubcommands }) {
+ // manual `Flag.continue`
+ const subcommand = this.handler.modules.get(`highlight-${args.subcommand}`)!;
+ return subcommand.exec(message, args);
+ }
+
+ public override async autocomplete(interaction: AutocompleteInteraction<CacheType>) {
+ if (!interaction.inCachedGuild())
+ return interaction.respond([{ name: 'You must be in a server to use this command.', value: 'error' }]);
+
+ switch (interaction.options.getSubcommand(true)) {
+ case 'word': {
+ const { words } = (await Highlight.findOne({
+ where: {
+ guild: interaction.guild.id,
+ user: interaction.user.id
+ }
+ })) ?? { words: [] as HighlightWord[] };
+ if (!words.length) return interaction.respond([]);
+ return interaction.respond(words.map((w) => ({ name: w.word, value: w.word })));
+ }
+ }
+ }
+}
diff --git a/src/commands/utilities/highlight-add.ts b/src/commands/utilities/highlight-add.ts
new file mode 100644
index 0000000..ec5443c
--- /dev/null
+++ b/src/commands/utilities/highlight-add.ts
@@ -0,0 +1,82 @@
+import { AllowedMentions, BushCommand, Highlight, type ArgType, type BushMessage, type BushSlashMessage } from '#lib';
+import assert from 'assert';
+import { ArgumentGeneratorReturn } from 'discord-akairo';
+import { highlightCommandArgs, highlightSubcommands } from './highlight-!';
+
+export default class HighlightAddCommand extends BushCommand {
+ public constructor() {
+ super('highlight-add', {
+ aliases: [],
+ category: 'utilities',
+ description: highlightSubcommands.add,
+ usage: [],
+ examples: [],
+ clientPermissions: [],
+ userPermissions: []
+ });
+ }
+
+ public override *args(): ArgumentGeneratorReturn {
+ const word: ArgType<'string'> = yield {
+ type: 'string',
+ match: 'rest',
+ prompt: {
+ start: highlightCommandArgs.add[0].description,
+ retry: highlightCommandArgs.add[0].retry,
+ optional: !highlightCommandArgs.add[0].required
+ }
+ };
+
+ const regex: boolean = yield {
+ match: 'flag',
+ flag: 'regex'
+ };
+
+ return { word, regex };
+ }
+
+ public override async exec(
+ message: BushMessage | BushSlashMessage,
+ args: { word: ArgType<'string'>; regex: ArgType<'boolean'> }
+ ) {
+ assert(message.inGuild());
+
+ if (!args.regex) {
+ if (args.word.length < 2)
+ return message.util.send(`${util.emojis.error} You can only highlight words that are longer than 2 characters.`);
+ if (args.word.length > 50)
+ return await message.util.reply(`${util.emojis.error} You can only highlight words that are shorter than 50 characters.`);
+ } else {
+ try {
+ new RegExp(args.word);
+ } catch (e) {
+ assert(e instanceof SyntaxError);
+ return message.util.send({
+ content: `${util.emojis.error} Invalid regex ${util.format.inlineCode(e.message)}.`,
+ allowedMentions: AllowedMentions.none()
+ });
+ }
+ }
+
+ const [highlight] = await Highlight.findOrCreate({
+ where: {
+ guild: message.guild.id,
+ user: message.author.id
+ }
+ });
+
+ if (highlight.words.some((w) => w.word === args.word))
+ return await message.util.reply({
+ content: `${util.emojis.error} You have already highlighted "${args.word}".`,
+ allowedMentions: AllowedMentions.none()
+ });
+
+ highlight.words = util.addToArray(highlight.words, { word: args.word, regex: args.regex });
+ await highlight.save();
+
+ return await message.util.reply({
+ content: `${util.emojis.success} Successfully added "${args.word}" to your highlight list.`,
+ allowedMentions: AllowedMentions.none()
+ });
+ }
+}
diff --git a/src/commands/utilities/highlight-block.ts b/src/commands/utilities/highlight-block.ts
new file mode 100644
index 0000000..5a18b8a
--- /dev/null
+++ b/src/commands/utilities/highlight-block.ts
@@ -0,0 +1,69 @@
+import { AllowedMentions, BushCommand, Highlight, type ArgType, type BushMessage, type BushSlashMessage } from '#lib';
+import assert from 'assert';
+import { Argument, ArgumentGeneratorReturn } from 'discord-akairo';
+import { Channel, GuildMember } from 'discord.js';
+import { highlightCommandArgs, highlightSubcommands } from './highlight-!';
+
+export default class HighlightBlockCommand extends BushCommand {
+ public constructor() {
+ super('highlight-block', {
+ aliases: [],
+ category: 'utilities',
+ description: highlightSubcommands.block,
+ usage: [],
+ examples: [],
+ clientPermissions: [],
+ userPermissions: []
+ });
+ }
+
+ public override *args(): ArgumentGeneratorReturn {
+ const target: ArgType<'member'> | ArgType<'channel'> = yield {
+ type: Argument.union('member', 'channel'),
+ match: 'rest',
+ prompt: {
+ start: highlightCommandArgs.block[0].description,
+ retry: highlightCommandArgs.block[0].retry,
+ optional: !highlightCommandArgs.block[0].required
+ }
+ };
+
+ return { target };
+ }
+
+ public override async exec(
+ message: BushMessage | BushSlashMessage,
+ args: { target: ArgType<'user'> | ArgType<'role'> | ArgType<'member'> }
+ ) {
+ assert(message.inGuild());
+
+ if (!(args.target instanceof GuildMember || args.target instanceof Channel))
+ return await message.util.reply(`${util.emojis.error} You can only block users or channels.`);
+
+ if (args.target instanceof Channel && !args.target.isTextBased())
+ return await message.util.reply(`${util.emojis.error} You can only block text-based channels.`);
+
+ const [highlight] = await Highlight.findOrCreate({
+ where: {
+ guild: message.guild.id,
+ user: message.author.id
+ }
+ });
+
+ const key = `blacklisted${args.target instanceof Channel ? 'Channels' : 'Users'}` as const;
+
+ if (highlight[key].includes(args.target.id))
+ return await message.util.reply({
+ content: `${util.emojis.error} You have already blocked ${args.target}.`,
+ allowedMentions: AllowedMentions.none()
+ });
+
+ highlight[key] = util.addToArray(highlight[key], args.target.id);
+ await highlight.save();
+
+ return await message.util.reply({
+ content: `${util.emojis.success} Successfully blocked ${args.target} from triggering your highlights.`,
+ allowedMentions: AllowedMentions.none()
+ });
+ }
+}
diff --git a/src/commands/utilities/highlight-clear.ts b/src/commands/utilities/highlight-clear.ts
new file mode 100644
index 0000000..aded467
--- /dev/null
+++ b/src/commands/utilities/highlight-clear.ts
@@ -0,0 +1,39 @@
+import { AllowedMentions, BushCommand, ConfirmationPrompt, Highlight, type BushMessage, type BushSlashMessage } from '#lib';
+import assert from 'assert';
+import { highlightSubcommands } from './highlight-!';
+
+export default class HighlightClearCommand extends BushCommand {
+ public constructor() {
+ super('highlight-clear', {
+ aliases: [],
+ category: 'utilities',
+ description: highlightSubcommands.clear,
+ usage: [],
+ examples: [],
+ clientPermissions: [],
+ userPermissions: []
+ });
+ }
+
+ public override async exec(message: BushMessage | BushSlashMessage) {
+ assert(message.inGuild());
+
+ const [highlight] = await Highlight.findOrCreate({
+ where: {
+ guild: message.guild.id,
+ user: message.author.id
+ }
+ });
+
+ const confirm = await ConfirmationPrompt.send(message, { content: `Are you sure you want to clear your highlight list?` });
+ if (!confirm) return await message.util.reply(`${util.emojis.warn} You decided not to clear your highlight list.`);
+
+ highlight.words = [];
+ await highlight.save();
+
+ return await message.util.reply({
+ content: `${util.emojis.success} Successfully cleared your highlight list.`,
+ allowedMentions: AllowedMentions.none()
+ });
+ }
+}
diff --git a/src/commands/utilities/highlight-matches.ts b/src/commands/utilities/highlight-matches.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/commands/utilities/highlight-matches.ts
diff --git a/src/commands/utilities/highlight-remove.ts b/src/commands/utilities/highlight-remove.ts
new file mode 100644
index 0000000..0432a16
--- /dev/null
+++ b/src/commands/utilities/highlight-remove.ts
@@ -0,0 +1,57 @@
+import { AllowedMentions, BushCommand, Highlight, type ArgType, type BushMessage, type BushSlashMessage } from '#lib';
+import assert from 'assert';
+import { ArgumentGeneratorReturn } from 'discord-akairo';
+import { highlightCommandArgs, highlightSubcommands } from './highlight-!';
+
+export default class HighlightRemoveCommand extends BushCommand {
+ public constructor() {
+ super('highlight-remove', {
+ aliases: [],
+ category: 'utilities',
+ description: highlightSubcommands.remove,
+ usage: [],
+ examples: [],
+ clientPermissions: [],
+ userPermissions: []
+ });
+ }
+
+ public override *args(): ArgumentGeneratorReturn {
+ const word: ArgType<'string'> = yield {
+ type: 'string',
+ match: 'rest',
+ prompt: {
+ start: highlightCommandArgs.remove[0].description,
+ retry: highlightCommandArgs.remove[0].retry,
+ optional: !highlightCommandArgs.remove[0].required
+ }
+ };
+
+ return { word };
+ }
+
+ public override async exec(message: BushMessage | BushSlashMessage, args: { word: ArgType<'string'> }) {
+ assert(message.inGuild());
+
+ const [highlight] = await Highlight.findOrCreate({
+ where: {
+ guild: message.guild.id,
+ user: message.author.id
+ }
+ });
+
+ if (!highlight.words.some((w) => w.word === args.word))
+ return await message.util.reply({
+ content: `${util.emojis.error} You have not highlighted "${args.word}".`,
+ allowedMentions: AllowedMentions.none()
+ });
+
+ highlight.words = util.removeFromArray(highlight.words, highlight.words.find((w) => w.word === args.word)!);
+ await highlight.save();
+
+ return await message.util.reply({
+ content: `${util.emojis.success} Successfully removed "${args.word}" from your highlight list.`,
+ allowedMentions: AllowedMentions.none()
+ });
+ }
+}
diff --git a/src/commands/utilities/highlight-show.ts b/src/commands/utilities/highlight-show.ts
new file mode 100644
index 0000000..ab7c0c5
--- /dev/null
+++ b/src/commands/utilities/highlight-show.ts
@@ -0,0 +1,34 @@
+import { AllowedMentions, BushCommand, Highlight, type BushMessage, type BushSlashMessage } from '#lib';
+import assert from 'assert';
+import { Embed } from 'discord.js';
+import { highlightSubcommands } from './highlight-!';
+
+export default class HighlightShowCommand extends BushCommand {
+ public constructor() {
+ super('highlight-show', {
+ aliases: [],
+ category: 'utilities',
+ description: highlightSubcommands.show,
+ usage: [],
+ examples: [],
+ clientPermissions: [],
+ userPermissions: []
+ });
+ }
+
+ public override async exec(message: BushMessage | BushSlashMessage) {
+ assert(message.inGuild());
+
+ const [highlight] = await Highlight.findOrCreate({
+ where: {
+ guild: message.guild.id,
+ user: message.author.id
+ }
+ });
+
+ return await message.util.reply({
+ embeds: [new Embed().setTitle('Highlight List').setDescription(highlight.words.join('\n')).setColor(util.colors.default)],
+ allowedMentions: AllowedMentions.none()
+ });
+ }
+}
diff --git a/src/commands/utilities/highlight-unblock.ts b/src/commands/utilities/highlight-unblock.ts
new file mode 100644
index 0000000..7e5c0fb
--- /dev/null
+++ b/src/commands/utilities/highlight-unblock.ts
@@ -0,0 +1,69 @@
+import { AllowedMentions, BushCommand, Highlight, type ArgType, type BushMessage, type BushSlashMessage } from '#lib';
+import assert from 'assert';
+import { Argument, ArgumentGeneratorReturn } from 'discord-akairo';
+import { Channel, GuildMember } from 'discord.js';
+import { highlightCommandArgs, highlightSubcommands } from './highlight-!';
+
+export default class HighlightUnblockCommand extends BushCommand {
+ public constructor() {
+ super('highlight-unblock', {
+ aliases: [],
+ category: 'utilities',
+ description: highlightSubcommands.unblock,
+ usage: [],
+ examples: [],
+ clientPermissions: [],
+ userPermissions: []
+ });
+ }
+
+ public override *args(): ArgumentGeneratorReturn {
+ const target: ArgType<'member'> | ArgType<'channel'> = yield {
+ type: Argument.union('member', 'channel'),
+ match: 'rest',
+ prompt: {
+ start: highlightCommandArgs.unblock[0].description,
+ retry: highlightCommandArgs.unblock[0].retry,
+ optional: !highlightCommandArgs.unblock[0].required
+ }
+ };
+
+ return { target };
+ }
+
+ public override async exec(
+ message: BushMessage | BushSlashMessage,
+ args: { target: ArgType<'user'> | ArgType<'role'> | ArgType<'member'> }
+ ) {
+ assert(message.inGuild());
+
+ if (!(args.target instanceof GuildMember || args.target instanceof Channel))
+ return await message.util.reply(`${util.emojis.error} You can only unblock users or channels.`);
+
+ if (args.target instanceof Channel && !args.target.isTextBased())
+ return await message.util.reply(`${util.emojis.error} You can only unblock text-based channels.`);
+
+ const [highlight] = await Highlight.findOrCreate({
+ where: {
+ guild: message.guild.id,
+ user: message.author.id
+ }
+ });
+
+ const key = `blacklisted${args.target instanceof Channel ? 'Channels' : 'Users'}` as const;
+
+ if (!highlight[key].includes(args.target.id))
+ return await message.util.reply({
+ content: `${util.emojis.error} ${args.target} is not blocked so cannot be unblock.`,
+ allowedMentions: AllowedMentions.none()
+ });
+
+ highlight[key] = util.removeFromArray(highlight[key], args.target.id);
+ await highlight.save();
+
+ return await message.util.reply({
+ content: `${util.emojis.success} Successfully blocked ${args.target} from triggering your highlights.`,
+ allowedMentions: AllowedMentions.none()
+ });
+ }
+}
diff --git a/src/lib/common/HighlightManager.ts b/src/lib/common/HighlightManager.ts
new file mode 100644
index 0000000..a74ce9e
--- /dev/null
+++ b/src/lib/common/HighlightManager.ts
@@ -0,0 +1,71 @@
+import { Highlight, type BushMessage, type HighlightWord } from '#lib';
+import type { Snowflake } from 'discord.js';
+
+export class HighlightManager {
+ public cachedHighlights: Map</* guild */ Snowflake, Map</* word */ HighlightWord, /* users */ Set<Snowflake>>> = new Map();
+ public userLastTalkedCooldown = new Map<Snowflake, Map<Snowflake, Date>>();
+ public lastedDMedUserCooldown = new Map</* user */ Snowflake, /* last dm */ Date>();
+
+ public async syncCache() {
+ const highlights = await Highlight.findAll();
+
+ this.cachedHighlights.clear();
+
+ for (const highlight of highlights) {
+ highlight.words.forEach((word) => {
+ if (!this.cachedHighlights.has(highlight.guild)) this.cachedHighlights.set(highlight.guild, new Map());
+ const guildCache = this.cachedHighlights.get(highlight.guild)!;
+ if (!guildCache.get(word)) guildCache.set(word, new Set());
+ guildCache.get(word)!.add(highlight.user);
+ });
+ }
+ }
+
+ public checkMessage(message: BushMessage): Map<Snowflake, string> {
+ // even if there are multiple matches, only the first one is returned
+ const ret = new Map<Snowflake, string>();
+ if (!message.content || !message.inGuild()) return ret;
+ if (!this.cachedHighlights.has(message.guildId)) return ret;
+
+ const guildCache = this.cachedHighlights.get(message.guildId)!;
+
+ for (const [word, users] of guildCache.entries()) {
+ if (this.isMatch(message.content, word)) {
+ for (const user of users) {
+ if (!ret.has(user)) ret.set(user, word.word);
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ public async checkPhraseForUser(guild: Snowflake, user: Snowflake, phrase: string): Promise<Map<string, boolean>> {
+ const highlights = await Highlight.findAll({ where: { guild, user } });
+
+ const results = new Map<string, boolean>();
+
+ for (const highlight of highlights) {
+ for (const word of highlight.words) {
+ if (this.isMatch(phrase, word)) {
+ results.set(word.word, true);
+ }
+ }
+ }
+
+ return results;
+ }
+
+ private isMatch(phrase: string, word: HighlightWord) {
+ if (word.regex) {
+ return new RegExp(word.word, 'gi').test(phrase);
+ } else {
+ if (word.word.includes(' ')) {
+ return phrase.includes(word.word);
+ } else {
+ const words = phrase.split(/\s*\b\s/);
+ return words.includes(word.word);
+ }
+ }
+ }
+}
diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts
index eb1fe88..3f1c944 100644
--- a/src/lib/extensions/discord-akairo/BushClient.ts
+++ b/src/lib/extensions/discord-akairo/BushClient.ts
@@ -10,7 +10,7 @@ import {
roleWithDuration,
snowflake
} from '#args';
-import type {
+import {
BushBaseGuildEmojiManager,
BushChannelManager,
BushClientEvents,
@@ -47,8 +47,10 @@ import type { Options as SequelizeOptions, Sequelize as SequelizeType } from 'se
import { fileURLToPath } from 'url';
import UpdateCacheTask from '../../../tasks/updateCache.js';
import UpdateStatsTask from '../../../tasks/updateStats.js';
+import { HighlightManager } from '../../common/HighlightManager';
import { ActivePunishment } from '../../models/instance/ActivePunishment.js';
import { Guild as GuildModel } from '../../models/instance/Guild.js';
+import { Highlight } from '../../models/instance/Highlight.js';
import { Level } from '../../models/instance/Level.js';
import { ModLog } from '../../models/instance/ModLog.js';
import { Reminder } from '../../models/instance/Reminder.js';
@@ -184,6 +186,11 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
public sentry!: typeof Sentry;
/**
+ * Manages most aspects of the highlight command
+ */
+ public highlightManager = new HighlightManager();
+
+ /**
* @param config The configuration for the bot.
*/
public constructor(config: Config) {
@@ -403,6 +410,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
Level.initModel(this.instanceDB);
StickyRole.initModel(this.instanceDB);
Reminder.initModel(this.instanceDB);
+ Highlight.initModel(this.instanceDB);
await this.instanceDB.sync({ alter: true }); // Sync all tables to fix everything if updated
await this.console.success('startup', `Successfully connected to <<instance database>>.`, false);
} catch (e) {
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts
index c3739d6..a3ddfed 100644
--- a/src/lib/extensions/discord-akairo/BushClientUtil.ts
+++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts
@@ -520,6 +520,24 @@ export class BushClientUtil extends ClientUtil {
}
/**
+ * Remove an item from an array. All duplicates will be removed.
+ * @param array The array to remove an element from.
+ * @param value The element to remove from the array.
+ */
+ public removeFromArray<T>(array: T[], value: T): T[] {
+ return this.addOrRemoveFromArray('remove', array, value);
+ }
+
+ /**
+ * Add an item from an array. All duplicates will be removed.
+ * @param array The array to add an element to.
+ * @param value The element to add to the array.
+ */
+ public addToArray<T>(array: T[], value: T): T[] {
+ return this.addOrRemoveFromArray('add', array, value);
+ }
+
+ /**
* Surrounds a string to the begging an end of each element in an array.
* @param array The array you want to surround.
* @param surroundChar1 The character placed in the beginning of the element.
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 0c73875..7a9ab5f 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -69,6 +69,7 @@ export * from './extensions/discord.js/other.js';
export * from './models/BaseModel.js';
export * from './models/instance/ActivePunishment.js';
export * from './models/instance/Guild.js';
+export * from './models/instance/Highlight.js';
export * from './models/instance/Level.js';
export * from './models/instance/ModLog.js';
export * from './models/instance/Reminder.js';
diff --git a/src/lib/models/instance/Highlight.ts b/src/lib/models/instance/Highlight.ts
new file mode 100644
index 0000000..5889fad
--- /dev/null
+++ b/src/lib/models/instance/Highlight.ts
@@ -0,0 +1,81 @@
+import { type Snowflake } from 'discord.js';
+import { nanoid } from 'nanoid';
+import { type Sequelize } from 'sequelize';
+import { BaseModel } from '../BaseModel.js';
+const { DataTypes } = (await import('sequelize')).default;
+
+export interface HighlightModel {
+ pk: string;
+ user: Snowflake;
+ guild: Snowflake;
+ words: HighlightWord[];
+ blacklistedChannels: Snowflake[];
+ blacklistedUsers: Snowflake[];
+}
+
+export interface HighLightCreationAttributes {
+ pk?: string;
+ user: Snowflake;
+ guild: Snowflake;
+ words?: HighlightWord[];
+ blacklistedChannels?: Snowflake[];
+ blacklistedUsers?: Snowflake[];
+}
+
+export interface HighlightWord {
+ word: string;
+ regex: boolean;
+}
+
+/**
+ * List of words that should cause the user to be notified for if found in the specified guild.
+ */
+export class Highlight extends BaseModel<HighlightModel, HighLightCreationAttributes> implements HighlightModel {
+ /**
+ * The primary key of the highlight.
+ */
+ public declare pk: string;
+
+ /**
+ * The user that the highlight is for.
+ */
+ public declare user: Snowflake;
+
+ /**
+ * The guild to look for highlights in.
+ */
+ public declare guild: Snowflake;
+
+ /**
+ * The words to look for.
+ */
+ public declare words: HighlightWord[];
+
+ /**
+ * Channels that the user choose to ignore highlights in.
+ */
+ public declare blacklistedChannels: Snowflake[];
+
+ /**
+ * Users that the user choose to ignore highlights from.
+ */
+ public declare blacklistedUsers: Snowflake[];
+
+ /**
+ * Initializes the model.
+ * @param sequelize The sequelize instance.
+ */
+ public static initModel(sequelize: Sequelize): void {
+ Highlight.init(
+ {
+ pk: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid },
+ user: { type: DataTypes.STRING, allowNull: false },
+ guild: { type: DataTypes.STRING, allowNull: false },
+ words: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ blacklistedChannels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
+ blacklistedUsers: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] }
+ },
+ { sequelize }
+ );
+ }
+}
diff --git a/src/listeners/message/highlight.ts b/src/listeners/message/highlight.ts
new file mode 100644
index 0000000..25c8364
--- /dev/null
+++ b/src/listeners/message/highlight.ts
@@ -0,0 +1,15 @@
+import { BushListener, type BushClientEvents } from '#lib';
+
+export default class HighlightListener extends BushListener {
+ public constructor() {
+ super('highlight', {
+ emitter: 'client',
+ event: 'messageCreate',
+ category: 'message'
+ });
+ }
+
+ public override async exec(...[message]: BushClientEvents['messageCreate']) {
+ if (!message.inGuild()) return;
+ }
+}