diff --git a/package-lock.json b/package-lock.json index 0df2e98..4d34415 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.8.2", "license": "ISC", "dependencies": { - "discord.js": "^14.16.3", + "discord.js": "^14.17.3", "dotenv": "^16.4.7", "ollama": "^0.5.11", "redis": "^4.7.0" @@ -123,32 +123,26 @@ } }, "node_modules/@discordjs/builders": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.9.0.tgz", - "integrity": "sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz", + "integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==", "license": "Apache-2.0", "dependencies": { - "@discordjs/formatters": "^0.5.0", + "@discordjs/formatters": "^0.6.0", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "0.37.97", + "discord-api-types": "^0.37.114", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" }, "engines": { - "node": ">=18" + "node": ">=16.11.0" }, "funding": { "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/builders/node_modules/discord-api-types": { - "version": "0.37.97", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz", - "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==", - "license": "MIT" - }, "node_modules/@discordjs/collection": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", @@ -159,30 +153,24 @@ } }, "node_modules/@discordjs/formatters": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.5.0.tgz", - "integrity": "sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", + "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", "license": "Apache-2.0", "dependencies": { - "discord-api-types": "0.37.97" + "discord-api-types": "^0.37.114" }, "engines": { - "node": ">=18" + "node": ">=16.11.0" }, "funding": { "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/formatters/node_modules/discord-api-types": { - "version": "0.37.97", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz", - "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==", - "license": "MIT" - }, "node_modules/@discordjs/rest": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.0.tgz", - "integrity": "sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz", + "integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==", "license": "Apache-2.0", "dependencies": { "@discordjs/collection": "^2.1.1", @@ -190,7 +178,7 @@ "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.97", + "discord-api-types": "^0.37.114", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.19.8" @@ -214,12 +202,6 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/rest/node_modules/discord-api-types": { - "version": "0.37.97", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz", - "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==", - "license": "MIT" - }, "node_modules/@discordjs/util": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", @@ -233,20 +215,20 @@ } }, "node_modules/@discordjs/ws": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.1.1.tgz", - "integrity": "sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz", + "integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==", "license": "Apache-2.0", "dependencies": { "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.3.0", + "@discordjs/rest": "^2.4.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "0.37.83", + "discord-api-types": "^0.37.114", "tslib": "^2.6.2", - "ws": "^8.16.0" + "ws": "^8.17.0" }, "engines": { "node": ">=16.11.0" @@ -267,12 +249,6 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/ws/node_modules/discord-api-types": { - "version": "0.37.83", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.83.tgz", - "integrity": "sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -1176,9 +1152,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -1553,25 +1529,25 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.100", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.100.tgz", - "integrity": "sha512-a8zvUI0GYYwDtScfRd/TtaNBDTXwP5DiDVX7K5OmE+DRT57gBqKnwtOC5Ol8z0mRW8KQfETIgiB8U0YZ9NXiCA==", + "version": "0.37.118", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.118.tgz", + "integrity": "sha512-MQkHHZcytmNQ3nQOBj6a0z38swsmHiROX7hdayfd0eWVrLxaQp/6tWBZ7FO2MCKKsc+W3QWnnfOJTbtyk8C4TQ==", "license": "MIT" }, "node_modules/discord.js": { - "version": "14.16.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.16.3.tgz", - "integrity": "sha512-EPCWE9OkA9DnFFNrO7Kl1WHHDYFXu3CNVFJg63bfU7hVtjZGyhShwZtSBImINQRWxWP2tgo2XI+QhdXx28r0aA==", + "version": "14.17.3", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.17.3.tgz", + "integrity": "sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q==", "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.9.0", + "@discordjs/builders": "^1.10.0", "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.5.0", - "@discordjs/rest": "^2.4.0", + "@discordjs/formatters": "^0.6.0", + "@discordjs/rest": "^2.4.2", "@discordjs/util": "^1.1.1", - "@discordjs/ws": "1.1.1", + "@discordjs/ws": "^1.2.0", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "0.37.100", + "discord-api-types": "^0.37.114", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "tslib": "^2.6.3", diff --git a/package.json b/package.json index 7ea5c1c..4180577 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "author": "Kevin Dang", "license": "ISC", "dependencies": { - "discord.js": "^14.16.3", + "discord.js": "^14.17.3", "dotenv": "^16.4.7", "ollama": "^0.5.11", "redis": "^4.7.0" @@ -45,4 +45,4 @@ "npm": ">=10.9.0", "node": ">=22.12.0" } -} +} \ No newline at end of file diff --git a/src/commands/capacity.ts b/src/commands/capacity.ts index 52a4c70..a0e219f 100644 --- a/src/commands/capacity.ts +++ b/src/commands/capacity.ts @@ -22,7 +22,9 @@ export const Capacity: SlashCommand = { if (!channel || !UserCommand.includes(channel.type)) return // set state of bot chat features - openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('context-capacity')?.value) + openConfig(`${interaction.user.username}-config.json`, interaction.commandName, + interaction.options.get('context-capacity')?.value + ) interaction.reply({ content: `Max message history is now set to \`${interaction.options.get('context-capacity')?.value}\``, diff --git a/src/commands/cleanUserChannelHistory.ts b/src/commands/cleanUserChannelHistory.ts index c25c602..59c1b62 100644 --- a/src/commands/cleanUserChannelHistory.ts +++ b/src/commands/cleanUserChannelHistory.ts @@ -14,19 +14,21 @@ export const ClearUserChannelHistory: SlashCommand = { if (!channel || !UserCommand.includes(channel.type)) return // clear channel info for user - const successfulWipe = await clearChannelInfo(interaction.channelId, - interaction.channel as TextChannel, - interaction.user.username) + const successfulWipe = await clearChannelInfo( + interaction.channelId, + interaction.channel as TextChannel, + interaction.user.username + ) // check result of clearing history if (successfulWipe) - interaction.reply({ - content: `History cleared in **this channel** cleared for **${interaction.user.username}**.`, + interaction.reply({ + content: `History cleared in **this channel** cleared for **${interaction.user.username}**.`, ephemeral: true }) else - interaction.reply({ - content: `History was not be found for **${interaction.user.username}** in **this channel**.\n\nPlease chat with **${client.user?.username}** to start a chat history.`, + interaction.reply({ + content: `History was not be found for **${interaction.user.username}** in **this channel**.\n\nPlease chat with **${client.user?.username}** to start a chat history.`, ephemeral: true }) } diff --git a/src/commands/deleteModel.ts b/src/commands/deleteModel.ts index de192b9..7315989 100644 --- a/src/commands/deleteModel.ts +++ b/src/commands/deleteModel.ts @@ -6,7 +6,7 @@ import { ModelResponse } from 'ollama' export const DeleteModel: SlashCommand = { name: 'delete-model', description: 'deletes a model from the local list of models. Administrator Only.', - + // set available user options to pass to the command options: [ { diff --git a/src/commands/disable.ts b/src/commands/disable.ts index 3f7cb49..499de71 100644 --- a/src/commands/disable.ts +++ b/src/commands/disable.ts @@ -31,10 +31,12 @@ export const Disable: SlashCommand = { } // set state of bot chat features - openConfig(`${interaction.guildId}-config.json`, interaction.commandName, interaction.options.get('enabled')?.value) + openConfig(`${interaction.guildId}-config.json`, interaction.commandName, + interaction.options.get('enabled')?.value + ) interaction.reply({ - content: `${client.user?.username} is now **${interaction.options.get('enabled')?.value ? "enabled" : "disabled" }**.`, + content: `${client.user?.username} is now **${interaction.options.get('enabled')?.value ? "enabled" : "disabled"}**.`, ephemeral: true }) } diff --git a/src/commands/messageStream.ts b/src/commands/messageStream.ts index c0611f5..b8fee6d 100644 --- a/src/commands/messageStream.ts +++ b/src/commands/messageStream.ts @@ -22,7 +22,9 @@ export const MessageStream: SlashCommand = { if (!channel || !UserCommand.includes(channel.type)) return // save value to json and write to it - openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('stream')?.value) + openConfig(`${interaction.user.username}-config.json`, interaction.commandName, + interaction.options.get('stream')?.value + ) interaction.reply({ content: `Message streaming is now set to: \`${interaction.options.get('stream')?.value}\``, diff --git a/src/commands/shutoff.ts b/src/commands/shutoff.ts index 0550452..de84aee 100644 --- a/src/commands/shutoff.ts +++ b/src/commands/shutoff.ts @@ -28,6 +28,7 @@ export const Shutoff: SlashCommand = { content: `${client.user?.tag} is shutting down.`, ephemeral: true }) + console.log(`[Command: shutoff] ${client.user?.tag} is shutting down.`) // clean up client instance and stop diff --git a/src/commands/switchModel.ts b/src/commands/switchModel.ts index 8748227..c81c1e9 100644 --- a/src/commands/switchModel.ts +++ b/src/commands/switchModel.ts @@ -31,20 +31,20 @@ export const SwitchModel: SlashCommand = { // Phase 1: Switch to the model let switchSuccess = false await ollama.list() - .then(response => { - for (const model in response.models) { - const currentModel: ModelResponse = response.models[model] - if (currentModel.name.startsWith(modelInput)) { - openConfig(`${interaction.user.username}-config.json`, interaction.commandName, modelInput) + .then(response => { + for (const model in response.models) { + const currentModel: ModelResponse = response.models[model] + if (currentModel.name.startsWith(modelInput)) { + openConfig(`${interaction.user.username}-config.json`, interaction.commandName, modelInput) - // successful switch - interaction.editReply({ - content: `Successfully switched to **${modelInput}** as the preferred model for ${interaction.user.username}.` - }) - switchSuccess = true + // successful switch + interaction.editReply({ + content: `Successfully switched to **${modelInput}** as the preferred model for ${interaction.user.username}.` + }) + switchSuccess = true + } } - } - }) + }) // todo: problem can be here if async messes up if (switchSuccess) { // set model now that it exists @@ -55,7 +55,7 @@ export const SwitchModel: SlashCommand = { // Phase 2: Notify user of failure to find model. interaction.editReply({ content: `Could not find **${modelInput}** in local model library.\n\nPlease contact an server admin for access to this model.` - }) + }) } catch (error) { // could not resolve user model switch interaction.editReply({ diff --git a/src/commands/threadCreate.ts b/src/commands/threadCreate.ts index 1c86818..4f917e2 100644 --- a/src/commands/threadCreate.ts +++ b/src/commands/threadCreate.ts @@ -21,9 +21,7 @@ export const ThreadCreate: SlashCommand = { thread.send(`Hello ${interaction.user} and others! \n\nIt's nice to meet you. Please talk to me by typing **@${client.user?.username}** with your message.`) // handle storing this chat channel - openChannelInfo(thread.id, - thread as ThreadChannel, - interaction.user.tag) + openChannelInfo(thread.id, thread as ThreadChannel, interaction.user.tag) // user only reply return interaction.reply({ diff --git a/src/commands/threadPrivateCreate.ts b/src/commands/threadPrivateCreate.ts index 360a1af..536a71a 100644 --- a/src/commands/threadPrivateCreate.ts +++ b/src/commands/threadPrivateCreate.ts @@ -22,10 +22,7 @@ export const PrivateThreadCreate: SlashCommand = { // handle storing this chat channel // store: thread.id, thread.name - openChannelInfo(thread.id, - thread as ThreadChannel, - interaction.user.tag - ) + openChannelInfo(thread.id, thread as ThreadChannel, interaction.user.tag) // user only reply return interaction.reply({ diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index a8ade72..6c6fa89 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -6,14 +6,15 @@ import commands from '../commands/index.js' * @param interaction the interaction received from the server */ export default event(Events.InteractionCreate, async ({ log, client }, interaction) => { - if (!interaction.isCommand() || !interaction.isChatInputCommand()) return - - log(`Interaction called \'${interaction.commandName}\' from ${interaction.user.tag}.`) + if (!interaction.isCommand() || !interaction.isChatInputCommand()) return - // ensure command exists, otherwise kill event - const command = commands.find(command => command.name === interaction.commandName) - if (!command) return + log(`Interaction called \'${interaction.commandName}\' from ${interaction.user.tag}.`) - // the command exists, execute it - command.run(client, interaction) -}) \ No newline at end of file + // ensure command exists, otherwise kill event + const command = commands.find(command => command.name === interaction.commandName) + if (!command) return + + // the command exists, execute it + command.run(client, interaction) + } +) \ No newline at end of file diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 9b57e06..3266754 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -1,6 +1,9 @@ import { TextChannel } from 'discord.js' -import { event, Events, normalMessage, UserMessage, clean, getTextFileAttachmentData } from '../utils/index.js' -import { getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, openConfig, UserConfig, getAttachmentData } from '../utils/index.js' +import { event, Events, normalMessage, UserMessage, clean } from '../utils/index.js' +import { + getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, + openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData +} from '../utils/index.js' /** * Max Message length for free users is 2000 characters (bot or not). @@ -13,129 +16,129 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client let cleanedMessage = clean(message.content, clientId) log(`Message \"${cleanedMessage}\" from ${message.author.tag} in channel/thread ${message.channelId}.`) - // Do not respond if bot talks in the chat - if (message.author.username === message.client.user.username) return + // Do not respond if bot talks in the chat + if (message.author.username === message.client.user.username) return - // Only respond if message mentions the bot - if (!message.mentions.has(clientId)) return + // Only respond if message mentions the bot + if (!message.mentions.has(clientId)) return - // default stream to false - let shouldStream = false + // default stream to false + let shouldStream = false - // Params for Preferences Fetching - const maxRetries = 3 - const delay = 1000 // in millisecons - - try { - // Retrieve Server/Guild Preferences - let attempt = 0 - while (attempt < maxRetries) { - try { - await new Promise((resolve, reject) => { - getServerConfig(`${message.guildId}-config.json`, (config) => { - // check if config.json exists - if (config === undefined) { - // Allowing chat options to be available - openConfig(`${message.guildId}-config.json`, 'toggle-chat', true) - reject(new Error('Failed to locate or create Server Preferences\n\nPlease try chatting again...')) - } - - // check if chat is disabled - else if (!config.options['toggle-chat']) - reject(new Error('Admin(s) have disabled chat features.\n\n Please contact your server\'s admin(s).')) - else - resolve(config) + // Params for Preferences Fetching + const maxRetries = 3 + const delay = 1000 // in millisecons + + try { + // Retrieve Server/Guild Preferences + let attempt = 0 + while (attempt < maxRetries) { + try { + await new Promise((resolve, reject) => { + getServerConfig(`${message.guildId}-config.json`, (config) => { + // check if config.json exists + if (config === undefined) { + // Allowing chat options to be available + openConfig(`${message.guildId}-config.json`, 'toggle-chat', true) + reject(new Error('Failed to locate or create Server Preferences\n\nPlease try chatting again...')) + } + + // check if chat is disabled + else if (!config.options['toggle-chat']) + reject(new Error('Admin(s) have disabled chat features.\n\n Please contact your server\'s admin(s).')) + else + resolve(config) + }) }) - }) - break // successful - } catch (error) { - ++attempt - if (attempt < maxRetries) { - log(`Attempt ${attempt} failed for Server Preferences. Retrying in ${delay}ms...`) - await new Promise(ret => setTimeout(ret, delay)) - } else - throw new Error(`Could not retrieve Server Preferences, please try chatting again...`) - } - } - - // Reset attempts for User preferences - attempt = 0 - let userConfig: UserConfig | undefined - - while (attempt < maxRetries) { - try { - // Retrieve User Preferences - userConfig = await new Promise((resolve, reject) => { - getUserConfig(`${message.author.username}-config.json`, (config) => { - if (config === undefined) { - openConfig(`${message.author.username}-config.json`, 'message-style', false) - openConfig(`${message.author.username}-config.json`, 'switch-model', defaultModel) - reject(new Error('No User Preferences is set up.\n\nCreating preferences file with \`message-style\` set as \`false\` for regular message style.\nPlease try chatting again.')) - return - } - - // check if there is a set capacity in config - else if (typeof config.options['modify-capacity'] !== 'number') - log(`Capacity is undefined, using default capacity of ${msgHist.capacity}.`) - else if (config.options['modify-capacity'] === msgHist.capacity) - log(`Capacity matches config as ${msgHist.capacity}, no changes made.`) - else { - log(`New Capacity found. Setting Context Capacity to ${config.options['modify-capacity']}.`) - msgHist.capacity = config.options['modify-capacity'] - } - - // set stream state - shouldStream = config.options['message-stream'] as boolean || false - - if (typeof config.options['switch-model'] !== 'string') - reject(new Error(`No Model was set. Please set a model by running \`/switch-model \`.\n\nIf you do not have any models. Run \`/pull-model \`.`)) - - resolve(config) - }) - }) - break // successful - } catch (error) { - ++attempt - if (attempt < maxRetries) { - log(`Attempt ${attempt} failed for User Preferences. Retrying in ${delay}ms...`) - await new Promise(ret => setTimeout(ret, delay)) - } else - throw new Error(`Could not retrieve User Preferences, please try chatting again...`) - } - } - - // need new check for "open/active" threads/channels here! - let chatMessages: UserMessage[] = await new Promise((resolve) => { - // set new queue to modify - getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => { - if (channelInfo?.messages) - resolve(channelInfo.messages) - else { - log(`Channel/Thread ${message.channel}-${message.author.username} does not exist. File will be created shortly...`) - resolve([]) + break // successful + } catch (error) { + ++attempt + if (attempt < maxRetries) { + log(`Attempt ${attempt} failed for Server Preferences. Retrying in ${delay}ms...`) + await new Promise(ret => setTimeout(ret, delay)) + } else + throw new Error(`Could not retrieve Server Preferences, please try chatting again...`) } - }) - }) + } - if (chatMessages.length === 0) { - chatMessages = await new Promise((resolve, reject) => { - openChannelInfo(message.channelId, - message.channel as TextChannel, - message.author.tag - ) + // Reset attempts for User preferences + attempt = 0 + let userConfig: UserConfig | undefined + + while (attempt < maxRetries) { + try { + // Retrieve User Preferences + userConfig = await new Promise((resolve, reject) => { + getUserConfig(`${message.author.username}-config.json`, (config) => { + if (config === undefined) { + openConfig(`${message.author.username}-config.json`, 'message-style', false) + openConfig(`${message.author.username}-config.json`, 'switch-model', defaultModel) + reject(new Error('No User Preferences is set up.\n\nCreating preferences file with \`message-style\` set as \`false\` for regular message style.\nPlease try chatting again.')) + return + } + + // check if there is a set capacity in config + else if (typeof config.options['modify-capacity'] !== 'number') + log(`Capacity is undefined, using default capacity of ${msgHist.capacity}.`) + else if (config.options['modify-capacity'] === msgHist.capacity) + log(`Capacity matches config as ${msgHist.capacity}, no changes made.`) + else { + log(`New Capacity found. Setting Context Capacity to ${config.options['modify-capacity']}.`) + msgHist.capacity = config.options['modify-capacity'] + } + + // set stream state + shouldStream = config.options['message-stream'] as boolean || false + + if (typeof config.options['switch-model'] !== 'string') + reject(new Error(`No Model was set. Please set a model by running \`/switch-model \`.\n\nIf you do not have any models. Run \`/pull-model \`.`)) + + resolve(config) + }) + }) + break // successful + } catch (error) { + ++attempt + if (attempt < maxRetries) { + log(`Attempt ${attempt} failed for User Preferences. Retrying in ${delay}ms...`) + await new Promise(ret => setTimeout(ret, delay)) + } else + throw new Error(`Could not retrieve User Preferences, please try chatting again...`) + } + } + + // need new check for "open/active" threads/channels here! + let chatMessages: UserMessage[] = await new Promise((resolve) => { + // set new queue to modify getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => { if (channelInfo?.messages) resolve(channelInfo.messages) else { log(`Channel/Thread ${message.channel}-${message.author.username} does not exist. File will be created shortly...`) - reject(new Error(`Failed to find ${message.author.username}'s history. Try chatting again.`)) + resolve([]) } }) }) - } - if (!userConfig) - throw new Error(`Failed to initialize User Preference for **${message.author.username}**.\n\nIt's likely you do not have a model set. Please use the \`switch-model\` command to do that.`) + if (chatMessages.length === 0) { + chatMessages = await new Promise((resolve, reject) => { + openChannelInfo(message.channelId, + message.channel as TextChannel, + message.author.tag + ) + getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => { + if (channelInfo?.messages) + resolve(channelInfo.messages) + else { + log(`Channel/Thread ${message.channel}-${message.author.username} does not exist. File will be created shortly...`) + reject(new Error(`Failed to find ${message.author.username}'s history. Try chatting again.`)) + } + }) + }) + } + + if (!userConfig) + throw new Error(`Failed to initialize User Preference for **${message.author.username}**.\n\nIt's likely you do not have a model set. Please use the \`switch-model\` command to do that.`) // get message attachment if exists const attachment = message.attachments.first() @@ -148,43 +151,44 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client const model: string = userConfig.options['switch-model'] - // set up new queue - msgHist.setQueue(chatMessages) + // set up new queue + msgHist.setQueue(chatMessages) - // check if we can push, if not, remove oldest - while (msgHist.size() >= msgHist.capacity) msgHist.dequeue() + // check if we can push, if not, remove oldest + while (msgHist.size() >= msgHist.capacity) msgHist.dequeue() - // push user response before ollama query - msgHist.enqueue({ - role: 'user', - content: cleanedMessage, - images: messageAttachment || [] - }) - - // response string for ollama to put its response - const response: string = await normalMessage(message, ollama, model, msgHist, shouldStream) + // push user response before ollama query + msgHist.enqueue({ + role: 'user', + content: cleanedMessage, + images: messageAttachment || [] + }) - // If something bad happened, remove user query and stop - if (response == undefined) { msgHist.pop(); return } + // response string for ollama to put its response + const response: string = await normalMessage(message, ollama, model, msgHist, shouldStream) - // if queue is full, remove the oldest message - while (msgHist.size() >= msgHist.capacity) msgHist.dequeue() + // If something bad happened, remove user query and stop + if (response == undefined) { msgHist.pop(); return } - // successful query, save it in context history - msgHist.enqueue({ - role: 'assistant', - content: response, - images: messageAttachment || [] - }) + // if queue is full, remove the oldest message + while (msgHist.size() >= msgHist.capacity) msgHist.dequeue() - // only update the json on success - openChannelInfo(message.channelId, - message.channel as TextChannel, - message.author.tag, - msgHist.getItems() - ) - } catch (error: any) { - msgHist.pop() // remove message because of failure - message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`) + // successful query, save it in context history + msgHist.enqueue({ + role: 'assistant', + content: response, + images: messageAttachment || [] + }) + + // only update the json on success + openChannelInfo(message.channelId, + message.channel as TextChannel, + message.author.tag, + msgHist.getItems() + ) + } catch (error: any) { + msgHist.pop() // remove message because of failure + message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`) + } } -}) \ No newline at end of file +) \ No newline at end of file diff --git a/src/events/ready.ts b/src/events/ready.ts index 1d8c447..2194656 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -4,14 +4,15 @@ import commands from '../commands/index.js' // Log when the bot successfully logs in and export it export default event(Events.ClientReady, ({ log }, client) => { - // Register the commands associated with the bot upon loggin in - registerCommands(client, commands) + // Register the commands associated with the bot upon loggin in + registerCommands(client, commands) - // set status of the bot - client.user.setActivity({ - name: 'Powered by Ollama', - type: ActivityType.Custom - }) + // set status of the bot + client.user.setActivity({ + name: 'Powered by Ollama', + type: ActivityType.Custom + }) - log(`Logged in as ${client.user.username}.`) -}) \ No newline at end of file + log(`Logged in as ${client.user.username}.`) + } +) \ No newline at end of file diff --git a/src/events/threadDelete.ts b/src/events/threadDelete.ts index 619231d..a57f9cc 100644 --- a/src/events/threadDelete.ts +++ b/src/events/threadDelete.ts @@ -6,35 +6,36 @@ import fs from 'fs' * Event to remove the associated .json file for a thread once deleted */ export default event(Events.ThreadDelete, async ({ log }, thread: ThreadChannel) => { - // iterate through every guild member in the thread and delete their history, except the bot - try { - log(`Number of User Guild Members in Thread being deleted: ${thread.memberCount!! - 1}`) - const dirPath = 'data/' + // iterate through every guild member in the thread and delete their history, except the bot + try { + log(`Number of User Guild Members in Thread being deleted: ${thread.memberCount!! - 1}`) + const dirPath = 'data/' - // read all files in data/ - fs.readdir(dirPath, (error, files) => { - if (error) { - log(`Error reading directory ${dirPath}`, error) - return - } + // read all files in data/ + fs.readdir(dirPath, (error, files) => { + if (error) { + log(`Error reading directory ${dirPath}`, error) + return + } - // filter files by thread id being deleted - const filesToDiscard = files.filter( - file => file.startsWith(`${thread.id}-`) && + // filter files by thread id being deleted + const filesToDiscard = files.filter( + file => file.startsWith(`${thread.id}-`) && file.endsWith('.json')) - // remove files by unlinking - filesToDiscard.forEach(file => { - const filePath = dirPath + file - fs.unlink(filePath, error => { - if (error) - log(`Error deleting file ${filePath}`, error) - else - log(`Successfully deleted ${filePath} thread information`) + // remove files by unlinking + filesToDiscard.forEach(file => { + const filePath = dirPath + file + fs.unlink(filePath, error => { + if (error) + log(`Error deleting file ${filePath}`, error) + else + log(`Successfully deleted ${filePath} thread information`) + }) }) }) - }) - } catch (error) { - log(`Issue deleting user history files from ${thread.id}`) - } -}) \ No newline at end of file + } catch (error) { + log(`Issue deleting user history files from ${thread.id}`) + } + } +) \ No newline at end of file diff --git a/src/utils/env.ts b/src/utils/env.ts index 9177c69..158932d 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -21,14 +21,13 @@ export function getEnvVar(name: string, fallback?: string): string { throw new Error(`Environment variable ${name} is not set.`) // validate User-Generated Discord Application Tokens - if (name === "CLIENT_TOKEN") - if (value.length < 72) throw new Error(`The "CLIENT_TOKEN" provided is not of at least length 72. + if (name === "CLIENT_TOKEN" && value.length > 72) + throw new Error(`The "CLIENT_TOKEN" provided is not of at least length 72. This is probably an invalid token unless Discord updated their token policy. Please provide a valid token.`) // validate IPv4 address found in environment variables - if (name.endsWith("_IP") || name.endsWith("_ADDRESS")) - if (!ipValidate.test(value)) - throw new Error(`Environment variable ${name} does not follow IPv4 formatting.`) + if ((name.endsWith("_IP") || name.endsWith("_ADDRESS")) && !ipValidate.test(value)) + throw new Error(`Environment variable ${name} does not follow IPv4 formatting.`) // return env variable return value diff --git a/src/utils/events.ts b/src/utils/events.ts index eaed33b..6bb34a4 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -15,8 +15,8 @@ export type EventKeys = keyof ClientEvents // only wants keys of ClientEvents ob * @param msgHist message history */ export type ChatParams = { - model: string, - ollama: Ollama, + model: string, + ollama: Ollama, msgHist: UserMessage[] } @@ -24,6 +24,7 @@ export type ChatParams = { * Format for the messages to be stored when communicating when the bot * @param role either assistant, user, or system * @param content string of the message the user or assistant provided + * @param images array of images that the user or assistant provided */ export type UserMessage = { role: string, @@ -33,12 +34,18 @@ export type UserMessage = { // Event properties export interface EventProps { - client: Client - log: LogMethod - msgHist: Queue + client: Client, + log: LogMethod, + msgHist: Queue, ollama: Ollama, defaultModel: String } + +/** + * Format for the callback function tied to an event + * @param props the properties of the event + * @param args the arguments of the event + */ export type EventCallback = ( props: EventProps, ...args: ClientEvents[T] @@ -50,6 +57,12 @@ export interface Event { callback: EventCallback } +/** + * Method to create an event object + * @param key type of event + * @param callback function to run when event is triggered + * @returns event object + */ export function event(key: T, callback: EventCallback): Event { return { key, callback } } @@ -62,8 +75,8 @@ export function event(key: T, callback: EventCallback): * @param ollama the initialized ollama instance */ export function registerEvents( - client: Client, - events: Event[], + client: Client, + events: Event[], msgHist: Queue, ollama: Ollama, defaultModel: String diff --git a/src/utils/handlers/chatHistoryHandler.ts b/src/utils/handlers/chatHistoryHandler.ts index 8895739..186e342 100644 --- a/src/utils/handlers/chatHistoryHandler.ts +++ b/src/utils/handlers/chatHistoryHandler.ts @@ -80,7 +80,14 @@ export async function openChannelInfo(filename: string, channel: TextChannel | T } }) } else { // file doesn't exist, create it - const object: Configuration = JSON.parse(`{ \"id\": \"${channel?.id}\", \"name\": \"${channel?.name}\", \"user\": \"${user}\", \"messages\": []}`) + const object: Configuration = JSON.parse( + `{ + \"id\": \"${channel?.id}\", + \"name\": \"${channel?.name}\", + \"user\": \"${user}\", + \"messages\": [] + }` + ) const directory = path.dirname(fullFileName) if (!fs.existsSync(directory)) @@ -103,7 +110,7 @@ export async function getChannelInfo(filename: string, callback: (config: Channe if (fs.existsSync(fullFileName)) { fs.readFile(fullFileName, 'utf8', (error, data) => { if (error) { - callback(undefined) + callback(undefined) return // something went wrong... stop } callback(JSON.parse(data)) diff --git a/src/utils/handlers/configHandler.ts b/src/utils/handlers/configHandler.ts index a46f394..b347c74 100644 --- a/src/utils/handlers/configHandler.ts +++ b/src/utils/handlers/configHandler.ts @@ -12,7 +12,7 @@ import path from 'path' // add type of change (server, user) export function openConfig(filename: string, key: string, value: any) { const fullFileName = `data/${filename}` - + // check if the file exists, if not then make the config file if (fs.existsSync(fullFileName)) { fs.readFile(fullFileName, 'utf8', (error, data) => { @@ -58,7 +58,7 @@ export async function getServerConfig(filename: string, callback: (config: Serve if (fs.existsSync(fullFileName)) { fs.readFile(fullFileName, 'utf8', (error, data) => { if (error) { - callback(undefined) + callback(undefined) return // something went wrong... stop } callback(JSON.parse(data)) @@ -81,7 +81,7 @@ export async function getUserConfig(filename: string, callback: (config: UserCon if (fs.existsSync(fullFileName)) { fs.readFile(fullFileName, 'utf8', (error, data) => { if (error) { - callback(undefined) + callback(undefined) return // something went wrong... stop } callback(JSON.parse(data)) diff --git a/src/utils/handlers/streamHandler.ts b/src/utils/handlers/streamHandler.ts index 4b6b4f4..48b5e6f 100644 --- a/src/utils/handlers/streamHandler.ts +++ b/src/utils/handlers/streamHandler.ts @@ -5,7 +5,7 @@ import { AbortableAsyncIterator } from "ollama/src/utils.js" /** * Method to query the Ollama client for async generation * @param params - * @returns Asyn + * @returns AsyncIterator generated by the Ollama client */ export async function streamResponse(params: ChatParams): Promise> { return await params.ollama.chat({ diff --git a/src/utils/messageNormal.ts b/src/utils/messageNormal.ts index 057d3a8..a438e41 100644 --- a/src/utils/messageNormal.ts +++ b/src/utils/messageNormal.ts @@ -28,7 +28,7 @@ export async function normalMessage( model: model, ollama: ollama, msgHist: msgHist.getItems() - } + } // run query based on stream preference, true = stream, false = block if (stream) { @@ -40,14 +40,15 @@ export async function normalMessage( result = portion.message.content // new message block, wait for it to send and assign new block to respond. - await channel.send("Creating new stream block...").then(sentMessage => { messageBlock = sentMessage }) + await channel.send("Creating new stream block...") + .then(sentMessage => { messageBlock = sentMessage }) } else { result += portion.message.content - + // ensure block is not empty if (result.length > 5) - messageBlock.edit(result) - } + messageBlock.edit(result) + } console.log(result) } } @@ -70,8 +71,8 @@ export async function normalMessage( channel.send(result) } else // edit the 'generic' response to new message since <2000 sentMessage.edit(result) - } - } catch(error: any) { + } + } catch (error: any) { console.log(`[Util: messageNormal] Error creating message: ${error.message}`) if (error.message.includes('try pulling it first')) sentMessage.edit(`**Response generation failed.**\n\nReason: You do not have the ${model} downloaded. Ask an admin to pull it using the \`pull-model\` command.`) diff --git a/tests/commands.test.ts b/tests/commands.test.ts index d4aa6d0..c6b6db9 100644 --- a/tests/commands.test.ts +++ b/tests/commands.test.ts @@ -34,7 +34,8 @@ describe('Commands Existence', () => { // test specific commands in the object it('references specific commands', () => { const commandsString = commands.map(e => e.name).join(', ') - expect(commandsString).toBe('thread, private-thread, message-stream, toggle-chat, shutoff, modify-capacity, clear-user-channel-history, pull-model, switch-model, delete-model') + const expectedCommands = ['thread', 'private-thread', 'message-stream', 'toggle-chat', 'shutoff', 'modify-capacity', 'clear-user-channel-history', 'pull-model', 'switch-model', 'delete-model'] + expect(commandsString).toBe(expectedCommands.join(', ')) }) }) @@ -49,23 +50,23 @@ describe('User Command Tests', () => { }) it('run clear-user-channel-history command', () => { - + }) it('run message-stream command', () => { - + }) it('run message-style command', () => { - + }) - + it('run thread command', () => { - + }) it('run private-thread command', () => { - + }) }) @@ -75,10 +76,10 @@ describe('User Command Tests', () => { */ describe('Admin Command Tests', () => { it('run shutoff command', () => { - + }) it('run toggle-chat command', () => { - + }) }) \ No newline at end of file diff --git a/tests/queue.test.ts b/tests/queue.test.ts index 622cc99..83c1e89 100644 --- a/tests/queue.test.ts +++ b/tests/queue.test.ts @@ -8,7 +8,7 @@ import { Queue } from '../src/queues/queue.js' * @param fn function holding tests to run */ describe('Queue Structure', () => { - let queue= new Queue() + let queue = new Queue() // test for queue creation it('creates a new queue', () => {