diff --git a/.env.sample b/.env.sample index b81c1cf..0e68a3f 100644 --- a/.env.sample +++ b/.env.sample @@ -10,11 +10,11 @@ MODEL = MODEL_NAME # discord bot user id for mentions CLIENT_UID = BOT_USER_ID -# ip/port address of docker container, I use 172.18.X.X for docker, 127.0.0.1 for local +# ip/port address of docker container, I use 172.18.0.3 for docker, 127.0.0.1 for local OLLAMA_IP = IP_ADDRESS OLLAMA_PORT = PORT -# ip address for discord bot container, I use 172.18.X.X, use different IP than ollama_ip +# ip address for discord bot container, I use 172.18.0.2, use different IP than ollama_ip DISCORD_IP = IP_ADDRESS # subnet address, ex. 172.18.0.0 as we use /16. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a88bee9..efa4bef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,18 @@ name: Builds run-name: Validate Node and Docker Builds on: - push: + pull_request: branches: - master + paths: + - '/' + - '!docs/**' + - '!imgs/**' + - '!.github/**' + - '.github/workflows/**' + - '!.gitignore' + - '!LICENSE' + - '!README' jobs: Discord-Node-Build: # test if the node install and run diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f87dab..c6928a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ on: branches: - master paths: + - '/' - '!docs/**' - '!imgs/**' - '!.github/**' diff --git a/README.md b/README.md index f3bafba..ee5a44c 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ Ollama is an AI model management tool that allows users to install and use custo The project aims to: * [x] Create a Discord bot that will utilize Ollama and chat to chat with users! * [ ] User Preferences on Chat - * [ ] Message Persistance on Channels and Threads + * [x] Message Persistance on Channels and Threads * [x] Threads - * [ ] Channels + * [x] Channels * [x] Containerization with Docker * [x] Slash Commands Compatible * [x] Generated Token Length Handling for >2000 diff --git a/docker-compose.yml b/docker-compose.yml index 39cd4e5..f493f8d 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.1 + image: discord/bot:0.5.2 environment: CLIENT_TOKEN: ${CLIENT_TOKEN} GUILD_ID: ${GUILD_ID} diff --git a/package-lock.json b/package-lock.json index 668fdb7..059f341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "discord-ollama", - "version": "0.5.1", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "discord-ollama", - "version": "0.5.1", + "version": "0.5.2", "license": "ISC", "dependencies": { "discord.js": "^14.15.3", diff --git a/package.json b/package.json index 02eafa7..9d7260b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord-ollama", - "version": "0.5.1", + "version": "0.5.2", "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 a512fec..40c9100 100644 --- a/src/commands/capacity.ts +++ b/src/commands/capacity.ts @@ -20,7 +20,7 @@ export const Capacity: SlashCommand = { run: async (client: Client, interaction: CommandInteraction) => { // fetch channel and message const channel = await client.channels.fetch(interaction.channelId) - if (!channel || channel.type !== ChannelType.PublicThread) return + 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) diff --git a/src/commands/channelToggle.ts b/src/commands/channelToggle.ts new file mode 100644 index 0000000..30a9d96 --- /dev/null +++ b/src/commands/channelToggle.ts @@ -0,0 +1,34 @@ +import { ApplicationCommandOptionType, ChannelType, Client, CommandInteraction } from 'discord.js' +import { SlashCommand } from '../utils/commands.js' +import { openConfig } from '../utils/jsonHandler.js' + +export const ChannelToggle: SlashCommand = { + name: 'channel-toggle', + description: 'toggles channel or thread usage.', + + // set user option for toggling + options: [ + { + name: 'toggle-channel', + description: 'toggle channel usage, otherwise threads', + type: ApplicationCommandOptionType.Boolean, + required: true + } + ], + + // Query for chatting preference + run: async (client: Client, interaction: CommandInteraction) => { + // fetch channel location + const channel = await client.channels.fetch(interaction.channelId) + if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return + + + // set state of bot channel preferences + openConfig('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}\``, + ephemeral: true + }) + } +} diff --git a/src/commands/index.ts b/src/commands/index.ts index 0722371..a3e03ac 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -6,6 +6,7 @@ import { Disable } from './disable.js' import { Shutoff } from './shutoff.js' import { Capacity } from './capacity.js' import { PrivateThreadCreate } from './threadPrivateCreate.js' +import { ChannelToggle } from './channelToggle.js' export default [ ThreadCreate, @@ -14,5 +15,6 @@ export default [ MessageStream, Disable, Shutoff, - Capacity + Capacity, + ChannelToggle ] as SlashCommand[] \ No newline at end of file diff --git a/src/commands/messageStream.ts b/src/commands/messageStream.ts index e6e8187..6e98040 100644 --- a/src/commands/messageStream.ts +++ b/src/commands/messageStream.ts @@ -20,7 +20,7 @@ export const MessageStream: SlashCommand = { run: async (client: Client, interaction: CommandInteraction) => { // verify channel const channel = await client.channels.fetch(interaction.channelId) - if (!channel || channel.type !== ChannelType.PublicThread) return + 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) diff --git a/src/commands/messageStyle.ts b/src/commands/messageStyle.ts index 92fcb76..52687b6 100644 --- a/src/commands/messageStyle.ts +++ b/src/commands/messageStyle.ts @@ -20,7 +20,7 @@ export const MessageStyle: SlashCommand = { run: async (client: Client, interaction: CommandInteraction) => { // fetch channel and message const channel = await client.channels.fetch(interaction.channelId) - if (!channel || channel.type !== ChannelType.PublicThread) return + if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return // set the message style openConfig('config.json', interaction.commandName, interaction.options.get('embed')?.value) diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 41ce5ce..27229a9 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -1,7 +1,7 @@ import { embedMessage, event, Events, normalMessage, UserMessage } from '../utils/index.js' -import { Configuration, getConfig, getThread, openConfig, openThreadInfo } from '../utils/jsonHandler.js' +import { Configuration, getChannelInfo, getConfig, getThread, openChannelInfo, openConfig, openThreadInfo } from '../utils/jsonHandler.js' import { clean } from '../utils/mentionClean.js' -import { ThreadChannel } from 'discord.js' +import { TextChannel, ThreadChannel } from 'discord.js' /** * Max Message length for free users is 2000 characters (bot or not). @@ -10,17 +10,6 @@ import { ThreadChannel } from 'discord.js' 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}.`) - // need new check for "open/active" threads here! - const threadMessages: UserMessage[] = await new Promise((resolve) => { - // set new queue to modify - getThread(`${message.channelId}.json`, (threadInfo) => { - if (threadInfo?.messages) - resolve(threadInfo.messages) - else - log(`Channel/Thread ${message.channelId} does not exist.`) - }) - }) - // Do not respond if bot talks in the chat if (message.author.tag === message.client.user.tag) return @@ -45,6 +34,14 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama return } + // ensure channel json exists, if not create it + if (config.options['channel-toggle']) { + openChannelInfo(message.channelId, + message.channel as TextChannel, + message.author.tag + ) + } + // 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}.`) @@ -62,11 +59,31 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama }) }) + // 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 (channelInfo?.messages) + resolve(channelInfo.messages) + else + log(`Channel ${message.channel}-${message.author.tag} does not exist.`) + }) + } else { + getThread(`${message.channelId}.json`, (threadInfo) => { + if (threadInfo?.messages) + resolve(threadInfo.messages) + else + log(`Thread ${message.channelId} does not exist.`) + }) + } + }) + // response string for ollama to put its response let response: string // set up new queue - msgHist.setQueue(threadMessages) + msgHist.setQueue(chatMessages) // check if we can push, if not, remove oldest while (msgHist.size() >= msgHist.capacity) msgHist.dequeue() @@ -96,10 +113,18 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama }) // only update the json on success - openThreadInfo(`${message.channelId}.json`, - client.channels.fetch(message.channelId) as unknown as ThreadChannel, - msgHist.getItems() - ) + if (config.options['channel-toggle']) { + openChannelInfo(message.channelId, + message.channel as TextChannel, + message.author.tag, + msgHist.getItems() + ) + } else { + openThreadInfo(`${message.channelId}.json`, + client.channels.fetch(message.channelId) as unknown as ThreadChannel, + msgHist.getItems() + ) + } } catch (error: any) { msgHist.pop() // remove message because of failure openConfig('config.json', 'message-style', false) diff --git a/src/utils/jsonHandler.ts b/src/utils/jsonHandler.ts index 2592661..599eecd 100644 --- a/src/utils/jsonHandler.ts +++ b/src/utils/jsonHandler.ts @@ -1,4 +1,4 @@ -import { ThreadChannel } from 'discord.js' +import { TextChannel, ThreadChannel } from 'discord.js' import { UserMessage } from './events.js' import fs from 'fs' import path from 'path' @@ -9,7 +9,8 @@ export interface Configuration { 'message-stream'?: boolean, 'message-style'?: boolean, 'toggle-chat'?: boolean, - 'modify-capacity'?: number + 'modify-capacity'?: number, + 'channel-toggle'?: boolean } } @@ -19,6 +20,13 @@ export interface Thread { messages: UserMessage[] } +export interface Channel { + readonly id: string + readonly name: string + readonly user: string + messages: UserMessage[] +} + /** * Method to open a file in the working directory and modify/create it * @@ -85,7 +93,7 @@ export function openThreadInfo(filename: string, thread: ThreadChannel, messages if (fs.existsSync(fullFileName)) { fs.readFile(fullFileName, 'utf8', (error, data) => { if (error) - console.log(`[Error: openConfig] Incorrect file format`) + console.log(`[Error: openThreadInfo] Incorrect file format`) else { const object = JSON.parse(data) object['messages'] = messages as [] @@ -125,4 +133,74 @@ export async function getThread(filename: string, callback: (config: Thread | un } else { callback(undefined) // file not found } +} + +/** + * Method to open the channel history + * + * @param filename name of the json file for the channel by user + * @param channel the text channel info + * @param user the user's name + * @param messages their messages + */ +export async function openChannelInfo(filename: string, channel: TextChannel, user: string, messages: UserMessage[] = []): Promise { + // thread exist handler + const isThread: boolean = await new Promise((resolve) => { + getThread(`${channel.id}.json`, (threadInfo) => { + if (threadInfo?.messages) + resolve(true) + else + resolve(false) + }) + }) + + // This is an existing thread, don't create another json + if (isThread) return + + const fullFileName = `data/${filename}-${user}.json` + if (fs.existsSync(fullFileName)) { + fs.readFile(fullFileName, 'utf8', (error, data) => { + if (error) + console.log(`[Error: openChannelInfo] Incorrect file format`) + else { + const object = JSON.parse(data) + if (object['messages'].length === 0) + object['messages'] = messages as [] + else if (object['messages'].length !== 0 && messages.length !== 0) + object['messages'] = messages as [] + fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2)) + } + }) + } else { // file doesn't exist, create it + const object: Configuration = JSON.parse(`{ \"id\": \"${channel?.id}\", \"name\": \"${channel?.name}\", \"user\": \"${user}\", \"messages\": []}`) + + const directory = path.dirname(fullFileName) + if (!fs.existsSync(directory)) + fs.mkdirSync(directory, { recursive: true }) + + // only creating it, no need to add anything + fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2)) + console.log(`[Util: openChannelInfo] Created '${fullFileName}' in working directory`) + } +} + +/** + * Method to get the channel information/history + * + * @param filename name of the json file for the channel by user + * @param callback function to handle resolving message history + */ +export async function getChannelInfo(filename: string, callback: (config: Channel | undefined) => void): Promise { + const fullFileName = `data/${filename}` + 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 + } } \ No newline at end of file diff --git a/tests/commands.test.ts b/tests/commands.test.ts index c6ac0e4..2ad7aad 100644 --- a/tests/commands.test.ts +++ b/tests/commands.test.ts @@ -22,6 +22,6 @@ describe('#commands', () => { // 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-style, message-stream, toggle-chat, shutoff, modify-capacity') + expect(commandsString).toBe('thread, private-thread, message-style, message-stream, toggle-chat, shutoff, modify-capacity, channel-toggle') }) }) \ No newline at end of file