aboutsummaryrefslogtreecommitdiff
path: root/src/lib/common/HighlightManager.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/common/HighlightManager.ts')
-rw-r--r--src/lib/common/HighlightManager.ts485
1 files changed, 0 insertions, 485 deletions
diff --git a/src/lib/common/HighlightManager.ts b/src/lib/common/HighlightManager.ts
deleted file mode 100644
index 4f891b7..0000000
--- a/src/lib/common/HighlightManager.ts
+++ /dev/null
@@ -1,485 +0,0 @@
-import { addToArray, format, Highlight, removeFromArray, timestamp, type HighlightWord } from '#lib';
-import assert from 'assert/strict';
-import {
- ChannelType,
- Collection,
- GuildMember,
- type Channel,
- type Client,
- type Message,
- type Snowflake,
- type TextBasedChannel
-} from 'discord.js';
-import { colors, Time } from '../utils/BushConstants.js';
-import { sanitizeInputForDiscord } from './util/Format.js';
-
-const NOTIFY_COOLDOWN = 5 * Time.Minute;
-const OWNER_NOTIFY_COOLDOWN = 5 * Time.Minute;
-const LAST_MESSAGE_COOLDOWN = 5 * Time.Minute;
-
-type users = Set<Snowflake>;
-type channels = Set<Snowflake>;
-type word = HighlightWord;
-type guild = Snowflake;
-type user = Snowflake;
-type lastMessage = Date;
-type lastDM = Message;
-
-type lastDmInfo = [lastDM: lastDM, guild: guild, channel: Snowflake, highlights: HighlightWord[]];
-
-export class HighlightManager {
- /**
- * Cached guild highlights.
- */
- public readonly guildHighlights = new Collection<guild, Collection<word, users>>();
-
- //~ /**
- //~ * Cached global highlights.
- //~ */
- //~ public readonly globalHighlights = new Collection<word, users>();
-
- /**
- * A collection of cooldowns of when a user last sent a message in a particular guild.
- */
- public readonly userLastTalkedCooldown = new Collection<guild, Collection<user, lastMessage>>();
-
- /**
- * Users that users have blocked
- */
- public readonly userBlocks = new Collection<guild, Collection<user, users>>();
-
- /**
- * Channels that users have blocked
- */
- public readonly channelBlocks = new Collection<guild, Collection<user, channels>>();
-
- /**
- * A collection of cooldowns of when the bot last sent each user a highlight message.
- */
- public readonly lastedDMedUserCooldown = new Collection<user, lastDmInfo>();
-
- /**
- * @param client The client to use.
- */
- public constructor(public readonly client: Client) {}
-
- /**
- * Sync the cache with the database.
- */
- public async syncCache(): Promise<void> {
- const highlights = await Highlight.findAll();
-
- this.guildHighlights.clear();
-
- for (const highlight of highlights) {
- highlight.words.forEach((word) => {
- if (!this.guildHighlights.has(highlight.guild)) this.guildHighlights.set(highlight.guild, new Collection());
- const guildCache = this.guildHighlights.get(highlight.guild)!;
- if (!guildCache.get(word)) guildCache.set(word, new Set());
- guildCache.get(word)!.add(highlight.user);
- });
-
- if (!this.userBlocks.has(highlight.guild)) this.userBlocks.set(highlight.guild, new Collection());
- this.userBlocks.get(highlight.guild)!.set(highlight.user, new Set(highlight.blacklistedUsers));
-
- if (!this.channelBlocks.has(highlight.guild)) this.channelBlocks.set(highlight.guild, new Collection());
- this.channelBlocks.get(highlight.guild)!.set(highlight.user, new Set(highlight.blacklistedChannels));
- }
- }
-
- /**
- * Checks a message for highlights.
- * @param message The message to check.
- * @returns A collection users mapped to the highlight matched
- */
- public checkMessage(message: Message): Collection<Snowflake, HighlightWord> {
- // even if there are multiple matches, only the first one is returned
- const ret = new Collection<Snowflake, HighlightWord>();
- if (!message.content || !message.inGuild()) return ret;
- if (!this.guildHighlights.has(message.guildId)) return ret;
-
- const guildCache = this.guildHighlights.get(message.guildId)!;
-
- for (const [word, users] of guildCache.entries()) {
- if (!this.isMatch(message.content, word)) continue;
-
- for (const user of users) {
- if (ret.has(user)) continue;
-
- if (!message.channel.permissionsFor(user)?.has('ViewChannel')) continue;
-
- const blockedUsers = this.userBlocks.get(message.guildId)?.get(user) ?? new Set();
- if (blockedUsers.has(message.author.id)) {
- void this.client.console.verbose(
- 'Highlight',
- `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> blocked the user <<${
- message.author.tag
- }>>`
- );
- continue;
- }
- const blockedChannels = this.channelBlocks.get(message.guildId)?.get(user) ?? new Set();
- if (blockedChannels.has(message.channel.id)) {
- void this.client.console.verbose(
- 'Highlight',
- `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> blocked the channel <<${
- message.channel.name
- }>>`
- );
- continue;
- }
- if (message.mentions.has(user)) {
- void this.client.console.verbose(
- 'Highlight',
- `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> is already mentioned in the message.`
- );
- continue;
- }
- ret.set(user, word);
- }
- }
-
- return ret;
- }
-
- /**
- * 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 Collection<HighlightWord, boolean>();
-
- for (const highlight of highlights) {
- for (const word of highlight.words) {
- results.set(word, this.isMatch(phrase, word));
- }
- }
-
- return results;
- }
-
- /**
- * 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 (hl.word.includes(' ')) {
- return phrase.toLocaleLowerCase().includes(hl.word.toLocaleLowerCase());
- } else {
- const words = phrase.split(/\s*\b\s/);
- return words.some((w) => w.toLocaleLowerCase() === hl.word.toLocaleLowerCase());
- }
- }
- }
-
- /**
- * 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.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection());
- const guildCache = this.guildHighlights.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 = addToArray(highlight.words, hl);
-
- return Boolean(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.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection());
- const guildCache = this.guildHighlights.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 = removeFromArray(highlight.words, toRemove);
-
- return Boolean(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.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection());
- const guildCache = this.guildHighlights.get(guild)!;
-
- for (const [word, users] of guildCache.entries()) {
- if (users.has(user)) users.delete(user);
- if (users.size === 0) guildCache.delete(word);
- }
-
- const highlight = await Highlight.findOne({ where: { guild, user } });
-
- if (!highlight) return false;
-
- highlight.words = [];
-
- return Boolean(await highlight.save().catch(() => false));
- }
-
- /**
- * Adds a new user or channel block to a user in a particular guild.
- * @param guild The guild to add the block to.
- * @param user The user that is blocking the target.
- * @param target The target that is being blocked.
- * @returns The result of the operation.
- */
- public async addBlock(
- guild: Snowflake,
- user: Snowflake,
- target: GuildMember | TextBasedChannel
- ): Promise<HighlightBlockResult> {
- const cacheKey = `${target instanceof GuildMember ? 'user' : 'channel'}Blocks` as const;
- const databaseKey = `blacklisted${target instanceof GuildMember ? 'Users' : 'Channels'}` as const;
-
- const [highlight] = await Highlight.findOrCreate({ where: { guild, user } });
-
- if (highlight[databaseKey].includes(target.id)) return HighlightBlockResult.ALREADY_BLOCKED;
-
- const newBlocks = addToArray(highlight[databaseKey], target.id);
-
- highlight[databaseKey] = newBlocks;
- const res = await highlight.save().catch(() => false);
- if (!res) return HighlightBlockResult.ERROR;
-
- if (!this[cacheKey].has(guild)) this[cacheKey].set(guild, new Collection());
- const guildBlocks = this[cacheKey].get(guild)!;
- guildBlocks.set(user, new Set(newBlocks));
-
- return HighlightBlockResult.SUCCESS;
- }
-
- /**
- * Removes a user or channel block from a user in a particular guild.
- * @param guild The guild to remove the block from.
- * @param user The user that is unblocking the target.
- * @param target The target that is being unblocked.
- * @returns The result of the operation.
- */
- public async removeBlock(guild: Snowflake, user: Snowflake, target: GuildMember | Channel): Promise<HighlightUnblockResult> {
- const cacheKey = `${target instanceof GuildMember ? 'user' : 'channel'}Blocks` as const;
- const databaseKey = `blacklisted${target instanceof GuildMember ? 'Users' : 'Channels'}` as const;
-
- const [highlight] = await Highlight.findOrCreate({ where: { guild, user } });
-
- if (!highlight[databaseKey].includes(target.id)) return HighlightUnblockResult.NOT_BLOCKED;
-
- const newBlocks = removeFromArray(highlight[databaseKey], target.id);
-
- highlight[databaseKey] = newBlocks;
- const res = await highlight.save().catch(() => false);
- if (!res) return HighlightUnblockResult.ERROR;
-
- if (!this[cacheKey].has(guild)) this[cacheKey].set(guild, new Collection());
- const guildBlocks = this[cacheKey].get(guild)!;
- guildBlocks.set(user, new Set(newBlocks));
-
- return HighlightUnblockResult.SUCCESS;
- }
-
- /**
- * Sends a user a direct message to alert them of their highlight being triggered.
- * @param message The message that triggered the highlight.
- * @param user The user who's highlights was triggered.
- * @param hl The highlight that was matched.
- * @returns Whether or a dm was sent.
- */
- public async notify(message: Message, user: Snowflake, hl: HighlightWord): Promise<boolean> {
- assert(message.inGuild());
-
- this.client.console.debug(`Notifying ${user} of highlight ${hl.word} in ${message.guild.name}`);
-
- dmCooldown: {
- const lastDM = this.lastedDMedUserCooldown.get(user);
- if (!lastDM?.[0]) break dmCooldown;
-
- const cooldown = this.client.config.owners.includes(user) ? OWNER_NOTIFY_COOLDOWN : NOTIFY_COOLDOWN;
-
- if (new Date().getTime() - lastDM[0].createdAt.getTime() < cooldown) {
- void this.client.console.verbose('Highlight', `User <<${user}>> has been DMed recently.`);
-
- if (lastDM[0].embeds.length < 10) {
- this.client.console.debug(`Trying to add to notification queue for ${user}`);
- return this.addToNotification(lastDM, message, hl);
- }
-
- this.client.console.debug(`User has too many embeds (${lastDM[0].embeds.length}).`);
- return false;
- }
- }
-
- talkCooldown: {
- const lastTalked = this.userLastTalkedCooldown.get(message.guildId)?.get(user);
- if (!lastTalked) break talkCooldown;
-
- presence: {
- // incase the bot left the guild
- if (message.guild) {
- const member = message.guild.members.cache.get(user);
- if (!member) {
- this.client.console.debug(`No member found for ${user} in ${message.guild.name}`);
- break presence;
- }
-
- const presence = member.presence ?? (await member.fetch()).presence;
- if (!presence) {
- this.client.console.debug(`No presence found for ${user} in ${message.guild.name}`);
- break presence;
- }
-
- if (presence.status === 'offline') {
- void this.client.console.verbose('Highlight', `User <<${user}>> is offline.`);
- break talkCooldown;
- }
- }
- }
-
- const now = new Date().getTime();
- const talked = lastTalked.getTime();
-
- if (now - talked < LAST_MESSAGE_COOLDOWN) {
- void this.client.console.verbose('Highlight', `User <<${user}>> has talked too recently.`);
-
- setTimeout(() => {
- const newTalked = this.userLastTalkedCooldown.get(message.guildId)?.get(user)?.getTime();
- if (talked !== newTalked) return;
-
- void this.notify(message, user, hl);
- }, LAST_MESSAGE_COOLDOWN).unref();
-
- return false;
- }
- }
-
- return this.client.users
- .send(user, {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- content: `In ${format.input(message.guild.name)} ${message.channel}, your highlight "${hl.word}" was matched:`,
- embeds: [this.generateDmEmbed(message, hl)]
- })
- .then((dm) => {
- this.lastedDMedUserCooldown.set(user, [dm, message.guildId!, message.channelId, [hl]]);
- return true;
- })
- .catch(() => false);
- }
-
- private async addToNotification(
- [originalDm, guild, channel, originalHl]: lastDmInfo,
- message: Message,
- hl: HighlightWord
- ): Promise<boolean> {
- assert(originalDm.embeds.length < 10);
- assert(originalDm.embeds.length > 0);
- assert(originalDm.channel.type === ChannelType.DM);
- this.client.console.debug(
- `Adding to notification queue for ${originalDm.channel.recipient?.tag ?? originalDm.channel.recipientId}`
- );
-
- const sameGuild = guild === message.guildId;
- const sameChannel = channel === message.channel.id;
- const sameWord = originalHl.every((w) => w.word === hl.word);
-
- /* eslint-disable @typescript-eslint/no-base-to-string */
- return originalDm
- .edit({
- content: `In ${sameGuild ? format.input(message.guild?.name ?? '[Unknown]') : 'multiple servers'} ${
- sameChannel ? message.channel ?? '[Unknown]' : 'multiple channels'
- }, ${sameWord ? `your highlight "${hl.word}" was matched:` : 'multiple highlights were matched:'}`,
- embeds: [...originalDm.embeds.map((e) => e.toJSON()), this.generateDmEmbed(message, hl)]
- })
- .then(() => true)
- .catch(() => false);
- /* eslint-enable @typescript-eslint/no-base-to-string */
- }
-
- private generateDmEmbed(message: Message, hl: HighlightWord) {
- 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 {
- description: [
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- message.channel!.toString(),
- ...[...recentMessages, message].map(
- (m) => `${timestamp(m.createdAt, 't')} ${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: colors.default,
- footer: { text: `Triggered in ${sanitizeInputForDiscord(`${message.guild}`)}` },
- timestamp: message.createdAt.toISOString()
- };
- }
-
- /**
- * Updates the time that a user last talked in a particular guild.
- * @param message The message the user sent.
- */
- public updateLastTalked(message: Message): void {
- if (!message.inGuild()) return;
- const lastTalked = (
- this.userLastTalkedCooldown.has(message.guildId)
- ? this.userLastTalkedCooldown
- : this.userLastTalkedCooldown.set(message.guildId, new Collection())
- ).get(message.guildId)!;
-
- lastTalked.set(message.author.id, new Date());
- }
-}
-
-export enum HighlightBlockResult {
- ALREADY_BLOCKED,
- ERROR,
- SUCCESS
-}
-
-export enum HighlightUnblockResult {
- NOT_BLOCKED,
- ERROR,
- SUCCESS
-}