diff --git a/README.md b/README.md index 5ef4424..4c76346 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [ID Bot](https://github.com/chrisdenman/id-bot) 0.0.3 +# [ID Bot](https://github.com/chrisdenman/id-bot) 0.0.4 ![An stylised image of the project's logo formed of a lower-case i cursively joining a capitalised D](res/img/id-gum-logo.png) diff --git a/VERSIONS.md b/VERSIONS.md index fb2099b..81071a8 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -5,3 +5,5 @@ 0.0.2 - `Functionality added to require the identification of emoji and custom (Discord) emoji.` 0.0.3 - `Cache expiration management.` + +0.0.4 - `Cache expiration bug fixes and improved factory usage.` diff --git a/package-lock.json b/package-lock.json index 4ac1d91..e8cdc16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ceilingcat/id-bot", - "version": "0.0.3", + "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ceilingcat/id-bot", - "version": "0.0.3", + "version": "0.0.4", "license": "Unlicense", "dependencies": { "discord-api-types": "^0.37.84", @@ -4444,10 +4444,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "http://mini.local:8090/repository/npm/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "license": "MIT", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 9a4e12a..17ab6f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ceilingcat/id-bot", - "version": "0.0.3", + "version": "0.0.4", "description": "A discord bot to remind users to tag uploaded images with identifiers.", "scripts": { "lint": "eslint \"src/*/*.js\"", diff --git a/src/__tests__/cache.spec.js b/src/__tests__/cache.spec.js index c34ab9b..8144b81 100644 --- a/src/__tests__/cache.spec.js +++ b/src/__tests__/cache.spec.js @@ -51,7 +51,7 @@ describe("Tests for our Cache", () => { }); it("Newly added cache entries have a 'createdAt' value greater or equal than that sampled before population.", () => { - const then = factory.utcTimeStampNow; + const then = factory.getNowInMilliSeconds; const meta = setK0V0().getMeta(k0); @@ -64,7 +64,7 @@ describe("Tests for our Cache", () => { expect(metaBefore.lastAccessedAt).toEqual(metaBefore.createdAt); - const then = factory.utcTimeStampNow; + const then = factory.getNowInMilliSeconds; cache.get(k0); const metaAfter = cache.getMeta(k0); @@ -78,7 +78,7 @@ describe("Tests for our Cache", () => { expect(metaBefore.lastUpdatedAt).toBeUndefined(); - const then = factory.utcTimeStampNow; + const then = factory.getNowInMilliSeconds; cache.set(k0, newValue); const metaAfter = cache.getMeta(k0); diff --git a/src/js/cache-max-stale-manager.js b/src/js/cache-max-stale-manager.js index 7547c28..691f5a7 100644 --- a/src/js/cache-max-stale-manager.js +++ b/src/js/cache-max-stale-manager.js @@ -63,14 +63,12 @@ class CacheMaxStaleManager { } #expireStaleCacheEntries() { - const NOW = this.#factory.utcTimeStampNow; - + const NOW = this.#factory.getNowInMilliSeconds; [...this.#cache.metaData] - .filter(it => it.lastAccessedAt + this.#maxStaleLifetimeMilliSeconds > NOW) - .map(it => it.key) + .filter(it => NOW - it.lastAccessedAt > this.#maxStaleLifetimeMilliSeconds) .map(it => { - this.#cache.remove(it); - this.#logger.debug(`Expired key @ ${NOW}`); + this.#cache.remove(it.key); + this.#logger.debug(`Expired key ${it.key}`); }); } diff --git a/src/js/cache.js b/src/js/cache.js index 36641f8..4e07572 100644 --- a/src/js/cache.js +++ b/src/js/cache.js @@ -45,7 +45,7 @@ class Cache { const value = this.#keyToValue.get(key); this.#keyToMetaData.set(key, this.#factory.withCacheAccess(this.#keyToMetaData.get(key))); - this.#logger.debug(`<[${key}]`); + this.#logger.debug(`<[${key}=${value}]`); return value; } else { diff --git a/src/js/discord-interface.js b/src/js/discord-interface.js index 5f4196c..87a0aa1 100644 --- a/src/js/discord-interface.js +++ b/src/js/discord-interface.js @@ -98,17 +98,13 @@ class DiscordInterface { /** * @param {IdBotMessage} received * @param {string} content + * @return Promise */ - replyTo(received, content) { + async replyTo(received, content) { this._logger.debug(`sending reply to ${received.id} with "${content}"`); - received.reply(content); - } - - update = (message, content) => message - .edit(content) - .then(updatedMessage => this._logger.log(`Updated ${message.id} to "${updatedMessage.content}"`)) - .catch(e => this._logger.error(`could not delete reply with id=${message.id}`, e)); + return received.discordJsMessage.reply(content); + } /** * @param {Channel} channel @@ -116,7 +112,8 @@ class DiscordInterface { */ deleteMessage(channel, messageId) { this._logger.debug(`attempting to delete replies to message with id=${messageId}`); - channel + + return channel .messages .delete(messageId) .then(() => this._logger.debug(`deleted reply with id=${messageId}`)) diff --git a/src/js/factory.js b/src/js/factory.js index 5646108..b738aef 100644 --- a/src/js/factory.js +++ b/src/js/factory.js @@ -9,6 +9,8 @@ import {GatewayIntentBits} from "discord-api-types/v10"; import {DiscordInterface} from "./discord-interface.js"; import {Application} from "./application.js"; import {CacheMaxStaleManager} from "./cache-max-stale-manager.js"; +import {numberOfEmojiContained} from "./emoji.js"; +import {isImageMediaType} from "./media-type.js"; const CLIENT_OPTIONS = { intents: [ @@ -20,6 +22,16 @@ const CLIENT_OPTIONS = { class Factory { + /** + * @type RegExp + */ + #MESSAGE_ID_REGEXP = /(?<=(^|\s|\W)ID:\s*)(\w+)(?!\WID:)/svg; + + /** + * @type RegExp + */ + #CUSTOM_EMOJI_REGEX = /<(a)?:(?\w+):(?\d+)>/g; + static get #CLIENT_OPTIONS() { return CLIENT_OPTIONS; } @@ -33,7 +45,8 @@ class Factory { */ createCacheExpirator(cache, tickIntervalDurationMilliSeconds = 100, maxStaleLifetimeMilliSeconds = undefined) { return new CacheMaxStaleManager( - cache, this.createLogger("MaxStaleCacheManager"), + cache, + this.createLogger("MaxStaleCacheManager"), this, tickIntervalDurationMilliSeconds, maxStaleLifetimeMilliSeconds @@ -96,8 +109,8 @@ class Factory { * * @returns {number} */ - get utcTimeStampNow() { - return new Date().getUTCMilliseconds(); + get getNowInMilliSeconds() { + return Date.now(); } /** @@ -105,7 +118,28 @@ class Factory { * @param discordJsMessage * @returns {IdBotMessage} */ - createIdBotMessage = discordJsMessage => new IdBotMessage(this, discordJsMessage); + createIdBotMessage = discordJsMessage => { + const content = discordJsMessage.content; + + const contentCustomEmojiMatches = [...content.matchAll(this.#CUSTOM_EMOJI_REGEX)]; + + const contentStrippedOfCustomEmoji = content.replaceAll(this.#CUSTOM_EMOJI_REGEX, ""); + const numberOfEmoji = numberOfEmojiContained(contentStrippedOfCustomEmoji); + + const idMatches = [...content.matchAll(this.#MESSAGE_ID_REGEXP)]; + + const imageIdStats = this.createImageIdStats( + [...discordJsMessage.attachments.values()] + .map(v => v.contentType) + .filter(isImageMediaType) + .length, + numberOfEmoji, + contentCustomEmojiMatches.length, + idMatches.length + ); + + return new IdBotMessage(imageIdStats, discordJsMessage); + }; /** * @param {string} [prefix] @@ -119,15 +153,15 @@ class Factory { * @returns {CacheMeta} */ createCacheMeta = key => { - const now = this.utcTimeStampNow; - + const now = this.getNowInMilliSeconds; + return new CacheMeta(key, now, now); }; - + withCacheAccess = cacheMeta => new CacheMeta( cacheMeta.key, cacheMeta.createdAt, - this.utcTimeStampNow, + this.getNowInMilliSeconds, cacheMeta.lastUpdatedAt ); @@ -135,7 +169,7 @@ class Factory { cacheMeta.key, cacheMeta.createdAt, cacheMeta.lastAccessedAt, - this.utcTimeStampNow + this.getNowInMilliSeconds ); /** diff --git a/src/js/id-bot-message.js b/src/js/id-bot-message.js index e93bfbd..f669e9f 100644 --- a/src/js/id-bot-message.js +++ b/src/js/id-bot-message.js @@ -1,6 +1,3 @@ -import {isImageMediaType} from "./media-type.js"; -import {numberOfEmojiContained} from "./emoji.js"; - /** * @typedef {typeof import('./image-id-stats.js')['ImageIdStats']} ImageIdStats * @typedef {typeof import('discord.js')['Snowflake']} Snowflake @@ -17,21 +14,16 @@ class IdBotMessage { */ #imageIdStats; - /** - * @type RegExp - */ - #MESSAGE_ID_REGEXP = /(?<=(^|\s|\W)ID:\s*)(\w+)(?!\WID:)/svg; - - /** - * @type RegExp - */ - #CUSTOM_EMOJI_REGEX = /<(a)?:(?\w+):(?\d+)>/g; /** * @type Message */ #discordJsMessage; + get discordJsMessage() { + return this.#discordJsMessage; + } + get id() { return this.#discordJsMessage.id; } @@ -49,12 +41,6 @@ class IdBotMessage { return this.#discordJsMessage.content; } - /** - * @param {string} content - */ - reply(content) { - return this.#discordJsMessage.reply(content); - } /** * @param {Snowflake} authorId @@ -107,29 +93,12 @@ class IdBotMessage { } /** - * @param {Factory} factory + * @param {ImageIdStats} imageIdStats * @param {Message} discordJsMessage the discord.js sourced discordJsMessage */ - constructor(factory, discordJsMessage) { + constructor(imageIdStats, discordJsMessage) { this.#discordJsMessage = discordJsMessage; - - const content = discordJsMessage.content; - const contentCustomEmojiMatches = [...content.matchAll(this.#CUSTOM_EMOJI_REGEX)]; - - const contentStrippedOfCustomEmoji = content.replaceAll(this.#CUSTOM_EMOJI_REGEX, ""); - const numberOfEmoji = numberOfEmojiContained(contentStrippedOfCustomEmoji); - - const idMatches = [...content.matchAll(this.#MESSAGE_ID_REGEXP)]; - - this.#imageIdStats = factory.createImageIdStats( - [...discordJsMessage.attachments.values()] - .map(v => v.contentType) - .filter(isImageMediaType) - .length, - numberOfEmoji, - contentCustomEmojiMatches.length, - idMatches.length - ); + this.#imageIdStats = imageIdStats; } } diff --git a/src/js/id-bot.js b/src/js/id-bot.js index cf19aa5..7310140 100644 --- a/src/js/id-bot.js +++ b/src/js/id-bot.js @@ -42,7 +42,7 @@ class IdBot { * @param {Channel} channel * @param messageId */ - #deleteChannelMessage(channel, messageId) { + async #deleteChannelMessage(channel, messageId) { this.#logger.debug(`deleting channel message with id=${messageId}`); this.#discordInterface.deleteMessage(channel, messageId); } @@ -53,11 +53,15 @@ class IdBot { #deleteOurReplyTo(message) { const messageId = message.id; this.#logger.info(`deleting our reply to ${message.toIdString()}`); + const replyId = this.#cache.get(messageId); if (replyId) { - this.#deleteChannelMessage(message.channel, replyId); - this.#cache.remove(messageId); - this.#logger.debug(this.#cache); + this + .#deleteChannelMessage(message.channel, replyId) + .then(() => { + this.#cache.remove(messageId); + this.#logger.debug(this.#cache); + }); } else { this.#logger.warn(`${message.toIdString()} has no known replies`); } @@ -74,17 +78,16 @@ class IdBot { * 1. if it is human authored and incorrectly identified, we post a reminder reply message * 2. else, if it's a self-authored reply, we cache: m.referencedMessageId -> m.id * - * Can we store arbitrary data in a message? - * * @param {IdBotMessage} message * - * @returns {Promise} + * @returns {Promise} */ #onMessageCreate = async message => { this.#logger.debug(`new ${message}`); if (message.isAuthorHuman) { const imageIdStats = message.imageIdStats; + if (!imageIdStats.isCorrectlyIdentified) { const reminderMessage = this.#reminderMessage(imageIdStats); this.#logger.debug( @@ -116,9 +119,9 @@ class IdBot { const imageIdStats = updatedMessage.imageIdStats; if (!imageIdStats.isCorrectlyIdentified) { + this.#deleteOurReplyTo(updatedMessage); const replyMessageContent = this.#reminderMessage(imageIdStats); this.#logger.debug(`${updatedMessage.toIdString()} not correctly identified, replying with "${replyMessageContent}"`); - this.#deleteOurReplyTo(updatedMessage); this.#discordInterface.replyTo(updatedMessage, replyMessageContent); } else { this.#deleteOurReplyTo(updatedMessage); @@ -133,7 +136,7 @@ class IdBot { #onMessageDelete = async message => { this.#logger.debug(`deleted id=${message.toIdString()}`); if (message.isAuthorHuman) { - this.#logger.debug(`onMessageDelete: message author is human`); + this.#logger.debug(`deleted message's author is human`); this.#deleteOurReplyTo(message); } }; diff --git a/src/js/logger.js b/src/js/logger.js index c602e61..d9f0e05 100644 --- a/src/js/logger.js +++ b/src/js/logger.js @@ -16,7 +16,8 @@ class Logger { this.#prefix = prefix; } - #format = (message, level) => `${level ? (level + ":") : ":"}${this.#prefix ? (this.#prefix + ":") : ":"}${message}`; + #format = (message, level) => `${level ? (level + ":") : ":"} + ${this.#prefix ? (this.#prefix + ":") : ":"}${message}`; /** * @param {*} message @@ -47,7 +48,7 @@ class Logger { /** * @param {*} message */ - warn = message => this.#console.warn(this.#format(message, "WARN")); + warn = message => this.#console.warn(this.#format(message)); } export {Logger};