mahiatlinux commited on
Commit
4e3fbe0
1 Parent(s): 1914d1a

Upload 13 files

Browse files
Files changed (13) hide show
  1. .dockerignore +5 -0
  2. .env +57 -0
  3. .eslintrc.json +53 -0
  4. .gitignore +5 -0
  5. Dockerfile +7 -0
  6. Makefile +27 -0
  7. README.md +43 -9
  8. assets/screenshot.png +0 -0
  9. docker-compose.yml +9 -0
  10. package-lock.json +378 -0
  11. package.json +14 -0
  12. src/bot.js +434 -0
  13. src/index.js +30 -0
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ package-lock.json
2
+ pnpm-lock.yaml
3
+ pnpm-lock.yml
4
+ node_modules/
5
+ .env
.env ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Discord bot token
2
+ TOKEN=MTIzNTE0MjgyODE5MTkxMTk2Ng.GTigmW.AkCl8B8TWqsrbRrgvuu1QWO7vgr-GLWN51qMW8
3
+
4
+ # What language model to use, orca is one of the lower-end models that doesn't require as much computer power as llama2
5
+ MODEL=tinydolphin
6
+
7
+ # Ollama URL (if you want to use multiple, separate them by commas)
8
+ OLLAMA=https://mahiatlinux-ollama-server.hf.space
9
+
10
+ # What Discord channels to enable it in (by ID)
11
+ CHANNELS=1235145472470159391
12
+
13
+ # System message that the language model can understand
14
+ # Feel free to change this
15
+ #SYSTEM="The current date and time is <date>.
16
+
17
+ Basic markdown is supported.
18
+ Bold: **bold text here**
19
+ Italics: _italic text here_
20
+ Underlined: __underlined text here__
21
+ Strikethrough: ~~strikethrough text here~~
22
+ Spoiler: ||spoiler text here||
23
+ Block quotes: Start the line with a > followed by a space, e.g
24
+ > Hello there
25
+
26
+ Inline code blocks are supported by surrounding text in backticks, e.g `print('Hello');`, block code is supported by surrounding text in three backticks, e.g ```print('Hello');```.
27
+ Surround code that is produced in code blocks. Use a code block with three backticks if the code has multiple lines, otherwise use an inline code block with one backtick.
28
+
29
+ Links are supported by wrapping the text in square brackets and the link in parenthesis, e.g [Example](https://example.com)
30
+
31
+ Lists are supported by starting the line with a dash followed by a space, e.g - List
32
+ Numbered lists are supported by starting the line with a number followed by a dot and a space, e.g 1. List.
33
+ Images, links, tables, LaTeX, and anything else is not supported.
34
+
35
+ If you need to use the symbols >, |, _, *, ~, @, #, :, `, put a backslash before them to escape them.
36
+
37
+ If the user is chatting casually, your responses should be only a few sentences, unless they are asking for help or a question.
38
+ Don't use unicode emoji unless needed."
39
+
40
+ # Use the system message above? (true/false)
41
+ USE_SYSTEM=true
42
+
43
+ # Use the model's system message? (true/false) If both are specified, model system message will be first
44
+ USE_MODEL_SYSTEM=true
45
+
46
+ # Require users to mention the bot to interact with it? (true/false)
47
+ REQUIRES_MENTION=true
48
+
49
+ # Whether to show a message at the start of a conversation
50
+ SHOW_START_OF_CONVERSATION=true
51
+
52
+ # Whether to use a random Ollama server or use the first available one
53
+ RANDOM_SERVER=false
54
+
55
+ # Whether to add a message before the first prompt of the conversation
56
+ INITIAL_PROMPT=""
57
+ USE_INITIAL_PROMPT=false
.eslintrc.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "eslint:recommended",
3
+ "env": {
4
+ "node": true,
5
+ "es6": true
6
+ },
7
+ "parserOptions": {
8
+ "ecmaVersion": 2021,
9
+ "sourceType": "module"
10
+ },
11
+ "rules": {
12
+ "arrow-spacing": ["warn", { "before": true, "after": true }],
13
+ "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
14
+ "comma-dangle": ["error", "never"],
15
+ "comma-spacing": "error",
16
+ "comma-style": "error",
17
+ "curly": ["error", "multi-line", "consistent"],
18
+ "dot-location": ["error", "property"],
19
+ "handle-callback-err": "off",
20
+ "indent": ["error", "tab", { "SwitchCase": 1 }],
21
+ "keyword-spacing": "error",
22
+ "max-nested-callbacks": ["error", { "max": 4 }],
23
+ "max-statements-per-line": ["error", { "max": 2 }],
24
+ "no-console": "off",
25
+ "no-empty": "warn",
26
+ "no-empty-function": "error",
27
+ "no-floating-decimal": "error",
28
+ "no-inline-comments": "error",
29
+ "no-lonely-if": "error",
30
+ "no-multi-spaces": "error",
31
+ "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
32
+ "no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
33
+ "no-trailing-spaces": ["error"],
34
+ "no-var": "error",
35
+ "object-curly-spacing": ["error", "always"],
36
+ "prefer-const": "error",
37
+ "quotes": ["error", "double"],
38
+ "semi": ["error", "always"],
39
+ "space-before-blocks": "error",
40
+ "space-before-function-paren": ["error", {
41
+ "anonymous": "never",
42
+ "named": "never",
43
+ "asyncArrow": "always"
44
+ }],
45
+ "space-in-parens": "error",
46
+ "space-infix-ops": "error",
47
+ "space-unary-ops": "error",
48
+ "spaced-comment": "error",
49
+ "yoda": "error",
50
+ "default-case-last": "error",
51
+ "switch-colon-spacing": ["error", {"after": true, "before": false}]
52
+ }
53
+ }
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ package-lock.json
2
+ pnpm-lock.yaml
3
+ pnpm-lock.yml
4
+ node_modules/
5
+ .env
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM node:20
2
+
3
+ COPY . .
4
+ RUN npm i --omit=dev --no-package-lock
5
+ USER node
6
+
7
+ CMD ["node","./src/index.js"]
Makefile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # As long as you have Make running on your machine you should be able to use this file.
2
+ # make <command-name> runs a given command (e.g. make compose-up)
3
+ # Command-names are given by starting a line without a tab and followed by a colon (i.e.':').
4
+ # what the command runs is the line below the colon and that line must start with a tab of size 4.
5
+ # Running make without a command after it will run the first command in the file.
6
+
7
+ # starts the discord-ai-bot
8
+ compose-up:
9
+ $(MAKE) setup_env && docker compose -p discord-ai up
10
+
11
+ # Stops docker compose without removing the containers from the system.
12
+ compose-stop:
13
+ docker compose -p discord-ai stop
14
+
15
+ # Stops docker compose and removes the containers from the system
16
+ compose-down:
17
+ docker compose -p discord-ai down
18
+
19
+ # Run the local node project with make and without docker
20
+ local:
21
+ $(MAKE) setup_env && npm i && node ./src/index.js
22
+
23
+ # This copies the .env.example (source) file to the .env (destination) file location
24
+ # The -n or no clobber means it will not overwrite the .env file if it already exists.
25
+ # The || : basically ignores the error code of the previous command and always succeeds.
26
+ setup_env:
27
+ cp -n ./.env.example ./.env 2>/dev/null || :
README.md CHANGED
@@ -1,10 +1,44 @@
1
- ---
2
- title: Test1234
3
- emoji: 📉
4
- colorFrom: pink
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <h1><a href="#"></a>Discord AI Bot</h1>
3
+ <h3 align="center"><a href="#"></a>Discord bot to interact with <a href="https://github.com/jmorganca/ollama">Ollama</a> as a chatbot</h3>
4
+ <h3><a href="#"></a><img alt="Stars" src="https://img.shields.io/github/stars/mekb-turtle/discord-ai-bot?display_name=tag&style=for-the-badge" /></h3>
5
+ <h3><a href="#"></a><img alt="Discord chat with the bot" src="assets/screenshot.png" /></h3>
6
+ </div>
 
 
7
 
8
+ ### Archived
9
+ I have decided to archive this project as I no longer have the time to maintain it. If you would like to take over the project, [please let me know](https://github.com/mekb-turtle).
10
+
11
+ ### Set-up instructions
12
+ 1. Install [Node.js](https://nodejs.org) (if you have a package manager, use that instead to install this)
13
+ - Make sure to install at least v14 of Node.js
14
+ 2. Install [Ollama](https://github.com/jmorganca/ollama) (ditto)
15
+ 3. Pull (download) a model, e.g `ollama pull orca` or `ollama pull llama2`
16
+ 4. Start Ollama by running `ollama serve`
17
+ 5. [Create a Discord bot](https://discord.com/developers/applications)
18
+ - Under Application » Bot
19
+ - Enable Message Content Intent
20
+ - Enable Server Members Intent (for replacing user mentions with the username)
21
+ 6. Invite the bot to a server
22
+ 1. Go to Application » OAuth2 » URL Generator
23
+ 2. Enable `bot`
24
+ 3. Enable Send Messages, Read Messages/View Channels, and Read Message History
25
+ 4. Under Generated URL, click Copy and paste the URL in your browser
26
+ 7. Rename `.env.example` to `.env` and edit the `.env` file
27
+ - You can get the token from Application » Bot » Token, **never share this with anyone**
28
+ - Make sure to change the model if you aren't using `orca`
29
+ - Ollama URL can be kept the same unless you have changed the port
30
+ - You can use multiple Ollama servers at the same time by separating the URLs with commas
31
+ - Set the channels to the channel ID, comma separated
32
+ 1. In Discord, go to User Settings » Advanced, and enable Developer Mode
33
+ 2. Right click on a channel you want to use, and click Copy Channel ID
34
+ - You can edit the system message the bot uses, or disable it entirely
35
+ 8. Start the bot with `npm start`
36
+ 9. You can interact with the bot by @mentioning it with your message
37
+
38
+ ### Set-up instructions with Docker
39
+ 1. Install [Docker](https://docs.docker.com/get-docker/)
40
+ - Should be atleast compatible with version 3 of compose (docker engine 1.13.0+)
41
+ 2. Repeat steps 2—7 from the other setup instructions
42
+ 3. Start the bot with `make compose-up` if you have Make installed
43
+ - Otherwise, try `docker compose -p discord-ai up` instead
44
+ 4. You can interact with the bot by @mentioning it with your message
assets/screenshot.png ADDED
docker-compose.yml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ version: "3"
2
+
3
+ services:
4
+ bot:
5
+ build: .
6
+ env_file: .env
7
+ environment:
8
+ - OLLAMA=http://host.docker.internal:11434
9
+ restart: unless-stopped
package-lock.json ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "discord-ai-bot",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "discord-ai-bot",
8
+ "dependencies": {
9
+ "axios": "^1.6.3",
10
+ "discord.js": "^14.14.1",
11
+ "dotenv": "^16.3.1",
12
+ "meklog": "^1.0.2"
13
+ }
14
+ },
15
+ "node_modules/@discordjs/builders": {
16
+ "version": "1.7.0",
17
+ "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.7.0.tgz",
18
+ "integrity": "sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==",
19
+ "dependencies": {
20
+ "@discordjs/formatters": "^0.3.3",
21
+ "@discordjs/util": "^1.0.2",
22
+ "@sapphire/shapeshift": "^3.9.3",
23
+ "discord-api-types": "0.37.61",
24
+ "fast-deep-equal": "^3.1.3",
25
+ "ts-mixer": "^6.0.3",
26
+ "tslib": "^2.6.2"
27
+ },
28
+ "engines": {
29
+ "node": ">=16.11.0"
30
+ }
31
+ },
32
+ "node_modules/@discordjs/collection": {
33
+ "version": "1.5.3",
34
+ "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
35
+ "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
36
+ "engines": {
37
+ "node": ">=16.11.0"
38
+ }
39
+ },
40
+ "node_modules/@discordjs/formatters": {
41
+ "version": "0.3.3",
42
+ "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.3.tgz",
43
+ "integrity": "sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==",
44
+ "dependencies": {
45
+ "discord-api-types": "0.37.61"
46
+ },
47
+ "engines": {
48
+ "node": ">=16.11.0"
49
+ }
50
+ },
51
+ "node_modules/@discordjs/rest": {
52
+ "version": "2.2.0",
53
+ "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.2.0.tgz",
54
+ "integrity": "sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==",
55
+ "dependencies": {
56
+ "@discordjs/collection": "^2.0.0",
57
+ "@discordjs/util": "^1.0.2",
58
+ "@sapphire/async-queue": "^1.5.0",
59
+ "@sapphire/snowflake": "^3.5.1",
60
+ "@vladfrangu/async_event_emitter": "^2.2.2",
61
+ "discord-api-types": "0.37.61",
62
+ "magic-bytes.js": "^1.5.0",
63
+ "tslib": "^2.6.2",
64
+ "undici": "5.27.2"
65
+ },
66
+ "engines": {
67
+ "node": ">=16.11.0"
68
+ }
69
+ },
70
+ "node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
71
+ "version": "2.0.0",
72
+ "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz",
73
+ "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==",
74
+ "engines": {
75
+ "node": ">=18"
76
+ }
77
+ },
78
+ "node_modules/@discordjs/util": {
79
+ "version": "1.0.2",
80
+ "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz",
81
+ "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==",
82
+ "engines": {
83
+ "node": ">=16.11.0"
84
+ }
85
+ },
86
+ "node_modules/@discordjs/ws": {
87
+ "version": "1.0.2",
88
+ "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.2.tgz",
89
+ "integrity": "sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==",
90
+ "dependencies": {
91
+ "@discordjs/collection": "^2.0.0",
92
+ "@discordjs/rest": "^2.1.0",
93
+ "@discordjs/util": "^1.0.2",
94
+ "@sapphire/async-queue": "^1.5.0",
95
+ "@types/ws": "^8.5.9",
96
+ "@vladfrangu/async_event_emitter": "^2.2.2",
97
+ "discord-api-types": "0.37.61",
98
+ "tslib": "^2.6.2",
99
+ "ws": "^8.14.2"
100
+ },
101
+ "engines": {
102
+ "node": ">=16.11.0"
103
+ }
104
+ },
105
+ "node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
106
+ "version": "2.0.0",
107
+ "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz",
108
+ "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==",
109
+ "engines": {
110
+ "node": ">=18"
111
+ }
112
+ },
113
+ "node_modules/@fastify/busboy": {
114
+ "version": "2.1.1",
115
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
116
+ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
117
+ "engines": {
118
+ "node": ">=14"
119
+ }
120
+ },
121
+ "node_modules/@sapphire/async-queue": {
122
+ "version": "1.5.2",
123
+ "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.2.tgz",
124
+ "integrity": "sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==",
125
+ "engines": {
126
+ "node": ">=v14.0.0",
127
+ "npm": ">=7.0.0"
128
+ }
129
+ },
130
+ "node_modules/@sapphire/shapeshift": {
131
+ "version": "3.9.7",
132
+ "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.7.tgz",
133
+ "integrity": "sha512-4It2mxPSr4OGn4HSQWGmhFMsNFGfFVhWeRPCRwbH972Ek2pzfGRZtb0pJ4Ze6oIzcyh2jw7nUDa6qGlWofgd9g==",
134
+ "dependencies": {
135
+ "fast-deep-equal": "^3.1.3",
136
+ "lodash": "^4.17.21"
137
+ },
138
+ "engines": {
139
+ "node": ">=v16"
140
+ }
141
+ },
142
+ "node_modules/@sapphire/snowflake": {
143
+ "version": "3.5.1",
144
+ "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz",
145
+ "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==",
146
+ "engines": {
147
+ "node": ">=v14.0.0",
148
+ "npm": ">=7.0.0"
149
+ }
150
+ },
151
+ "node_modules/@types/node": {
152
+ "version": "20.12.7",
153
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
154
+ "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
155
+ "dependencies": {
156
+ "undici-types": "~5.26.4"
157
+ }
158
+ },
159
+ "node_modules/@types/ws": {
160
+ "version": "8.5.9",
161
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz",
162
+ "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==",
163
+ "dependencies": {
164
+ "@types/node": "*"
165
+ }
166
+ },
167
+ "node_modules/@vladfrangu/async_event_emitter": {
168
+ "version": "2.2.4",
169
+ "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.4.tgz",
170
+ "integrity": "sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==",
171
+ "engines": {
172
+ "node": ">=v14.0.0",
173
+ "npm": ">=7.0.0"
174
+ }
175
+ },
176
+ "node_modules/asynckit": {
177
+ "version": "0.4.0",
178
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
179
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
180
+ },
181
+ "node_modules/axios": {
182
+ "version": "1.6.8",
183
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
184
+ "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
185
+ "dependencies": {
186
+ "follow-redirects": "^1.15.6",
187
+ "form-data": "^4.0.0",
188
+ "proxy-from-env": "^1.1.0"
189
+ }
190
+ },
191
+ "node_modules/combined-stream": {
192
+ "version": "1.0.8",
193
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
194
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
195
+ "dependencies": {
196
+ "delayed-stream": "~1.0.0"
197
+ },
198
+ "engines": {
199
+ "node": ">= 0.8"
200
+ }
201
+ },
202
+ "node_modules/delayed-stream": {
203
+ "version": "1.0.0",
204
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
205
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
206
+ "engines": {
207
+ "node": ">=0.4.0"
208
+ }
209
+ },
210
+ "node_modules/discord-api-types": {
211
+ "version": "0.37.61",
212
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz",
213
+ "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw=="
214
+ },
215
+ "node_modules/discord.js": {
216
+ "version": "14.14.1",
217
+ "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz",
218
+ "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==",
219
+ "dependencies": {
220
+ "@discordjs/builders": "^1.7.0",
221
+ "@discordjs/collection": "1.5.3",
222
+ "@discordjs/formatters": "^0.3.3",
223
+ "@discordjs/rest": "^2.1.0",
224
+ "@discordjs/util": "^1.0.2",
225
+ "@discordjs/ws": "^1.0.2",
226
+ "@sapphire/snowflake": "3.5.1",
227
+ "@types/ws": "8.5.9",
228
+ "discord-api-types": "0.37.61",
229
+ "fast-deep-equal": "3.1.3",
230
+ "lodash.snakecase": "4.1.1",
231
+ "tslib": "2.6.2",
232
+ "undici": "5.27.2",
233
+ "ws": "8.14.2"
234
+ },
235
+ "engines": {
236
+ "node": ">=16.11.0"
237
+ }
238
+ },
239
+ "node_modules/dotenv": {
240
+ "version": "16.4.5",
241
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
242
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
243
+ "engines": {
244
+ "node": ">=12"
245
+ },
246
+ "funding": {
247
+ "url": "https://dotenvx.com"
248
+ }
249
+ },
250
+ "node_modules/fast-deep-equal": {
251
+ "version": "3.1.3",
252
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
253
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
254
+ },
255
+ "node_modules/follow-redirects": {
256
+ "version": "1.15.6",
257
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
258
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
259
+ "funding": [
260
+ {
261
+ "type": "individual",
262
+ "url": "https://github.com/sponsors/RubenVerborgh"
263
+ }
264
+ ],
265
+ "engines": {
266
+ "node": ">=4.0"
267
+ },
268
+ "peerDependenciesMeta": {
269
+ "debug": {
270
+ "optional": true
271
+ }
272
+ }
273
+ },
274
+ "node_modules/form-data": {
275
+ "version": "4.0.0",
276
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
277
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
278
+ "dependencies": {
279
+ "asynckit": "^0.4.0",
280
+ "combined-stream": "^1.0.8",
281
+ "mime-types": "^2.1.12"
282
+ },
283
+ "engines": {
284
+ "node": ">= 6"
285
+ }
286
+ },
287
+ "node_modules/lodash": {
288
+ "version": "4.17.21",
289
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
290
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
291
+ },
292
+ "node_modules/lodash.snakecase": {
293
+ "version": "4.1.1",
294
+ "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
295
+ "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
296
+ },
297
+ "node_modules/magic-bytes.js": {
298
+ "version": "1.10.0",
299
+ "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz",
300
+ "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ=="
301
+ },
302
+ "node_modules/meklog": {
303
+ "version": "1.0.2",
304
+ "resolved": "https://registry.npmjs.org/meklog/-/meklog-1.0.2.tgz",
305
+ "integrity": "sha512-9jkTaZzWEpO0tiWQl0xoj/DPktcHELg74nSzsEaLaN7IYGmcczMXgQXQmZT1cXWykj2FAhv0BBRUGpnFCZn/dg=="
306
+ },
307
+ "node_modules/mime-db": {
308
+ "version": "1.52.0",
309
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
310
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
311
+ "engines": {
312
+ "node": ">= 0.6"
313
+ }
314
+ },
315
+ "node_modules/mime-types": {
316
+ "version": "2.1.35",
317
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
318
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
319
+ "dependencies": {
320
+ "mime-db": "1.52.0"
321
+ },
322
+ "engines": {
323
+ "node": ">= 0.6"
324
+ }
325
+ },
326
+ "node_modules/proxy-from-env": {
327
+ "version": "1.1.0",
328
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
329
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
330
+ },
331
+ "node_modules/ts-mixer": {
332
+ "version": "6.0.4",
333
+ "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
334
+ "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="
335
+ },
336
+ "node_modules/tslib": {
337
+ "version": "2.6.2",
338
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
339
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
340
+ },
341
+ "node_modules/undici": {
342
+ "version": "5.27.2",
343
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz",
344
+ "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==",
345
+ "dependencies": {
346
+ "@fastify/busboy": "^2.0.0"
347
+ },
348
+ "engines": {
349
+ "node": ">=14.0"
350
+ }
351
+ },
352
+ "node_modules/undici-types": {
353
+ "version": "5.26.5",
354
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
355
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
356
+ },
357
+ "node_modules/ws": {
358
+ "version": "8.14.2",
359
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
360
+ "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
361
+ "engines": {
362
+ "node": ">=10.0.0"
363
+ },
364
+ "peerDependencies": {
365
+ "bufferutil": "^4.0.1",
366
+ "utf-8-validate": ">=5.0.2"
367
+ },
368
+ "peerDependenciesMeta": {
369
+ "bufferutil": {
370
+ "optional": true
371
+ },
372
+ "utf-8-validate": {
373
+ "optional": true
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
package.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "discord-ai-bot",
3
+ "main": "src/index.js",
4
+ "scripts": {
5
+ "start": "node src/index.js"
6
+ },
7
+ "dependencies": {
8
+ "axios": "^1.6.3",
9
+ "discord.js": "^14.14.1",
10
+ "dotenv": "^16.3.1",
11
+ "meklog": "^1.0.2"
12
+ },
13
+ "type": "module"
14
+ }
src/bot.js ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Client, Events, GatewayIntentBits, MessageType, Partials } from "discord.js";
2
+ import { Logger, LogLevel } from "meklog";
3
+ import dotenv from "dotenv";
4
+ import axios from "axios";
5
+
6
+ dotenv.config();
7
+
8
+ const model = process.env.MODEL;
9
+ const servers = process.env.OLLAMA.split(",").map(url => ({ url: new URL(url), available: true }));
10
+ const channels = process.env.CHANNELS.split(",");
11
+
12
+ if (servers.length == 0) {
13
+ throw new Error("No servers available");
14
+ }
15
+
16
+ let log;
17
+ process.on("message", data => {
18
+ if (data.shardID) client.shardID = data.shardID;
19
+ if (data.logger) log = new Logger(data.logger);
20
+ });
21
+
22
+ const logError = (error) => {
23
+ if (error.response) {
24
+ let str = `Error ${error.response.status} ${error.response.statusText}: ${error.request.method} ${error.request.path}`;
25
+ if (error.response.data?.error) {
26
+ str += ": " + error.response.data.error;
27
+ }
28
+ log(LogLevel.Error, str);
29
+ } else {
30
+ log(LogLevel.Error, error);
31
+ }
32
+ };
33
+
34
+ function shuffleArray(array) {
35
+ for (let i = array.length - 1; i > 0; i--) {
36
+ const j = Math.floor(Math.random() * (i + 1));
37
+ [array[i], array[j]] = [array[j], array[i]];
38
+ }
39
+ return array;
40
+ }
41
+
42
+ async function makeRequest(path, method, data) {
43
+ while (servers.filter(server => server.available).length == 0) {
44
+ // wait until a server is available
45
+ await new Promise(res => setTimeout(res, 1000));
46
+ }
47
+
48
+ let error = null;
49
+ // randomly loop through the servers available, don't shuffle the actual array because we want to be notified of any updates
50
+ let order = new Array(servers.length).fill().map((_, i) => i);
51
+ if (randomServer) order = shuffleArray(order);
52
+ for (const j in order) {
53
+ if (!order.hasOwnProperty(j)) continue;
54
+ const i = order[j];
55
+ // try one until it succeeds
56
+ try {
57
+ // make a request to ollama
58
+ if (!servers[i].available) continue;
59
+ const url = new URL(servers[i].url); // don't modify the original URL
60
+
61
+ servers[i].available = false;
62
+
63
+ if (path.startsWith("/")) path = path.substring(1);
64
+ if (!url.pathname.endsWith("/")) url.pathname += "/"; // safety
65
+ url.pathname += path;
66
+ log(LogLevel.Debug, `Making request to ${url}`);
67
+ const result = await axios({
68
+ method, url, data,
69
+ responseType: "text"
70
+ });
71
+ servers[i].available = true;
72
+ return result.data;
73
+ } catch (err) {
74
+ servers[i].available = true;
75
+ error = err;
76
+ logError(error);
77
+ }
78
+ }
79
+ if (!error) {
80
+ throw new Error("No servers available");
81
+ }
82
+ throw error;
83
+ }
84
+
85
+ const client = new Client({
86
+ intents: [
87
+ GatewayIntentBits.Guilds,
88
+ GatewayIntentBits.GuildMessages,
89
+ GatewayIntentBits.GuildMembers,
90
+ GatewayIntentBits.DirectMessages,
91
+ GatewayIntentBits.MessageContent
92
+ ],
93
+ allowedMentions: { users: [], roles: [], repliedUser: false },
94
+ partials: [
95
+ Partials.Channel
96
+ ]
97
+ });
98
+
99
+ client.once(Events.ClientReady, async () => {
100
+ await client.guilds.fetch();
101
+ client.user.setPresence({ activities: [], status: "online" });
102
+ });
103
+
104
+ const messages = {};
105
+
106
+ // split text so it fits in a Discord message
107
+ function splitText(str, length) {
108
+ // trim matches different characters to \s
109
+ str = str
110
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n")
111
+ .replace(/^\s+|\s+$/g, "");
112
+ const segments = [];
113
+ let segment = "";
114
+ let word, suffix;
115
+ function appendSegment() {
116
+ segment = segment.replace(/^\s+|\s+$/g, "");
117
+ if (segment.length > 0) {
118
+ segments.push(segment);
119
+ segment = "";
120
+ }
121
+ }
122
+ // match a word
123
+ while ((word = str.match(/^[^\s]*(?:\s+|$)/)) != null) {
124
+ suffix = "";
125
+ word = word[0];
126
+ if (word.length == 0) break;
127
+ if (segment.length + word.length > length) {
128
+ // prioritise splitting by newlines over other whitespaces
129
+ if (segment.includes("\n")) {
130
+ // append up all but last paragraph
131
+ const beforeParagraph = segment.match(/^.*\n/s);
132
+ if (beforeParagraph != null) {
133
+ const lastParagraph = segment.substring(beforeParagraph[0].length, segment.length);
134
+ segment = beforeParagraph[0];
135
+ appendSegment();
136
+ segment = lastParagraph;
137
+ continue;
138
+ }
139
+ }
140
+ appendSegment();
141
+ // if word is larger than the split length
142
+ if (word.length > length) {
143
+ word = word.substring(0, length);
144
+ if (length > 1 && word.match(/^[^\s]+$/)) {
145
+ // try to hyphenate word
146
+ word = word.substring(0, word.length - 1);
147
+ suffix = "-";
148
+ }
149
+ }
150
+ }
151
+ str = str.substring(word.length, str.length);
152
+ segment += word + suffix;
153
+ }
154
+ appendSegment();
155
+ return segments;
156
+ }
157
+
158
+ function getBoolean(str) {
159
+ return !!str && str != "false" && str != "no" && str != "off" && str != "0";
160
+ }
161
+
162
+ function parseJSONMessage(str) {
163
+ return str.split(/[\r\n]+/g).map(function(line) {
164
+ const result = JSON.parse(`"${line}"`);
165
+ if (typeof result !== "string") throw new "Invalid syntax in .env file";
166
+ return result;
167
+ }).join("\n");
168
+ }
169
+
170
+ function parseEnvString(str) {
171
+ return typeof str === "string" ?
172
+ parseJSONMessage(str).replace(/<date>/gi, new Date().toUTCString()) : null;
173
+ }
174
+
175
+ const customSystemMessage = parseEnvString(process.env.SYSTEM);
176
+ const useCustomSystemMessage = getBoolean(process.env.USE_SYSTEM) && !!customSystemMessage;
177
+ const useModelSystemMessage = getBoolean(process.env.USE_MODEL_SYSTEM);
178
+ const showStartOfConversation = getBoolean(process.env.SHOW_START_OF_CONVERSATION);
179
+ const randomServer = getBoolean(process.env.RANDOM_SERVER);
180
+ let modelInfo = null;
181
+ const initialPrompt = parseEnvString(process.env.INITIAL_PROMPT);
182
+ const useInitialPrompt = getBoolean(process.env.USE_INITIAL_PROMPT) && !!initialPrompt;
183
+
184
+ const requiresMention = getBoolean(process.env.REQUIRES_MENTION);
185
+
186
+ async function replySplitMessage(replyMessage, content) {
187
+ const responseMessages = splitText(content, 2000).map(content => ({ content }));
188
+
189
+ const replyMessages = [];
190
+ for (let i = 0; i < responseMessages.length; ++i) {
191
+ if (i == 0) {
192
+ replyMessages.push(await replyMessage.reply(responseMessages[i]));
193
+ } else {
194
+ replyMessages.push(await replyMessage.channel.send(responseMessages[i]));
195
+ }
196
+ }
197
+ return replyMessages;
198
+ }
199
+
200
+ client.on(Events.MessageCreate, async message => {
201
+ let typing = false;
202
+ try {
203
+ await message.fetch();
204
+
205
+ // return if not in the right channel
206
+ const channelID = message.channel.id;
207
+ if (message.guild && !channels.includes(channelID)) return;
208
+
209
+ // return if user is a bot, or non-default message
210
+ if (!message.author.id) return;
211
+ if (message.author.bot || message.author.id == client.user.id) return;
212
+
213
+ const botRole = message.guild?.members?.me?.roles?.botRole;
214
+ const myMention = new RegExp(`<@((!?${client.user.id}${botRole ? `)|(&${botRole.id}` : ""}))>`, "g"); // RegExp to match a mention for the bot
215
+
216
+ if (typeof message.content !== "string" || message.content.length == 0) {
217
+ return;
218
+ }
219
+
220
+ let context = null;
221
+ if (message.type == MessageType.Reply) {
222
+ const reply = await message.fetchReference();
223
+ if (!reply) return;
224
+ if (reply.author.id != client.user.id) return;
225
+ if (messages[channelID] == null) return;
226
+ if ((context = messages[channelID][reply.id]) == null) return;
227
+ } else if (message.type != MessageType.Default) {
228
+ return;
229
+ }
230
+
231
+ // fetch info about the model like the template and system message
232
+ if (modelInfo == null) {
233
+ modelInfo = (await makeRequest("/api/show", "post", {
234
+ name: model
235
+ }));
236
+ if (typeof modelInfo === "string") modelInfo = JSON.parse(modelInfo);
237
+ if (typeof modelInfo !== "object") throw "failed to fetch model information";
238
+ }
239
+
240
+ const systemMessages = [];
241
+
242
+ if (useModelSystemMessage && modelInfo.system) {
243
+ systemMessages.push(modelInfo.system);
244
+ }
245
+
246
+ if (useCustomSystemMessage) {
247
+ systemMessages.push(customSystemMessage);
248
+ }
249
+
250
+ // join them together
251
+ const systemMessage = systemMessages.join("\n\n");
252
+
253
+ // deal with commands first before passing to LLM
254
+ let userInput = message.content
255
+ .replace(new RegExp("^\s*" + myMention.source, ""), "").trim();
256
+
257
+ // may change this to slash commands in the future
258
+ // i'm using regular text commands currently because the bot interacts with text content anyway
259
+ if (userInput.startsWith(".")) {
260
+ const args = userInput.substring(1).split(/\s+/g);
261
+ const cmd = args.shift();
262
+ switch (cmd) {
263
+ case "reset":
264
+ case "clear":
265
+ if (messages[channelID] != null) {
266
+ // reset conversation
267
+ const cleared = messages[channelID].amount;
268
+
269
+ // clear
270
+ delete messages[channelID];
271
+
272
+ if (cleared > 0) {
273
+ await message.reply({ content: `Cleared conversation of ${cleared} messages` });
274
+ break;
275
+ }
276
+ }
277
+ await message.reply({ content: "No messages to clear" });
278
+ break;
279
+ case "help":
280
+ case "?":
281
+ case "h":
282
+ await message.reply({ content: "Commands:\n- `.reset` `.clear`\n- `.help` `.?` `.h`\n- `.ping`\n- `.model`\n- `.system`" });
283
+ break;
284
+ case "model":
285
+ await message.reply({
286
+ content: `Current model: ${model}`
287
+ });
288
+ break;
289
+ case "system":
290
+ await replySplitMessage(message, `System message:\n\n${systemMessage}`);
291
+ break;
292
+ case "ping":
293
+ // get ms difference
294
+ const beforeTime = Date.now();
295
+ const reply = await message.reply({ content: "Ping" });
296
+ const afterTime = Date.now();
297
+ const difference = afterTime - beforeTime;
298
+ await reply.edit({ content: `Ping: ${difference}ms` });
299
+ break;
300
+ case "":
301
+ break;
302
+ default:
303
+ await message.reply({ content: "Unknown command, type `.help` for a list of commands" });
304
+ break;
305
+ }
306
+ return;
307
+ }
308
+
309
+ if (message.type == MessageType.Default && (requiresMention && message.guild && !message.content.match(myMention))) return;
310
+
311
+ if (message.guild) {
312
+ await message.guild.channels.fetch();
313
+ await message.guild.members.fetch();
314
+ }
315
+
316
+ userInput = userInput
317
+ .replace(myMention, "")
318
+ .replace(/<#([0-9]+)>/g, (_, id) => {
319
+ if (message.guild) {
320
+ const chn = message.guild.channels.cache.get(id);
321
+ if (chn) return `#${chn.name}`;
322
+ }
323
+ return "#unknown-channel";
324
+ })
325
+ .replace(/<@!?([0-9]+)>/g, (_, id) => {
326
+ if (id == message.author.id) return message.author.username;
327
+ if (message.guild) {
328
+ const mem = message.guild.members.cache.get(id);
329
+ if (mem) return `@${mem.user.username}`;
330
+ }
331
+ return "@unknown-user";
332
+ })
333
+ .replace(/<:([a-zA-Z0-9_]+):([0-9]+)>/g, (_, name) => {
334
+ return `emoji:${name}:`;
335
+ })
336
+ .trim();
337
+
338
+ if (userInput.length == 0) return;
339
+
340
+ // create conversation
341
+ if (messages[channelID] == null) {
342
+ messages[channelID] = { amount: 0, last: null };
343
+ }
344
+
345
+ // log user's message
346
+ log(LogLevel.Debug, `${message.guild ? `#${message.channel.name}` : "DMs"} - ${message.author.username}: ${userInput}`);
347
+
348
+ // start typing
349
+ typing = true;
350
+ await message.channel.sendTyping();
351
+ let typingInterval = setInterval(async () => {
352
+ try {
353
+ await message.channel.sendTyping();
354
+ } catch (error) {
355
+ if (typingInterval != null) {
356
+ clearInterval(typingInterval);
357
+ }
358
+ typingInterval = null;
359
+ }
360
+ }, 7000);
361
+
362
+ let response;
363
+ try {
364
+ // context if the message is not a reply
365
+ if (context == null) {
366
+ context = messages[channelID].last;
367
+ }
368
+
369
+ if (useInitialPrompt && messages[channelID].amount == 0) {
370
+ userInput = `${initialPrompt}\n\n${userInput}`;
371
+ log(LogLevel.Debug, "Adding initial prompt to message");
372
+ }
373
+
374
+ // make request to model
375
+ response = (await makeRequest("/api/generate", "post", {
376
+ model: model,
377
+ prompt: userInput,
378
+ system: systemMessage,
379
+ context
380
+ }));
381
+
382
+ if (typeof response != "string") {
383
+ log(LogLevel.Debug, response);
384
+ throw new TypeError("response is not a string, this may be an error with ollama");
385
+ }
386
+
387
+ response = response.split("\n").filter(e => !!e).map(e => {
388
+ return JSON.parse(e);
389
+ });
390
+ } catch (error) {
391
+ if (typingInterval != null) {
392
+ clearInterval(typingInterval);
393
+ }
394
+ typingInterval = null;
395
+ throw error;
396
+ }
397
+
398
+ if (typingInterval != null) {
399
+ clearInterval(typingInterval);
400
+ }
401
+ typingInterval = null;
402
+
403
+ let responseText = response.map(e => e.response).filter(e => e != null).join("").trim();
404
+ if (responseText.length == 0) {
405
+ responseText = "(No response)";
406
+ }
407
+
408
+ log(LogLevel.Debug, `Response: ${responseText}`);
409
+
410
+ const prefix = showStartOfConversation && messages[channelID].amount == 0 ?
411
+ "> This is the beginning of the conversation, type `.help` for help.\n\n" : "";
412
+
413
+ // reply (will automatically stop typing)
414
+ const replyMessageIDs = (await replySplitMessage(message, `${prefix}${responseText}`)).map(msg => msg.id);
415
+
416
+ // add response to conversation
417
+ context = response.filter(e => e.done && e.context)[0].context;
418
+ for (let i = 0; i < replyMessageIDs.length; ++i) {
419
+ messages[channelID][replyMessageIDs[i]] = context;
420
+ }
421
+ messages[channelID].last = context;
422
+ ++messages[channelID].amount;
423
+ } catch (error) {
424
+ if (typing) {
425
+ try {
426
+ // return error
427
+ await message.reply({ content: "Error, please check the console" });
428
+ } catch (ignored) {}
429
+ }
430
+ logError(error);
431
+ }
432
+ });
433
+
434
+ client.login(process.env.TOKEN);
src/index.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ShardingManager, Events } from "discord.js";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Logger, LogLevel } from "meklog";
5
+ import dotenv from "dotenv";
6
+
7
+ dotenv.config();
8
+
9
+ const production = process.env.NODE_ENV == "prod" || process.env.NODE_ENV == "production";
10
+ const log = new Logger(production, "Shard Manager");
11
+
12
+ log(LogLevel.Info, "Loading");
13
+
14
+ const filePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "bot.js");
15
+ const manager = new ShardingManager(filePath, { token: process.env.TOKEN });
16
+
17
+ manager.on("shardCreate", async shard => {
18
+ const shardLog = new Logger(production, `Shard #${shard.id}`);
19
+
20
+ shardLog(LogLevel.Info, "Created shard");
21
+
22
+ shard.once(Events.ClientReady, async () => {
23
+ shard.send({ shardID: shard.id, logger: shardLog.data });
24
+
25
+ shardLog(LogLevel.Info, "Shard ready");
26
+ });
27
+ });
28
+
29
+ manager.spawn();
30
+