Compare commits

..

2 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
28 changed files with 168 additions and 65 deletions

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`

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

View File

@@ -7,6 +7,7 @@
<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
@@ -54,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

@@ -7,7 +7,7 @@ services:
build: ./ # find docker file in designated path
container_name: discord
restart: always # rebuild container always
image: kevinthedang/discord-ollama:0.6.0
image: kevinthedang/discord-ollama:0.6.1
environment:
CLIENT_TOKEN: ${CLIENT_TOKEN}
MODEL: ${MODEL}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "discord-ollama",
"version": "0.6.0",
"version": "0.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "discord-ollama",
"version": "0.6.0",
"version": "0.6.1",
"license": "ISC",
"dependencies": {
"discord.js": "^14.15.3",

View File

@@ -1,6 +1,6 @@
{
"name": "discord-ollama",
"version": "0.6.0",
"version": "0.6.1",
"description": "Ollama Integration into discord",
"main": "build/index.js",
"exports": "./build/index.js",

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

View File

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

View File

@@ -1,6 +1,5 @@
import { ChannelType, Client, CommandInteraction, TextChannel } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { clearChannelInfo } from '../utils/index.js'
import { clearChannelInfo, SlashCommand } from '../utils/index.js'
export const ClearUserChannelHistory: SlashCommand = {
name: 'clear-user-channel-history',

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openChannelInfo } from '../utils/index.js'
import { openChannelInfo, SlashCommand } from '../utils/index.js'
export const ThreadCreate: SlashCommand = {
name: 'thread',

View File

@@ -1,6 +1,5 @@
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openChannelInfo } from '../utils/index.js'
import { openChannelInfo, SlashCommand } from '../utils/index.js'
export const PrivateThreadCreate: SlashCommand = {
name: 'private-thread',

View File

@@ -1,7 +1,6 @@
import { embedMessage, event, Events, normalMessage, UserMessage } from '../utils/index.js'
import { getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, openConfig, UserConfig, getAttachmentData } from '../utils/index.js'
import { clean } from '../utils/mentionClean.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).

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

@@ -1,4 +1,4 @@
import { getEnvVar } from './utils/env.js'
import { getEnvVar } from './utils/index.js'
export const Keys = {
clientToken: getEnvVar('CLIENT_TOKEN'),

View File

@@ -1,4 +1,4 @@
import { UserMessage } from './events.js'
import { UserMessage } from './index.js'
export interface UserConfiguration {
'message-stream'?: boolean,

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'

View File

@@ -5,6 +5,7 @@ export * from './messageEmbed.js'
export * from './messageNormal.js'
export * from './commands.js'
export * from './configInterfaces.js'
export * from './mentionClean.js'
// handler imports
export * from './handlers/chatHistoryHandler.js'

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
@@ -24,4 +24,49 @@ describe('#commands', () => {
const commandsString = commands.map(e => e.name).join(', ')
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/*', 'tests/*'], // <-- exclude JS build
reporter: ['text', 'html'] // <-- reports in text, html
reporter: ['text-summary'] // <-- report in text-summary
}
}
})