Compare commits

...

20 Commits

Author SHA1 Message Date
Kevin Dang
947ff89958 Environment Variable Validation (#122)
* Update: env validation and discord token validation

* Add: IPv4 Address Validation

* Update: version increment
2024-10-05 19:29:01 -07:00
Jonathan Smoley
6a9ee2d6d0 Code Coverage and Clean References (#120)
* Add: skeleton suite for command tests (#119)

* test naming updated

* fix imports, remove old references

* added code coverage badge

* Add: coverage environment

* Fix: Readme hyperlink to coverage workflow

* grab coverage pct from env

* Update: gist hyperlink

* color range on coverage

* fix contributing, simplify coverage assessment

* lmiit coverage to master, add branch naming conventions

---------

Co-authored-by: Kevin Dang <77701718+kevinthedang@users.noreply.github.com>
2024-10-01 10:11:23 -07:00
Kevin Dang
e3b0c9abe4 Message Attachment Image Recognition (#118)
* Add: Image recognition

* Fix: Non-Attachment Query

* Update: version increment

* Remove: Debugging logs from buffer file

* Add: comments for bufferHandler
2024-09-18 20:28:23 -07:00
Kevin Dang
36a0cd309b Removed Channel Toggle Command (#115)
* Remove: channel-toggle as command and server config

* Remove: Thread interface

* Fix: Users Thread files will now delete

* Fix: Any user can chat in threads now

* Fix: Thread history files are now deleted with multiple users

* Update: version increment
2024-09-14 13:34:40 -07:00
Kevin Dang
b49b464afb Script Cleaning and Fixes (#114)
* organized existing scripts

* Delete nodemon.json

* exclude test/* from vitest

* remove nodemon from package

* Update: slight adjustments to package.json and readme

* Update: more small changes to readme

* Fix: cleanup scripts and start scripts

---------

Co-authored-by: JT2M0L3Y <jtsmoley@icloud.com>
2024-09-05 19:15:44 -07:00
Kevin Dang
2caf54346a Dependency Upgrade (#111)
* update: dependency upgrade

* update: version increment
2024-08-31 15:20:54 -07:00
Jonathan Smoley
6e6467c2a5 Removed GUILD_ID references (#109) 2024-08-03 12:53:24 -07:00
Kevin Dang
b463b0a8cb Deploy Shield (#107)
* Add: Deploy shield

* Update: job name to release
2024-08-03 10:18:40 -07:00
Kevin Dang
42ef38db14 Data Directory on Chat for Preferences (#105)
* Fix: data directory created on for openConfig

* Update: version increment
2024-08-03 09:50:03 -07:00
Kevin Dang
af23db20bb Removed GUILD_ID as an Environment Variable (#103) 2024-08-03 09:48:47 -07:00
Kevin Dang
117b195095 Fixed Incorrect Image Name (#98) 2024-08-01 17:55:42 -07:00
Kevin Dang
b361636a93 Push Docker Image Pipeline (#97)
* Update: run build after merge

* Add: deploy image pipeline to docker

* Update: release pipeline

* Add: release with latest tag

* Update: docker username as vars because its not really a secret

* Remove: workflow bot token and guild_id

* Add: supplimentary guild_id and token

* Update: version increment
2024-08-01 17:46:43 -07:00
Kevin Dang
4dbd45bccd Run Build Pipelines (#96) 2024-08-01 16:15:50 -07:00
Kevin Dang
02ffb6a196 Remove Unnecessary Docker Test Pipeline (#93)
* Remove: container test pipeline

* Update: build pipelines rely on test pipeline

* Fix: typo in build file

* Fix: naming conventions for workflows in yml
2024-07-31 06:20:00 -07:00
Kevin Dang
060494e883 Adjusted Slash Command Scope (#91)
* Update: Slash Command Scope

* Update: version increment
2024-07-31 06:19:23 -07:00
Kevin Dang
352d88ee9d Clear User Channel History Command (#88)
* Add: Clear user channel message history command

* Update: Checks if messages are empty and has clearer replies

* Fix: Issue where duplication happens on channel-toggle true in threads

* Update: version increment

* Fix: Missing test case for commands.test.ts

* Readability fix

---------

Co-authored-by: Jonathan Smoley <67881240+JT2M0L3Y@users.noreply.github.com>
2024-07-25 14:26:50 -07:00
Kevin Dang
e60c2f88b8 Handlers Directory and Universal Import Fix (#86)
* Update: split jsonHandler.ts to different files

* Add: handlers folder and moved some files there

* Update: interface file name
2024-07-23 16:59:54 -07:00
Kevin Dang
b498276978 Dependency Upgrade (#85)
* Update: dependencies upgrade

* Fix: Run tests at root scope
2024-07-23 15:41:16 -07:00
Kevin Dang
ae9cac65a9 PR Template Update (#84)
* Update: version increment and reminder on template

* Update: comment on incrementing as necessary
2024-07-11 17:08:34 -07:00
Kevin Dang
61d3dc4312 User Preferences Fix (#83)
* Fix: incorrect user preferences saving
2024-07-10 20:41:23 -07:00
44 changed files with 1898 additions and 1397 deletions

View File

@@ -1,9 +1,6 @@
# Discord token for the bot
CLIENT_TOKEN = BOT_TOKEN
# id token of a discord server
GUILD_ID = GUILD_ID
# model for the bot to query from (i.e. llama2 [llama2:13b], mistral, codellama, etc... )
MODEL = MODEL_NAME

View File

@@ -1,31 +1,38 @@
<!--
Author: Kevin Dang
Date: 1-30-2024
Author: Kevin Dang
Date: 1-30-2024
Changes:
10-01-2024 - Jonathan Smoley
-->
## Naming Conventions
* Branches
* prefix your branch name with the type of contribution:
* features: `'feature/**'`
* releases: `'releases/**'`
* bugs: `'bug/**'`
## Run the Bot
* Refer to all sections below before running the bot.
* You should now have `Ollama`, `NodeJS`, ran `npm install`.
* You will also need a discord bot to run. Refer to the [developer portal](https://discord.com/developers/) to learn how to set one up and invite it to your server. If that does not help then look up a YouTube video like this [one](https://www.youtube.com/watch?v=KZ3tIGHU314&ab_channel=UnderCtrl).
* Now run `npm run start` to run the client and ollama at the same time (this must be one in wsl or a Linux distro)
* Now run `npm run client` to run the client (this must be done in wsl or a Linux distro)
## Set up (Development-side)
* Pull the repository using `https://github.com/kevinthedang/discord-ollama.git`.
* Refer to `Ollama Setup` in the readme to set up Ollama.
* Refer to `Environment Setup` in the readme to set up Ollama.
* This must be set up in a Linux environment or wsl2.
* Install NodeJS `v18.18.2`
* You can check out `Resources` and `To Run` in the readme for a bit of help.
* You can check out `Resources` in the readme for a bit of help.
* You can also reference [NodeJS Setup](#nodejs-setup)
* When you have the project pulled from github, open up a terminal and run `npm i` or `npm install` to get all of the packages for the project.
* In some kind of terminal (`git bash` is good) to run the client. You can run Ollama but opening up wsl2 and typing `ollama serve`.
* Refer to `Ollama Setup` if there are any issues.
## Environment
* You will need two environment files:
* You will need an environment file:
* `.env`: for running the bot
* Please refer to `.env.sample` for all environment variables to include
* `.env.dev.local`: also runs the bot, but with development variables
* Currently there are no differences between the two, but when needed, you may add environment variables as needed.
## NodeJS Setup
* Install [nvm](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating) using `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash`

View File

@@ -14,4 +14,6 @@
## After the Pull Request is Opened
* One the Pull Request has been created, please add any Issue(s) that are being addressed by this change (if any).
* If the reviewer(s) mention any changes or open threads for questions, please resolve those as soon as you can.
* If the reviewer(s) mention any changes or open threads for questions, please resolve those as soon as you can.
# Ensure you version increment as necessary!!!

View File

@@ -1,20 +1,9 @@
name: Builds
run-name: Validate Node and Docker Builds
on:
pull_request:
push:
branches:
- master
paths:
- '/'
- 'src/**'
- 'tests/**'
- '!docs/**'
- '!imgs/**'
- '!.github/**'
- '.github/workflows/**'
- '!.gitignore'
- '!LICENSE'
- '!README'
- master # runs after Pull Request is merged
jobs:
Discord-Node-Build: # test if the node install and run
@@ -42,7 +31,6 @@ jobs:
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
@@ -71,7 +59,6 @@ jobs:
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
@@ -83,7 +70,7 @@ jobs:
- name: Check Images Exist
run: |
(docker images | grep -q 'discord/bot' && docker images | grep -qE 'ollama/ollama') || exit 1
(docker images | grep -q 'kevinthedang/discord-ollama' && docker images | grep -qE 'ollama/ollama') || exit 1
- name: Check Containers Exist
run: |

51
.github/workflows/coverage.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Coverage
run-name: Code Coverage
on:
push:
branches:
- master
jobs:
Discord-Node-Coverage:
runs-on: ubuntu-latest
environment: coverage
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment lts/hydrogen
uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
cache: "npm"
- name: Install Project Dependencies
run: |
npm install
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Collect Code Coverage
run: |
LINE_PCT=$(npm run test:coverage | tail -2 | head -1 | awk '{print $3}')
echo "COVERAGE=$LINE_PCT" >> $GITHUB_ENV
- name: Upload Code Coverage
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ secrets.GIST_SECRET }}
gistID: ${{ vars.GIST_ID }}
filename: coverage.json
label: Coverage
message: ${{ env.COVERAGE }}
valColorRange: ${{ env.COVERAGE }}
maxColorRange: 100
minColorRange: 0

50
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Deploy
run-name: Release Docker Image
on:
push:
tags:
- 'v*'
jobs:
Release-Docker-Image:
runs-on: ubuntu-latest
environment: release
timeout-minutes: 3
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment lts/hydrogen
uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
cache: "npm"
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = NOT_REAL_TOKEN >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Get Version from package.json
run: echo "VERSION=$(jq -r '.version' package.json)" >> $GITHUB_ENV
- name: Build Image
run: |
npm run docker:build
- name: Build Image as Latest
run: |
npm run docker:build-latest
- name: Log into Docker
run: |
docker login --username ${{ vars.DOCKER_USER }} --password ${{ secrets.DOCKER_PASS }}
- name: Release Docker Image
run: |
docker push ${{ vars.DOCKER_USER }}/discord-ollama:${{ env.VERSION }}
docker push ${{ vars.DOCKER_USER }}/discord-ollama:latest

View File

@@ -5,7 +5,8 @@ on:
branches:
- master
paths:
- '/'
- '*'
- 'package*.json'
- 'src/**'
- 'tests/**'
- '!docs/**'
@@ -38,7 +39,6 @@ jobs:
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
@@ -47,34 +47,3 @@ jobs:
- name: Test Application
run: |
npm run test:run
Discord-Ollama-Container-Test:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment lts/hydrogen
uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
cache: "npm"
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Setup Docker Network and Images
run: |
npm run docker:start-cpu
- name: Test Docker Container
run: |
npm run docker:test

View File

@@ -5,14 +5,16 @@
<p><a href="#"></a><a href="https://creativecommons.org/licenses/by/4.0/"><img alt="License" src="https://img.shields.io/badge/License-CC_BY_4.0-darkgreen.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/kevinthedang/discord-ollama?logo=github" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml"><img alt="Build Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/release.yml"><img alt="Release Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/release.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml"><img alt="Testing Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/coverage.yml"><img alt="Code Coverage" src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kevinthedang/bc7b5dcfa16561ab02bb3df67a99b22d/raw/coverage.json"></a>
</div>
## About/Goals
Ollama is an AI model management tool that allows users to install and use custom large language models locally.
The project aims to:
* [x] Create a Discord bot that will utilize Ollama and chat to chat with users!
* [ ] User Preferences on Chat
* [x] User Preferences on Chat
* [x] Message Persistance on Channels and Threads
* [x] Threads
* [x] Channels
@@ -20,10 +22,10 @@ The project aims to:
* [x] Slash Commands Compatible
* [x] Generated Token Length Handling for >2000
* [x] Token Length Handling of any message size
* [ ] User vs. Server Preferences
* [x] User vs. Server Preferences
* [ ] Redis Caching
* [x] Administrator Role Compatible
* [ ] Multi-User Chat Generation (Multiple users chatting at the same time)
* [x] Multi-User Chat Generation (Multiple users chatting at the same time) - This was built into from Ollama `v0.2.1+`
* [ ] Automatic and Manual model pulling through the Discord client
* [ ] Allow others to create their own models personalized for their own servers!
* [ ] Documentation on creating your own LLM
@@ -34,25 +36,17 @@ The project aims to:
* You will need a `.env` file in the root of the project directory with the bot's token. There is a `.env.sample` is provided for you as a reference for what environment variables.
* For example, `CLIENT_TOKEN = [Bot Token]`
* Please refer to the docs for bot setup.
* [Creating a Discord App](./docs/setup-discord-app.md)
* [Local Machine Setup](./docs/setup-local.md)
* [Docker Setup for Servers and Local Machines](./docs/setup-docker.md)
* Nvidia is recommended for now, but support for other GPUs should be development.
* Local use is not recommended.
* [Creating a Discord App](./docs/setup-discord-app.md)
## Resources
* [NodeJS](https://nodejs.org/en)
* This project runs on `lts\hydrogen`.
* To run dev in `ts-node`/`nodemon`, using `v18.18.2` is recommended.
* To run dev with `tsx`, you can use `v20.10.0` or earlier.
* This project runs on `lts\hydrogen`.
* This project supports any NodeJS version above `16.x.x` to only allow ESModules.
* [Ollama](https://ollama.com/)
* [Ollama Docker Image](https://hub.docker.com/r/ollama/ollama)
> [!CAUTION]
> `v18.X.X` or `lts/hydrogen` will not run properly for `npm run dev-mon`. It is recommended to just use `npm run dev-tsx` for development. The nodemon version will likely be removed in a future update.
* [Discord Developer Portal](https://discord.com/developers/docs/intro)
* [Discord.js Docs](https://discord.js.org/docs/packages/discord.js/main)
* [Setting up Docker (Ubuntu 20.04)](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04)
* [Setting up Nvidia Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
@@ -61,4 +55,4 @@ The project aims to:
* [Kevin Dang](https://github.com/kevinthedang)
* [Jonathan Smoley](https://github.com/JT2M0L3Y)
[discord-ollama](https://github.com/kevinthedang/discord-ollama) © 2023 by [Kevin Dang](https://github.com/kevinthedang) is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
[discord-ollama](https://github.com/kevinthedang/discord-ollama) © 2023 by [Kevin Dang](https://github.com/kevinthedang) is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)

View File

@@ -1,5 +1,4 @@
# creates the docker compose
version: '3.7'
# creates the docker compose
# build individual services
services:
@@ -8,10 +7,9 @@ services:
build: ./ # find docker file in designated path
container_name: discord
restart: always # rebuild container always
image: discord/bot:0.5.4
image: kevinthedang/discord-ollama:0.6.1
environment:
CLIENT_TOKEN: ${CLIENT_TOKEN}
GUILD_ID: ${GUILD_ID}
MODEL: ${MODEL}
CLIENT_UID: ${CLIENT_UID}
OLLAMA_IP: ${OLLAMA_IP}

View File

@@ -56,7 +56,7 @@ sudo systemctl restart docker
* `docker compose rm`
* `docker ps` to check if containers have been removed.
* This may not work if the nvidia installation was done incorrectly. If this is the case, please utilize the [Manual "Clean-up"](#manual-run-with-docker) shown below.
* You can also use `npm run clean` to clean up the containers and remove the network to address a possible `Address already in use` problem.
* You can also use `npm run clean` to clean up the containers and remove the network to address a possible `Address already in use` problem. This script does not have to end without error to work.
## Manual Run (with Docker)
* Run the following commands:

View File

@@ -14,7 +14,7 @@
## To Run Locally (without Docker)
* Run `npm install` to install the npm packages.
* Ensure that your [.env](../.env.sample) file's `OLLAMA_IP` is `127.0.0.1` to work properly.
* You only need your `CLIENT_TOKEN`, `GUILD_ID`, `MODEL`, `CLIENT_UID`, `OLLAMA_IP`, `OLLAMA_PORT`.
* You only need your `CLIENT_TOKEN`, `MODEL`, `CLIENT_UID`, `OLLAMA_IP`, `OLLAMA_PORT`.
* The ollama ip and port should just use it's defaults by nature. If not, utilize `OLLAMA_IP = 127.0.0.1` and `OLLAMA_PORT = 11434`.
* Now, you can run the bot by running `npm run client` which will build and run the decompiled typescript and run the setup for ollama.
* **IMPORTANT**: This must be ran in the wsl/Linux instance to work properly! Using Command Prompt/Powershell/Git Bash/etc. will not work on Windows (at least in my experience).

View File

@@ -1,12 +0,0 @@
{
"restartable": "rs",
"ignore": ["node_modules/"],
"watch": ["src/"],
"execMap": {
"ts": "ts-node --esm"
},
"env": {
"NODE_ENV": "development"
},
"ext": "js,json,ts"
}

2061
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,42 @@
{
"name": "discord-ollama",
"version": "0.5.4",
"version": "0.6.1",
"description": "Ollama Integration into discord",
"main": "build/index.js",
"exports": "./build/index.js",
"scripts": {
"dev-tsx": "tsx watch src/index.ts",
"dev-mon": "nodemon --config nodemon.json src/index.ts",
"build": "tsc",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"watch": "tsx watch src",
"build": "tsc",
"prod": "node .",
"client": "npm run build && npm run prod",
"clean": "docker compose down && docker rmi $(docker images | grep $(node -p \"require('./package.json').version\") | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"clean": "docker compose down && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"start": "docker compose build --no-cache && docker compose up -d",
"docker:start": "npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama",
"docker:start-cpu": "npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama-cpu",
"docker:clean": "docker rm -f discord && docker rm -f ollama && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"docker:clean": "docker rm -f discord && docker rm -f ollama && docker network prune -f && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"docker:network": "docker network create --subnet=172.18.0.0/16 ollama-net",
"docker:build": "docker build --no-cache -t discord/bot:$(node -p \"require('./package.json').version\") .",
"docker:test": "docker run -d --rm -v discord:/src/app --name test discord/bot:$(node -p \"require('./package.json').version\") npm run test:run",
"docker:client": "docker run -d -v discord:/src/app --name discord --network ollama-net --ip 172.18.0.3 discord/bot:$(node -p \"require('./package.json').version\")",
"docker:build": "docker build --no-cache -t kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\") .",
"docker:build-latest": "docker build --no-cache -t kevinthedang/discord-ollama:latest .",
"docker:client": "docker run -d -v discord:/src/app --name discord --network ollama-net --ip 172.18.0.3 kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\")",
"docker:ollama": "docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest",
"docker:ollama-cpu": "docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest"
"docker:ollama-cpu": "docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest",
"docker:start": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama",
"docker:start-cpu": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama-cpu"
},
"author": "Kevin Dang",
"license": "ISC",
"dependencies": {
"discord.js": "^14.15.3",
"dotenv": "^16.4.5",
"ollama": "^0.5.2"
"ollama": "^0.5.8"
},
"devDependencies": {
"@types/node": "^20.14.2",
"@vitest/coverage-v8": "^1.6.0",
"nodemon": "^3.1.3",
"@types/node": "^22.5.1",
"@vitest/coverage-v8": "^2.0.5",
"ts-node": "^10.9.2",
"tsx": "^4.15.5",
"typescript": "^5.4.5",
"vitest": "^1.6.0"
"tsx": "^4.19.0",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
},
"type": "module",
"engines": {

View File

@@ -1,10 +1,8 @@
import { Client, GatewayIntentBits } from 'discord.js'
import { UserMessage, registerEvents } from './utils/events.js'
import Events from './events/index.js'
import { Ollama } from 'ollama'
import { Queue } from './queues/queue.js'
// Import keys/tokens
import { UserMessage, registerEvents } from './utils/index.js'
import Events from './events/index.js'
import Keys from './keys.js'
@@ -26,13 +24,7 @@ const ollama = new Ollama({
// Create Queue managed by Events
const messageHistory: Queue<UserMessage> = new Queue<UserMessage>
/**
* register events for bot to listen to in discord
* @param messageHistory message history for the llm
* @param Events events to register
* @param client the bot reference
* @param Keys tokens from .env files
*/
// register all events
registerEvents(client, Events, messageHistory, Keys, ollama)
// Try to log in the client
@@ -45,5 +37,6 @@ await client.login(Keys.clientToken)
// queue up bots name
messageHistory.enqueue({
role: 'assistant',
content: `My name is ${client.user?.username}`
content: `My name is ${client.user?.username}`,
images: []
})

View File

@@ -1,6 +1,5 @@
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openConfig } from '../utils/jsonHandler.js'
import { openConfig, SlashCommand } from '../utils/index.js'
export const Capacity: SlashCommand = {
name: 'modify-capacity',
@@ -20,10 +19,10 @@ 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 && ChannelType.GuildText)) return
if (!channel || channel.type !== (ChannelType.PrivateThread && ChannelType.PublicThread && ChannelType.GuildText)) return
// set state of bot chat features
openConfig(`${interaction.client.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: `Message History Capacity has been set to \`${interaction.options.get('context-capacity')?.value}\``,

View File

@@ -1,34 +0,0 @@
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(`${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}\``,
ephemeral: true
})
}
}

View File

@@ -0,0 +1,33 @@
import { ChannelType, Client, CommandInteraction, TextChannel } from 'discord.js'
import { clearChannelInfo, SlashCommand } from '../utils/index.js'
export const ClearUserChannelHistory: SlashCommand = {
name: 'clear-user-channel-history',
description: 'clears history for user running this command in current channel',
// Clear channel history for intended user
run: async (client: Client, interaction: CommandInteraction) => {
// fetch current channel
const channel = await client.channels.fetch(interaction.channelId)
// if not an existing channel or a GuildText, fail command
if (!channel || channel.type !== ChannelType.GuildText) return
// clear channel info for user
const successfulWipe = await clearChannelInfo(interaction.channelId,
interaction.channel as TextChannel,
interaction.user.username)
// check result of clearing history
if (successfulWipe)
interaction.reply({
content: `Channel history in **${channel.name}** cleared for **${interaction.user.username}**.`,
ephemeral: true
})
else
interaction.reply({
content: `Channel history could not be found for **${interaction.user.username}** in **${channel.name}**.\n\nPlease chat with **${client.user?.username}** to start a chat history.`,
ephemeral: true
})
}
}

View File

@@ -1,6 +1,5 @@
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openConfig } from '../utils/jsonHandler.js'
import { openConfig, SlashCommand } from '../utils/index.js'
export const Disable: SlashCommand = {
name: 'toggle-chat',

View File

@@ -6,7 +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'
import { ClearUserChannelHistory } from './cleanUserChannelHistory.js'
export default [
ThreadCreate,
@@ -16,5 +16,5 @@ export default [
Disable,
Shutoff,
Capacity,
ChannelToggle
ClearUserChannelHistory
] as SlashCommand[]

View File

@@ -1,6 +1,5 @@
import { ApplicationCommandOptionType, ChannelType, Client, CommandInteraction } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openConfig } from '../utils/jsonHandler.js'
import { openConfig, SlashCommand } from '../utils/index.js'
export const MessageStream: SlashCommand = {
name: 'message-stream',
@@ -20,10 +19,10 @@ 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 && ChannelType.GuildText)) return
if (!channel || channel.type !== (ChannelType.PrivateThread && ChannelType.PublicThread && ChannelType.GuildText)) return
// save value to json and write to it
openConfig(`${interaction.client.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 preferences set to: \`${interaction.options.get('stream')?.value}\``,

View File

@@ -1,6 +1,5 @@
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openConfig } from '../utils/jsonHandler.js'
import { openConfig, SlashCommand } from '../utils/index.js'
export const MessageStyle: SlashCommand = {
name: 'message-style',
@@ -20,10 +19,10 @@ 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 && ChannelType.GuildText)) return
if (!channel || channel.type !== (ChannelType.PrivateThread && ChannelType.PublicThread && ChannelType.GuildText)) return
// set the message style
openConfig(`${interaction.client.user.username}-config.json`, interaction.commandName, interaction.options.get('embed')?.value)
openConfig(`${interaction.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}\``,

View File

@@ -1,6 +1,5 @@
import { ChannelType, Client, CommandInteraction, TextChannel } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openThreadInfo } from '../utils/jsonHandler.js'
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js'
import { openChannelInfo, SlashCommand } from '../utils/index.js'
export const ThreadCreate: SlashCommand = {
name: 'thread',
@@ -22,8 +21,9 @@ 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 prompt.`)
// handle storing this chat channel
// store: thread.id, thread.name
openThreadInfo(`${thread.id}.json`, thread)
openChannelInfo(thread.id,
thread as ThreadChannel,
interaction.user.tag)
// user only reply
return interaction.reply({

View File

@@ -1,6 +1,5 @@
import { ChannelType, Client, CommandInteraction, TextChannel } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openThreadInfo } from '../utils/jsonHandler.js'
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js'
import { openChannelInfo, SlashCommand } from '../utils/index.js'
export const PrivateThreadCreate: SlashCommand = {
name: 'private-thread',
@@ -23,7 +22,10 @@ export const PrivateThreadCreate: SlashCommand = {
// handle storing this chat channel
// store: thread.id, thread.name
openThreadInfo(`${thread.id}.json`, thread)
openChannelInfo(thread.id,
thread as ThreadChannel,
interaction.user.tag
)
// user only reply
return interaction.reply({

View File

@@ -1,7 +1,6 @@
import { embedMessage, event, Events, normalMessage, UserMessage } from '../utils/index.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'
import { TextChannel } from 'discord.js'
import { embedMessage, event, Events, normalMessage, UserMessage, clean } from '../utils/index.js'
import { getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, openConfig, UserConfig, getAttachmentData } from '../utils/index.js'
/**
* Max Message length for free users is 2000 characters (bot or not).
@@ -9,7 +8,7 @@ import { TextChannel, ThreadChannel } from 'discord.js'
*
* @param message the message received from the channel
*/
export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama, client }, message) => {
export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama }, message) => {
log(`Message \"${clean(message.content)}\" from ${message.author.tag} in channel/thread ${message.channelId}.`)
// Do not respond if bot talks in the chat
@@ -23,15 +22,12 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
try {
// Retrieve Server/Guild Preferences
const serverConfig: ServerConfig = await new Promise((resolve, reject) => {
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)
// 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
}
@@ -42,14 +38,6 @@ 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.username
)
}
resolve(config)
})
})
@@ -80,31 +68,42 @@ 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) => {
let chatMessages: UserMessage[] = await new Promise((resolve) => {
// set new queue to modify
if (serverConfig.options['channel-toggle']) {
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([])
}
})
})
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 ${message.channel}-${message.author.username} does not exist.`)
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.`))
}
})
} 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
// get message attachment if exists
const messageAttachment: string[] = await getAttachmentData(message.attachments.first())
// set up new queue
msgHist.setQueue(chatMessages)
@@ -114,7 +113,8 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
// push user response before ollama query
msgHist.enqueue({
role: 'user',
content: clean(message.content)
content: clean(message.content),
images: messageAttachment || []
})
// undefined or false, use normal, otherwise use embed
@@ -132,22 +132,16 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
// successful query, save it in context history
msgHist.enqueue({
role: 'assistant',
content: response
content: response,
images: messageAttachment || []
})
// only update the json on success
if (serverConfig.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()
)
}
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}*`)

View File

@@ -1,5 +1,5 @@
import { event, Events, registerCommands } from '../utils/index.js'
import { ActivityType } from 'discord.js'
import { event, Events, registerCommands } from '../utils/index.js'
import commands from '../commands/index.js'
// Log when the bot successfully logs in and export it

View File

@@ -5,16 +5,36 @@ import fs from 'fs'
/**
* Event to remove the associated .json file for a thread once deleted
*/
export default event(Events.ThreadDelete, ({ log }, thread: ThreadChannel) => {
const filePath = `data/${thread.id}.json`
if (fs.existsSync(filePath)) {
fs.unlink(filePath, (error) => {
if (error)
log(`Error deleting file ${filePath}`, error)
else
log(`Successfully deleted ${filePath} thread info`)
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/'
// 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}-`) &&
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`)
})
})
})
} else {
log(`File ${filePath} does not exist.`)
}
} catch (error) {
log(`Issue deleting user history files from ${thread.id}`)
}
})

View File

@@ -1,10 +1,9 @@
import { getEnvVar } from './utils/env.js'
import { getEnvVar } from './utils/index.js'
export const Keys = {
clientToken: getEnvVar('CLIENT_TOKEN'),
model: getEnvVar('MODEL'),
clientUid: getEnvVar('CLIENT_UID'),
guildId: getEnvVar('GUILD_ID'),
ipAddress: getEnvVar('OLLAMA_IP'),
portAddress: getEnvVar('OLLAMA_PORT'),
} as const // readonly keys

View File

@@ -0,0 +1,51 @@
import { UserMessage } from './index.js'
export interface UserConfiguration {
'message-stream'?: boolean,
'message-style'?: boolean,
'modify-capacity': number
}
export interface ServerConfiguration {
'toggle-chat'?: 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: 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 Channel {
readonly id: string
readonly name: string
readonly user: string
messages: UserMessage[]
}
/**
* 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);
}

View File

@@ -1,21 +1,35 @@
import { resolve } from 'path'
import { config } from 'dotenv'
// Find config - ONLY WORKS WITH NODEMON
const envFile = process.env.NODE_ENV === 'development' ? '.env.dev.local' : '.env'
// resolve config file
const envFilePath = resolve(process.cwd(), envFile)
const envFilePath = resolve(process.cwd(), '.env')
const ipValidate: RegExp = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/
// set current environment variable file
config({ path: envFilePath })
// Getter for environment variables
/**
* Method to validate if environment variables found in file utils/env.ts
*
* @param name Name of the environment variable in .env
* @param fallback fallback value to set if environment variable is not set (used manually in src/keys.ts)
* @returns environment variable value
*/
export function getEnvVar(name: string, fallback?: string): string {
const value = process.env[name] ?? fallback
if (value == undefined)
if (!value)
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.
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.`)
// return env variable
return value
}

View File

@@ -1,4 +1,4 @@
import type { ClientEvents, Awaitable, Client, User } from 'discord.js'
import type { ClientEvents, Awaitable, Client } from 'discord.js'
import { Ollama } from 'ollama'
import { Queue } from '../queues/queue.js'
@@ -38,7 +38,8 @@ export type ChatParams = {
*/
export type UserMessage = {
role: string,
content: string
content: string,
images: string[] // May or may not have images in message
}
// Event properties

View File

@@ -0,0 +1,57 @@
import { Attachment } from "discord.js"
/**
* Method to convert a Discord attachment url to an array buffer
*
* @param url Discord Attachment Url
* @returns array buffer from Attachment Url
*/
async function getAttachmentBuffer(url: string): Promise<ArrayBuffer> {
// Get the data from the image
const response = await fetch(url)
// Validate the image came in fine
if (!response.ok)
throw new Error('Failed to fetch the attachment.')
// Return image as Buffer
return await response.arrayBuffer()
}
/**
* Method to convert an array buffer to a Base64 String
*
* @param buffer Array Buffer from attachment
* @returns converted Base64 string
*/
function arrayBufferToBase64(buffer: ArrayBuffer): string {
// Converting to Uint8Array
const uint8Array = new Uint8Array(buffer)
let binary = ''
const len = uint8Array.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(uint8Array[i])
}
// Return as Base64
return Buffer.from(binary, 'binary').toString('base64')
}
/**
* Method to retrieve the Base64 Array of provided Message Attachment
*
* @param attachment Message Attachment from Discord
* @returns Base64 string array
*/
export async function getAttachmentData(attachment: Attachment | undefined): Promise<string[]> {
const url: string = attachment !== undefined ? attachment.url : "Missing Url"
// case of no attachment
if (url === "Missing Url")
return []
// Convert data to base64
const buffer = await getAttachmentBuffer(url)
const base64String = arrayBufferToBase64(buffer)
return [base64String]
}

View File

@@ -0,0 +1,115 @@
import { TextChannel, ThreadChannel } from 'discord.js'
import { Configuration, Channel, UserMessage } from '../index.js'
import fs from 'fs'
import path from 'path'
/**
* Method to check if a thread history file exists
*
* @param channel parent thread of the requested thread (can be GuildText)
* @returns true if channel does not exist, false otherwise
*/
async function checkChannelInfoExists(channel: TextChannel, user: string) {
const doesExists: boolean = await new Promise((resolve) => {
getChannelInfo(`${channel.id}-${user}.json`, (channelInfo) => {
if (channelInfo?.messages)
resolve(true)
else
resolve(false)
})
})
return doesExists
}
/**
* Method to clear channel history for requesting user
*
* @param filename guild id string
* @param channel the TextChannel in the Guild
* @param user username of user
* @returns nothing
*/
export async function clearChannelInfo(filename: string, channel: TextChannel, user: string): Promise<boolean> {
const channelInfoExists: boolean = await checkChannelInfoExists(channel, user)
// If thread does not exist, file can't be found
if (!channelInfoExists) return false
// Attempt to clear user channel history
const fullFileName = `data/${filename}-${user}.json`
const cleanedHistory: boolean = await new Promise((resolve) => {
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) // already empty, let user know
resolve(false)
else {
object['messages'] = [] // cleared history
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
resolve(true)
}
}
})
})
console.log(cleanedHistory)
return cleanedHistory
}
/**
* 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 | ThreadChannel, user: string, messages: UserMessage[] = []): Promise<void> {
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<void> {
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
}
}

View File

@@ -0,0 +1,92 @@
import { Configuration, ServerConfig, UserConfig, isServerConfigurationKey } from '../index.js'
import fs from 'fs'
import path from 'path'
/**
* Method to open a file in the working directory and modify/create it
*
* @param filename name of the file
* @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(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(fullFileName, JSON.stringify(object, null, 2))
}
})
} else { // work on dynamic file creation
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
}
const directory = path.dirname(fullFileName)
if (!fs.existsSync(directory))
fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(`data/${filename}`, JSON.stringify(object, null, 2))
console.log(`[Util: openConfig] Created '${filename}' in working directory`)
}
}
/**
* 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 getServerConfig(filename: string, callback: (config: ServerConfig | undefined) => void): Promise<void> {
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
}
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<void> {
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
}
callback(JSON.parse(data))
})
} else {
callback(undefined) // file not found
}
}

View File

@@ -1,5 +1,5 @@
import { ChatResponse } from "ollama"
import { ChatParams } from "./index.js"
import { ChatParams } from "../index.js"
import { AbortableAsyncIterator } from "ollama/src/utils.js"
/**

View File

@@ -4,4 +4,11 @@ export * from './events.js'
export * from './messageEmbed.js'
export * from './messageNormal.js'
export * from './commands.js'
export * from './streamHandler.js'
export * from './configInterfaces.js'
export * from './mentionClean.js'
// handler imports
export * from './handlers/chatHistoryHandler.js'
export * from './handlers/configHandler.js'
export * from './handlers/streamHandler.js'
export * from './handlers/bufferHandler.js'

View File

@@ -1,270 +0,0 @@
import { TextChannel, ThreadChannel } from 'discord.js'
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: 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 {
readonly id: string
readonly name: string
messages: UserMessage[]
}
export interface Channel {
readonly id: string
readonly name: string
readonly user: string
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
*
* @param filename name of the file
* @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(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(fullFileName, JSON.stringify(object, null, 2))
}
})
} else { // work on dynamic file creation
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(`data/${filename}`, JSON.stringify(object, null, 2))
console.log(`[Util: openConfig] Created '${filename}' in working directory`)
}
}
/**
* 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 getServerConfig(filename: string, callback: (config: ServerConfig | undefined) => void): Promise<void> {
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
}
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<void> {
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
}
callback(JSON.parse(data))
})
} else {
callback(undefined) // file not found
}
}
/**
* Method to open/create and modify a json file containing thread information
*
* @param filename name of the thread file
* @param thread the thread with all of the interactions
* @param message message contents and from who
*/
export function openThreadInfo(filename: string, thread: ThreadChannel, messages: UserMessage[] = []) {
// check if the file exists, if not then make the config file
const fullFileName = `data/${filename}`
if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => {
if (error)
console.log(`[Error: openThreadInfo] Incorrect file format`)
else {
const object = JSON.parse(data)
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\": \"${thread?.id}\", \"name\": \"${thread?.name}\", \"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: openThreadInfo] Created '${fullFileName}' in working directory`)
}
}
/**
* 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 getThread(filename: string, callback: (config: Thread | undefined) => void): Promise<void> {
// attempt to read the file and get the configuration
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
}
}
/**
* 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<void> {
// 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<void> {
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
}
}

View File

@@ -8,7 +8,7 @@ import Keys from "../keys.js"
* - replace function works well for this
*
* @param message
* @returns
* @returns message without client id
*/
export function clean(message: string): string {
const cleanedMessage: string = message.replace(`<@${Keys.clientUid}>`, '').trim()

View File

@@ -2,7 +2,7 @@
// expect takes a value from an expression
// it marks a test case
import { describe, expect, it } from 'vitest'
import commands from '../src/commands'
import commands from '../src/commands/index.js'
/**
* Commands test suite, tests the commands object
@@ -12,7 +12,7 @@ import commands from '../src/commands'
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#commands', () => {
describe('Commands Existence', () => {
// test definition of commands object
it('references defined object', () => {
// toBe compares the value to the expected value
@@ -22,6 +22,51 @@ 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, channel-toggle')
expect(commandsString).toBe('thread, private-thread, message-style, message-stream, toggle-chat, shutoff, modify-capacity, clear-user-channel-history')
})
})
/**
* User Commands Test suite for testing out commands
* that would be run by users when using the application.
*/
describe('User Command Tests', () => {
// test capacity command
it('run modify-capacity command', () => {
})
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', () => {
})
})
/**
* Admin Commands Test suite for running administrative
* commands with the application.
*/
describe('Admin Command Tests', () => {
it('run shutoff command', () => {
})
it('run toggle-chat command', () => {
})
})

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import events from '../src/events'
import events from '../src/events/index.js'
/**
* Events test suite, tests the events object
@@ -9,7 +9,7 @@ import events from '../src/events'
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#events', () => {
describe('Events Existence', () => {
// test definition of events object
it('references defined object', () => {
expect(typeof events).toBe('object')

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { getEnvVar } from '../src/utils'
import { getEnvVar } from '../src/utils/index.js'
/**
* getEnvVar test suite, tests the getEnvVar function
@@ -7,7 +7,7 @@ import { getEnvVar } from '../src/utils'
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#getEnvVar', () => {
describe('Environment Setup', () => {
// dummy set of keys
const keys = {
clientToken: 'CLIENT_TOKEN',

View File

@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest'
import { clean } from '../src/utils/mentionClean'
import { getEnvVar } from '../src/utils'
import { getEnvVar, clean } from '../src/utils/index.js'
/**
* MentionClean test suite, tests the clean function
@@ -8,7 +7,7 @@ import { getEnvVar } from '../src/utils'
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#clean', () => {
describe('Mentions Cleaned', () => {
// test for id removal from message
it('removes the mention from a message', () => {
const message = `<@${getEnvVar('CLIENT_UID')}> Hello, World!`

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Queue } from '../src/queues/queue'
import { Queue } from '../src/queues/queue.js'
/**
* Queue test suite, tests the Queue class
@@ -7,7 +7,7 @@ import { Queue } from '../src/queues/queue'
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#queue', () => {
describe('Queue Structure', () => {
let queue= new Queue<string>()
// test for queue creation

View File

@@ -4,9 +4,10 @@ import { defineConfig, configDefaults } from 'vitest/config'
export default defineConfig({
test: {
globals: true, // <-- reduces test file imports
reporters: ['verbose'], // <-- verbose output
coverage: {
exclude: [...configDefaults.exclude, 'build/*'], // <-- exclude JS build
reporter: ['text', 'html'] // <-- reports in text, html
exclude: [...configDefaults.exclude, 'build/*', 'tests/*'], // <-- exclude JS build
reporter: ['text-summary'] // <-- report in text-summary
}
}
})