diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 186c0e4..8a7e8f6 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -54,10 +54,14 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client // Check if message is a bot response to avoid loops const isBotResponseKey = `message:${message.id}:is_bot_response` if (isBotMessage) { - const isBotResponse = await redis.get(isBotResponseKey) - if (isBotResponse === 'true') { - log(`Skipping bot message ${message.id} as it is a bot response.`) - return + try { + const isBotResponse = await redis.get(isBotResponseKey) + if (isBotResponse === 'true') { + log(`Skipping bot message ${message.id} as it is a bot response.`) + return + } + } catch (error) { + log(`Failed to check is_bot_response: ${error}`) } } @@ -110,16 +114,18 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client try { // Retrieve Server/Guild Preferences let attempt = 0 + let serverConfig: ServerConfig | undefined while (attempt < maxRetries) { try { - await new Promise((resolve, reject) => { + serverConfig = await new Promise((resolve, reject) => { getServerConfig(`${message.guildId}-config.json`, (config) => { - if (config === undefined) { - redis.set(`server:${message.guildId}:config`, JSON.stringify({ options: { 'toggle-chat': true } })) - reject(new Error('Failed to locate or create Server Preferences\n\nPlease try chatting again...')) + if (!config) { + reject(new Error('Failed to retrieve or create Server Preferences')) } else if (!config.options['toggle-chat']) { reject(new Error('Admin(s) have disabled chat features.\n\nPlease contact your server\'s admin(s).')) } else { + // Sync with Redis + redis.set(`server:${message.guildId}:config`, JSON.stringify(config)).catch((err) => log(`Failed to sync server config to Redis: ${err}`)) resolve(config) } }) @@ -131,7 +137,21 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client 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...`) + // Check Redis for server config as fallback + try { + const redisConfig = await redis.get(`server:${message.guildId}:config`) + if (redisConfig) { + serverConfig = JSON.parse(redisConfig) + if (serverConfig.options['toggle-chat']) { + break + } else { + throw new Error('Admin(s) have disabled chat features.\n\nPlease contact your server\'s admin(s).')) + } + } + } catch (redisError) { + log(`Redis fallback failed: ${redisError}`) + } + throw new Error('Could not retrieve Server Preferences, please try chatting again...') } } } @@ -305,7 +325,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client : `Responding to user ${message.author.tag}` // Construct prompt with [CHARACTER], [SENTIMENT], and [CONTEXT] - const prompt = `[CHARACTER]\n${personality}\n[SENTIMENT]\n${sentimentData}\n[CONTEXT]\n${messageContext}\n[USER_INPUT]\n${cleanedMessage}` + const prompt = `[CHARACTER]\n${personality}\n[SENTIMENT]\n${sentimentData}\n[CONTEXT]\n${messageContext}\n[USER_INPUT]\n${cleanedMessage}\n[INSTRUCTION]\nRespond in JSON format with keys: status, reply, metadata. Example: {"status":"success","reply":"Hi!","metadata":{}}` // Set up message history queue msgHist.setQueue(chatMessages) @@ -333,6 +353,9 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client // Strip Markdown code fences if present let content = response.message.content content = content.replace(/^```json\n|```$/g, '').trim() + if (!content.startsWith('{') || !content.endsWith('}')) { + throw new Error('Model response is not valid JSON') + } jsonResponse = JSON.parse(content) if (!jsonResponse.status || !jsonResponse.reply) { throw new Error('Missing status or reply in model response') diff --git a/src/utils/configInterfaces.ts b/src/utils/configInterfaces.ts index 98bb2e1..bd112a1 100644 --- a/src/utils/configInterfaces.ts +++ b/src/utils/configInterfaces.ts @@ -1,67 +1,91 @@ -import { ChannelType } from 'discord.js' -import { UserMessage } from './index.js' - -export interface UserConfiguration { - 'message-stream'?: boolean, - 'modify-capacity': number, - 'switch-model': string -} - -export interface ServerConfiguration { - 'toggle-chat'?: boolean, -} +import { Configuration, ServerConfig, UserConfig, isServerConfigurationKey } from '../index.js' +import fs from 'fs/promises' // Use promises for async +import path from 'path' /** - * Parent Configuration interface + * Method to open a file in the working directory and modify/create it * - * @see ServerConfiguration server settings per guild - * @see UserConfiguration user configurations (only for the user for any server) + * @param filename name of the file + * @param key key value to access + * @param value new value to assign */ -export interface Configuration { - readonly name: string - options: UserConfiguration | ServerConfiguration +export async function openConfig(filename: string, key: string, value: any): Promise { + const fullFileName = `data/${filename}` + + let object: Configuration; + try { + if (await fs.access(fullFileName).then(() => true).catch(() => false)) { + const data = await fs.readFile(fullFileName, 'utf8'); + object = JSON.parse(data); + object['options'][key] = value; + } else { + // Create new config + object = { + name: isServerConfigurationKey(key) ? "Server Configurations" : "User Configurations", + options: { [key]: value } + }; + + const directory = path.dirname(fullFileName); + await fs.mkdir(directory, { recursive: true }); + } + + await fs.writeFile(fullFileName, JSON.stringify(object, null, 2)); + console.log(`[Util: openConfig] Updated/Created '${filename}' in working directory`); + } catch (error) { + console.error(`[Error: openConfig] Failed to process config file: ${error}`); + throw error; + } } /** - * User config to use outside of this file + * 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 interface UserConfig { - readonly name: string - options: UserConfiguration -} +export async function getServerConfig(filename: string, callback: (config: ServerConfig | undefined) => void): Promise { + const fullFileName = `data/${filename}`; -export interface ServerConfig { - readonly name: string - options: ServerConfiguration -} - -export interface Channel { - readonly id: string - readonly name: string - readonly user: string - messages: UserMessage[] + try { + if (await fs.access(fullFileName).then(() => true).catch(() => false)) { + const data = await fs.readFile(fullFileName, 'utf8'); + callback(JSON.parse(data)); + } else { + // Create default server config + const defaultConfig: ServerConfig = { + name: "Server Configurations", + options: { 'toggle-chat': true } + }; + const directory = path.dirname(fullFileName); + await fs.mkdir(directory, { recursive: true }); + await fs.writeFile(fullFileName, JSON.stringify(defaultConfig, null, 2)); + console.log(`[Util: getServerConfig] Created default config '${filename}'`); + callback(defaultConfig); + } + } catch (error) { + console.error(`[Error: getServerConfig] Failed to read or create config: ${error}`); + callback(undefined); + } } /** - * The following 2 types is allow for better readability in commands - * Admin Command -> Don't run in Threads - * User Command -> Used anywhere + * 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 const AdminCommand = [ - ChannelType.GuildText -] +export async function getUserConfig(filename: string, callback: (config: UserConfig | undefined) => void): Promise { + const fullFileName = `data/${filename}`; -export const UserCommand = [ - ChannelType.GuildText, - ChannelType.PublicThread, - ChannelType.PrivateThread -] - -/** - * Check if the configuration we are editing/taking from is a Server Config - * @param key name of command we ran - * @returns true if command is from Server Config, false otherwise - */ -export function isServerConfigurationKey(key: string): key is keyof ServerConfiguration { - return ['toggle-chat'].includes(key); -} \ No newline at end of file + try { + if (await fs.access(fullFileName).then(() => true).catch(() => false)) { + const data = await fs.readFile(fullFileName, 'utf8'); + callback(JSON.parse(data)); + } else { + callback(undefined); // User config handled by Redis in messageCreate.ts + } + } catch (error) { + console.error(`[Error: getUserConfig] Failed to read config: ${error}`); + callback(undefined); + } +}