From 35b9ad71cb900161c7424a6af27f018d714e6bb7 Mon Sep 17 00:00:00 2001 From: Kevin Dang <77701718+kevinthedang@users.noreply.github.com> Date: Thu, 4 Jul 2024 13:54:25 -0700 Subject: [PATCH] User vs Server Preferences (#80) * Update: Server vs User prefs * Add: User vs Server Prefs * Update: version increment * Fix: src and tests added to validation range --- .github/workflows/build.yml | 2 + .github/workflows/test.yml | 2 + docker-compose.yml | 2 +- package-lock.json | 4 +- package.json | 2 +- src/commands/capacity.ts | 2 +- src/commands/channelToggle.ts | 2 +- src/commands/disable.ts | 2 +- src/commands/messageStream.ts | 2 +- src/commands/messageStyle.ts | 2 +- src/events/messageCreate.ts | 54 ++++++++++++++------ src/utils/jsonHandler.ts | 94 +++++++++++++++++++++++++++++------ 12 files changed, 130 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index efa4bef..a3de07d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,8 @@ on: - master paths: - '/' + - 'src/**' + - 'tests/**' - '!docs/**' - '!imgs/**' - '!.github/**' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6928a1..36500e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,8 @@ on: - master paths: - '/' + - 'src/**' + - 'tests/**' - '!docs/**' - '!imgs/**' - '!.github/**' diff --git a/docker-compose.yml b/docker-compose.yml index f493f8d..207df14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: build: ./ # find docker file in designated path container_name: discord restart: always # rebuild container always - image: discord/bot:0.5.2 + image: discord/bot:0.5.4 environment: CLIENT_TOKEN: ${CLIENT_TOKEN} GUILD_ID: ${GUILD_ID} diff --git a/package-lock.json b/package-lock.json index 059f341..302930b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "discord-ollama", - "version": "0.5.2", + "version": "0.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "discord-ollama", - "version": "0.5.2", + "version": "0.5.4", "license": "ISC", "dependencies": { "discord.js": "^14.15.3", diff --git a/package.json b/package.json index 9d7260b..8724956 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord-ollama", - "version": "0.5.2", + "version": "0.5.4", "description": "Ollama Integration into discord", "main": "build/index.js", "exports": "./build/index.js", diff --git a/src/commands/capacity.ts b/src/commands/capacity.ts index 40c9100..6a47dc3 100644 --- a/src/commands/capacity.ts +++ b/src/commands/capacity.ts @@ -23,7 +23,7 @@ export const Capacity: SlashCommand = { if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return // set state of bot chat features - openConfig('config.json', interaction.commandName, interaction.options.get('context-capacity')?.value) + openConfig(`${interaction.client.user.username}-config.json`, interaction.commandName, interaction.options.get('context-capacity')?.value) interaction.reply({ content: `Message History Capacity has been set to \`${interaction.options.get('context-capacity')?.value}\``, diff --git a/src/commands/channelToggle.ts b/src/commands/channelToggle.ts index 30a9d96..78b63d6 100644 --- a/src/commands/channelToggle.ts +++ b/src/commands/channelToggle.ts @@ -24,7 +24,7 @@ export const ChannelToggle: SlashCommand = { // set state of bot channel preferences - openConfig('config.json', interaction.commandName, interaction.options.get('toggle-channel')?.value) + openConfig(`${interaction.guildId}-config.json`, interaction.commandName, interaction.options.get('toggle-channel')?.value) interaction.reply({ content: `Channel Preferences have for Regular Channels set to \`${interaction.options.get('toggle-channel')?.value}\``, diff --git a/src/commands/disable.ts b/src/commands/disable.ts index d0dfa55..ca0bd81 100644 --- a/src/commands/disable.ts +++ b/src/commands/disable.ts @@ -32,7 +32,7 @@ export const Disable: SlashCommand = { } // set state of bot chat features - openConfig('config.json', interaction.commandName, interaction.options.get('enabled')?.value) + openConfig(`${interaction.guildId}-config.json`, interaction.commandName, interaction.options.get('enabled')?.value) interaction.reply({ content: `Chat features has been \`${interaction.options.get('enabled')?.value ? "enabled" : "disabled" }\``, diff --git a/src/commands/messageStream.ts b/src/commands/messageStream.ts index 6e98040..ab56b3f 100644 --- a/src/commands/messageStream.ts +++ b/src/commands/messageStream.ts @@ -23,7 +23,7 @@ export const MessageStream: SlashCommand = { if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return // save value to json and write to it - openConfig('config.json', interaction.commandName, interaction.options.get('stream')?.value) + openConfig(`${interaction.client.user.username}-config.json`, interaction.commandName, interaction.options.get('stream')?.value) interaction.reply({ content: `Message streaming preferences set to: \`${interaction.options.get('stream')?.value}\``, diff --git a/src/commands/messageStyle.ts b/src/commands/messageStyle.ts index 52687b6..ae7a1c7 100644 --- a/src/commands/messageStyle.ts +++ b/src/commands/messageStyle.ts @@ -23,7 +23,7 @@ export const MessageStyle: SlashCommand = { if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return // set the message style - openConfig('config.json', interaction.commandName, interaction.options.get('embed')?.value) + openConfig(`${interaction.client.user.username}-config.json`, interaction.commandName, interaction.options.get('embed')?.value) interaction.reply({ content: `Message style preferences for embed set to: \`${interaction.options.get('embed')?.value}\``, diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 27229a9..d13bacc 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -1,30 +1,38 @@ import { embedMessage, event, Events, normalMessage, UserMessage } from '../utils/index.js' -import { Configuration, getChannelInfo, getConfig, getThread, openChannelInfo, openConfig, openThreadInfo } from '../utils/jsonHandler.js' +import { getChannelInfo, getServerConfig, getThread, getUserConfig, openChannelInfo, openConfig, openThreadInfo, ServerConfig, UserConfig } from '../utils/jsonHandler.js' import { clean } from '../utils/mentionClean.js' import { TextChannel, ThreadChannel } from 'discord.js' /** * Max Message length for free users is 2000 characters (bot or not). + * Bot supports infinite lengths for normal messages. + * * @param message the message received from the channel */ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama, client }, message) => { log(`Message \"${clean(message.content)}\" from ${message.author.tag} in channel/thread ${message.channelId}.`) // Do not respond if bot talks in the chat - if (message.author.tag === message.client.user.tag) return + if (message.author.username === message.client.user.username) return // Only respond if message mentions the bot if (!message.mentions.has(tokens.clientUid)) return + // default stream to false let shouldStream = false - - // Try to query and send embed + try { - const config: Configuration = await new Promise((resolve, reject) => { - getConfig('config.json', (config) => { + // Retrieve Server/Guild Preferences + const serverConfig: ServerConfig = await new Promise((resolve, reject) => { + getServerConfig(`${message.guildId}-config.json`, (config) => { // check if config.json exists if (config === undefined) { - reject(new Error('No Configuration is set up.\n\nCreating \`config.json\` with \`message-style\` set as \`false\` for regular messages.\nPlease try chatting again.')) + // Allowing chat options to be available + openConfig(`${message.guildId}-config.json`, 'toggle-chat', true) + + // default to channel scope chats + openConfig(`${message.guildId}-config.json`, 'channel-toggle', true) + reject(new Error('No Server Preferences is set up.\n\nCreating default server preferences file...\nPlease try chatting again.')) return } @@ -38,10 +46,23 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama if (config.options['channel-toggle']) { openChannelInfo(message.channelId, message.channel as TextChannel, - message.author.tag + message.author.username ) } + resolve(config) + }) + }) + + // Retrieve User Preferences + const userConfig: 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) + reject(new Error('No User Preferences is set up.\n\nCreating preferences file with \`message-style\` set as \`false\` for regular messages.\nPlease try chatting again.')) + return + } + // check if there is a set capacity in config if (typeof config.options['modify-capacity'] !== 'number') log(`Capacity is undefined, using default capacity of ${msgHist.capacity}.`) @@ -51,23 +72,25 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama 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 - + resolve(config) }) }) + + // need new check for "open/active" threads/channels here! const chatMessages: UserMessage[] = await new Promise((resolve) => { // set new queue to modify - if (config.options['channel-toggle']) { - getChannelInfo(`${message.channelId}-${message.author.tag}.json`, (channelInfo) => { + if (serverConfig.options['channel-toggle']) { + getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => { if (channelInfo?.messages) resolve(channelInfo.messages) else - log(`Channel ${message.channel}-${message.author.tag} does not exist.`) + log(`Channel ${message.channel}-${message.author.username} does not exist.`) }) } else { getThread(`${message.channelId}.json`, (threadInfo) => { @@ -95,7 +118,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama }) // undefined or false, use normal, otherwise use embed - if (config.options['message-style']) + if (userConfig.options['message-style']) response = await embedMessage(message, ollama, tokens, msgHist, shouldStream) else response = await normalMessage(message, ollama, tokens, msgHist, shouldStream) @@ -113,7 +136,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama }) // only update the json on success - if (config.options['channel-toggle']) { + if (serverConfig.options['channel-toggle']) { openChannelInfo(message.channelId, message.channel as TextChannel, message.author.tag, @@ -127,7 +150,6 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama } } catch (error: any) { msgHist.pop() // remove message because of failure - openConfig('config.json', 'message-style', false) message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`) } }) \ No newline at end of file diff --git a/src/utils/jsonHandler.ts b/src/utils/jsonHandler.ts index 599eecd..85ff44e 100644 --- a/src/utils/jsonHandler.ts +++ b/src/utils/jsonHandler.ts @@ -3,15 +3,39 @@ import { UserMessage } from './events.js' import fs from 'fs' import path from 'path' +export interface UserConfiguration { + 'message-stream'?: boolean, + 'message-style'?: boolean, + 'modify-capacity': number +} + +export interface ServerConfiguration { + 'toggle-chat'?: boolean, + 'channel-toggle'?: boolean +} + +/** + * Parent Configuration interface + * + * @see ServerConfiguration server settings per guild + * @see UserConfiguration user configurations (only for the user for any server) + */ export interface Configuration { readonly name: string - options: { - 'message-stream'?: boolean, - 'message-style'?: boolean, - 'toggle-chat'?: boolean, - 'modify-capacity'?: number, - 'channel-toggle'?: boolean - } + options: UserConfiguration | ServerConfiguration +} + +/** + * User config to use outside of this file + */ +export interface UserConfig { + readonly name: string + options: UserConfiguration +} + +export interface ServerConfig { + readonly name: string + options: ServerConfiguration } export interface Thread { @@ -27,6 +51,14 @@ export interface Channel { messages: UserMessage[] } +function isUserConfigurationKey(key: string): key is keyof UserConfiguration { + return ['message-stream', 'message-style', 'modify-capacity'].includes(key); +} + +function isServerConfigurationKey(key: string): key is keyof ServerConfiguration { + return ['toggle-chat', 'channel-toggle'].includes(key); +} + /** * Method to open a file in the working directory and modify/create it * @@ -34,27 +66,34 @@ export interface Channel { * @param key key value to access * @param value new value to assign */ +// 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(filename)) { - fs.readFile(filename, 'utf8', (error, data) => { + if (fs.existsSync(fullFileName)) { + fs.readFile(fullFileName, 'utf8', (error, data) => { if (error) console.log(`[Error: openConfig] Incorrect file format`) else { const object = JSON.parse(data) object['options'][key] = value - fs.writeFileSync(filename, JSON.stringify(object, null, 2)) + fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2)) } }) } else { // work on dynamic file creation - const object: Configuration = JSON.parse('{ \"name\": \"Discord Ollama Confirgurations\" }') + let object: Configuration + if (isServerConfigurationKey(key)) + object = JSON.parse('{ \"name\": \"Server Confirgurations\" }') + else + object = JSON.parse('{ \"name\": \"User Confirgurations\" }') // set standard information for config file and options object['options'] = { [key]: value } - fs.writeFileSync(filename, JSON.stringify(object, null, 2)) + fs.writeFileSync(`data/${filename}`, JSON.stringify(object, null, 2)) console.log(`[Util: openConfig] Created '${filename}' in working directory`) } } @@ -65,10 +104,35 @@ export function openConfig(filename: string, key: string, value: any) { * @param filename name of the configuration file to get * @param callback function to allow a promise from getting the config */ -export async function getConfig(filename: string, callback: (config: Configuration | undefined) => void): Promise { +export async function getServerConfig(filename: string, callback: (config: ServerConfig | undefined) => void): Promise { + const fullFileName = `data/${filename}` + // attempt to read the file and get the configuration - if (fs.existsSync(filename)) { - fs.readFile(filename, 'utf8', (error, data) => { + if (fs.existsSync(fullFileName)) { + fs.readFile(fullFileName, 'utf8', (error, data) => { + if (error) { + callback(undefined) + return // something went wrong... stop + } + callback(JSON.parse(data)) + }) + } else { + callback(undefined) // file not found + } +} + +/** + * Method to obtain the configurations of the message chat/thread + * + * @param filename name of the configuration file to get + * @param callback function to allow a promise from getting the config + */ +export async function getUserConfig(filename: string, callback: (config: UserConfig | undefined) => void): Promise { + const fullFileName = `data/${filename}` + + // attempt to read the file and get the configuration + if (fs.existsSync(fullFileName)) { + fs.readFile(fullFileName, 'utf8', (error, data) => { if (error) { callback(undefined) return // something went wrong... stop