diff options
Diffstat (limited to 'src/lib/common/HighlightManager.ts')
-rw-r--r-- | src/lib/common/HighlightManager.ts | 192 |
1 files changed, 172 insertions, 20 deletions
diff --git a/src/lib/common/HighlightManager.ts b/src/lib/common/HighlightManager.ts index a74ce9e..6194255 100644 --- a/src/lib/common/HighlightManager.ts +++ b/src/lib/common/HighlightManager.ts @@ -1,19 +1,48 @@ import { Highlight, type BushMessage, type HighlightWord } from '#lib'; -import type { Snowflake } from 'discord.js'; +import assert from 'assert'; +import { Collection, 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>(); + /** + * Cached highlights: guildId -> word -> userId + */ + public readonly cachedHighlights = new Collection< + /* guild */ Snowflake, + Collection</* word */ HighlightWord, /* users */ Set<Snowflake>> + >(); - public async syncCache() { + /** + * A collection of cooldowns of when a user last sent a message in a particular guild. + */ + public readonly userLastTalkedCooldown = new Collection< + /* guild */ Snowflake, + Collection</* user */ Snowflake, /* last message */ Date> + >(); + + /** + * Users that users have blocked + */ + public readonly userBlocks = new Collection< + /* guild */ Snowflake, + Collection</* word */ Snowflake, /* users */ Set<Snowflake>> + >(); + + /** + * A collection of cooldowns of when the bot last sent each user a highlight message. + */ + public readonly lastedDMedUserCooldown = new Collection</* user */ Snowflake, /* last dm */ Date>(); + + /** + * Sync the cache with the database. + */ + public async syncCache(): Promise<void> { 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()); + if (!this.cachedHighlights.has(highlight.guild)) this.cachedHighlights.set(highlight.guild, new Collection()); const guildCache = this.cachedHighlights.get(highlight.guild)!; if (!guildCache.get(word)) guildCache.set(word, new Set()); guildCache.get(word)!.add(highlight.user); @@ -21,9 +50,14 @@ export class HighlightManager { } } - public checkMessage(message: BushMessage): Map<Snowflake, string> { + /** + * Checks a message for highlights. + * @param message The message to check. + * @returns A collection users mapped to the highlight matched + */ + public checkMessage(message: BushMessage): Collection<Snowflake, HighlightWord> { // even if there are multiple matches, only the first one is returned - const ret = new Map<Snowflake, string>(); + const ret = new Collection<Snowflake, HighlightWord>(); if (!message.content || !message.inGuild()) return ret; if (!this.cachedHighlights.has(message.guildId)) return ret; @@ -32,7 +66,7 @@ export class HighlightManager { 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); + if (!ret.has(user)) ret.set(user, word); } } } @@ -40,32 +74,150 @@ export class HighlightManager { return ret; } - public async checkPhraseForUser(guild: Snowflake, user: Snowflake, phrase: string): Promise<Map<string, boolean>> { + /** + * Checks a user provided phrase for their highlights. + * @param guild The guild to check in. + * @param user The user to get the highlights for. + * @param phrase The phrase for highlights in. + * @returns A collection of the user's highlights mapped to weather or not it was matched. + */ + public async checkPhrase(guild: Snowflake, user: Snowflake, phrase: string): Promise<Collection<HighlightWord, boolean>> { const highlights = await Highlight.findAll({ where: { guild, user } }); - const results = new Map<string, boolean>(); + const results = new Collection<HighlightWord, boolean>(); for (const highlight of highlights) { for (const word of highlight.words) { - if (this.isMatch(phrase, word)) { - results.set(word.word, true); - } + results.set(word, this.isMatch(phrase, word)); } } return results; } - private isMatch(phrase: string, word: HighlightWord) { - if (word.regex) { - return new RegExp(word.word, 'gi').test(phrase); + /** + * Checks a particular highlight for a match within a phrase. + * @param phrase The phrase to check for the word in. + * @param hl The highlight to check for. + * @returns Whether or not the highlight was matched. + */ + private isMatch(phrase: string, hl: HighlightWord): boolean { + if (hl.regex) { + return new RegExp(hl.word, 'gi').test(phrase); } else { - if (word.word.includes(' ')) { - return phrase.includes(word.word); + if (hl.word.includes(' ')) { + return phrase.toLocaleLowerCase().includes(hl.word.toLocaleLowerCase()); } else { const words = phrase.split(/\s*\b\s/); - return words.includes(word.word); + return words.includes(hl.word); } } } + + /** + * Adds a new highlight to a user in a particular guild. + * @param guild The guild to add the highlight to. + * @param user The user to add the highlight to. + * @param hl The highlight to add. + * @returns A string representing a user error or a boolean indicating the database success. + */ + public async addHighlight(guild: Snowflake, user: Snowflake, hl: HighlightWord): Promise<string | boolean> { + if (!this.cachedHighlights.has(guild)) this.cachedHighlights.set(guild, new Collection()); + const guildCache = this.cachedHighlights.get(guild)!; + + if (!guildCache.has(hl)) guildCache.set(hl, new Set()); + guildCache.get(hl)!.add(user); + + const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); + + if (highlight.words.some((w) => w.word === hl.word)) return `You have already highlighted "${hl.word}".`; + + highlight.words = util.addToArray(highlight.words, hl); + + return !!(await highlight.save().catch(() => false)); + } + + /** + * Removes a highlighted word for a user in a particular guild. + * @param guild The guild to remove the highlight from. + * @param user The user to remove the highlight from. + * @param hl The word to remove. + * @returns A string representing a user error or a boolean indicating the database success. + */ + public async removeHighlight(guild: Snowflake, user: Snowflake, hl: string): Promise<string | boolean> { + if (!this.cachedHighlights.has(guild)) this.cachedHighlights.set(guild, new Collection()); + const guildCache = this.cachedHighlights.get(guild)!; + + const wordCache = guildCache.find((_, key) => key.word === hl); + + if (!wordCache?.has(user)) return `You have not highlighted "${hl}".`; + + wordCache!.delete(user); + + const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); + + const toRemove = highlight.words.find((w) => w.word === hl); + if (!toRemove) return `Uhhhhh... This shouldn't happen.`; + + highlight.words = util.removeFromArray(highlight.words, toRemove); + + return !!(await highlight.save().catch(() => false)); + } + + /** + * Remove all highlight words for a user in a particular guild. + * @param guild The guild to remove the highlights from. + * @param user The user to remove the highlights from. + * @returns A boolean indicating the database success. + */ + public async removeAllHighlights(guild: Snowflake, user: Snowflake): Promise<boolean> { + if (!this.cachedHighlights.has(guild)) this.cachedHighlights.set(guild, new Collection()); + const guildCache = this.cachedHighlights.get(guild)!; + + for (const [word, users] of guildCache.entries()) { + if (users.has(user)) users.delete(user); + if (!users.size) guildCache.delete(word); + } + + const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); + + highlight.words = []; + + return !!(await highlight.save().catch(() => false)); + } + + public async notify(message: BushMessage, user: Snowflake, hl: HighlightWord): Promise<boolean> { + assert(message.inGuild()); + const recentMessages = message.channel.messages.cache + .filter((m) => m.createdTimestamp <= message.createdTimestamp && m.id !== message.id) + .filter((m) => m.cleanContent?.trim().length > 0) + .sort((a, b) => b.createdTimestamp - a.createdTimestamp) + .first(4) + .reverse(); + + return client.users + .send(user, { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + content: `In ${util.format.input(message.guild.name)} ${message.channel}, your highlight "${hl.word}" was matched:`, + embeds: [ + { + description: [...recentMessages, message] + .map( + (m) => + `${util.timestamp(m.createdAt, 't')} ${util.format.input(`${m.author.tag}:`)} ${m.cleanContent + .trim() + .substring(0, 512)}` + ) + .join('\n'), + author: { name: hl.regex ? `/${hl.word}/gi` : hl.word }, + fields: [{ name: 'Source message', value: `[Jump to message](${message.url})` }], + color: util.colors.default, + footer: { text: 'Triggered' }, + timestamp: message.createdAt.toISOString() + } + ] + }) + .then(() => true) + .catch(() => false); + } } |