diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/common/HighlightManager.ts | 71 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClient.ts | 10 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClientUtil.ts | 18 | ||||
-rw-r--r-- | src/lib/index.ts | 1 | ||||
-rw-r--r-- | src/lib/models/instance/Highlight.ts | 81 |
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 } + ); + } +} |