aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-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
5 files changed, 180 insertions, 1 deletions
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 }
+ );
+ }
+}