diff --git a/.env.example b/.env.example
index 5f175ff..056affb 100644
--- a/.env.example
+++ b/.env.example
@@ -4,9 +4,12 @@ TOKEN=
# What language model to use, orca is one of the lower-end models that doesn't require as much computer power as llama2
MODEL=orca
-# Ollama URL (if you want to use multiple, separate them by commas)
+# Ollama URL
OLLAMA=http://localhost:11434
+# Stable diffusion URL
+STABLE_DIFFUSION=http://localhost:7860
+
# What Discord channels to enable it in (by ID)
CHANNELS=123456789,987654321
diff --git a/.eslintrc.json b/.eslintrc.json
index 12dcd8e..c652c3b 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -25,7 +25,6 @@
"no-empty": "warn",
"no-empty-function": "error",
"no-floating-decimal": "error",
- "no-inline-comments": "error",
"no-lonely-if": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
diff --git a/.gitignore b/.gitignore
index 553aeb5..0d51cca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ pnpm-lock.yaml
pnpm-lock.yml
node_modules/
.env
+.vscode/
diff --git a/README.md b/README.md
index f742f03..1050bd5 100644
--- a/README.md
+++ b/README.md
@@ -1,38 +1,46 @@
-
Discord AI Bot
-
Discord bot to interact with Ollama as a chatbot
-

-

+
Discord AI Bot
+
Repository is now in maintanance mode - rewriting project to Typescript on typescript branch
+
+

+

+The project started thanks to [mekb](https://github.com/mekb-turtle).
+
### Set-up instructions
+
1. Install [Node.js](https://nodejs.org) (if you have a package manager, use that instead to install this)
- - Make sure to install at least v14 of Node.js
+ - Make sure to install at least v14 of Node.js
2. Install [Ollama](https://github.com/jmorganca/ollama) (ditto)
3. Pull (download) a model, e.g `ollama pull orca` or `ollama pull llama2`
4. Start Ollama by running `ollama serve`
5. [Create a Discord bot](https://discord.com/developers/applications)
- - Under Application » Bot
- - Enable Message Content Intent
- - Enable Server Members Intent (for replacing user mentions with the username)
+ - Under Application » Bot
+ - Enable Message Content Intent
+ - Enable Server Members Intent (for replacing user mentions with the username)
6. Invite the bot to a server
- 1. Go to Application » OAuth2 » URL Generator
- 2. Enable `bot`
- 3. Enable Send Messages, Read Messages/View Channels, and Read Message History
- 4. Under Generated URL, click Copy and paste the URL in your browser
+ 1. Go to Application » OAuth2 » URL Generator
+ 2. Enable `bot`
+ 3. Enable Send Messages, Read Messages/View Channels, and Read Message History
+ 4. Under Generated URL, click Copy and paste the URL in your browser
7. Rename `.env.example` to `.env` and edit the `.env` file
- - You can get the token from Application » Bot » Token, **never share this with anyone**
- - Make sure to change the model if you aren't using `orca`
- - Ollama URL can be kept the same unless you have changed the port
- - You can use multiple Ollama servers at the same time by separating the URLs with commas
- - Set the channels to the channel ID, comma separated
- 1. In Discord, go to User Settings » Advanced, and enable Developer Mode
- 2. Right click on a channel you want to use, and click Copy Channel ID
- - You can edit the system message the bot uses, or disable it entirely
-8. Start the bot with `npm start`
-9. You can interact with the bot by @mentioning it with your message
+ - You can get the token from Application » Bot » Token, **never share this with anyone**
+ - Make sure to change the model if you aren't using `orca`
+ - Ollama URL can be kept the same unless you have changed the port
+ - You can use multiple Ollama servers at the same time by separating the URLs with commas
+ - Set the channels to the channel ID, comma separated
+ 1. In Discord, go to User Settings » Advanced, and enable Developer Mode
+ 2. Right click on a channel you want to use, and click Copy Channel ID
+ - You can edit the system message the bot uses, or disable it entirely
+8. Install the required dependencies with `npm i`
+9. Start the bot with `npm start`
+10. You can interact with the bot by @mentioning it with your message
+11. Install Stable Diffusion
+12. Run the script `./webui.sh --api --listen`
### Set-up instructions with Docker
+
1. Install [Docker](https://docs.docker.com/get-docker/)
- Should be atleast compatible with version 3 of compose (docker engine 1.13.0+)
2. Repeat steps 2—7 from the other setup instructions
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..6ca2cbc
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,99 @@
+import globals from "globals";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import js from "@eslint/js";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+ recommendedConfig: js.configs.recommended,
+ allConfig: js.configs.all
+});
+
+export default [...compat.extends("eslint:recommended"), {
+ languageOptions: {
+ globals: {
+ ...globals.node
+ },
+
+ ecmaVersion: 2021,
+ sourceType: "module"
+ },
+
+ rules: {
+ "arrow-spacing": ["warn", {
+ before: true,
+ after: true
+ }],
+
+ "brace-style": ["error", "1tbs", {
+ allowSingleLine: true
+ }],
+
+ "comma-dangle": ["error", "never"],
+ "comma-spacing": "error",
+ "comma-style": "error",
+ curly: ["error", "multi-line", "consistent"],
+ "dot-location": ["error", "property"],
+ "handle-callback-err": "off",
+
+ indent: ["error", "tab", {
+ SwitchCase: 1
+ }],
+
+ "keyword-spacing": "error",
+
+ "max-nested-callbacks": ["error", {
+ max: 4
+ }],
+
+ "max-statements-per-line": ["error", {
+ max: 2
+ }],
+
+ "no-console": "off",
+ "no-empty": "warn",
+ "no-empty-function": "error",
+ "no-floating-decimal": "error",
+ "no-lonely-if": "error",
+ "no-multi-spaces": "error",
+
+ "no-multiple-empty-lines": ["error", {
+ max: 2,
+ maxEOF: 1,
+ maxBOF: 0
+ }],
+
+ "no-shadow": ["error", {
+ allow: ["err", "resolve", "reject"]
+ }],
+
+ "no-trailing-spaces": ["error"],
+ "no-var": "error",
+ "object-curly-spacing": ["error", "always"],
+ "prefer-const": "error",
+ quotes: ["error", "double"],
+ semi: ["error", "always"],
+ "space-before-blocks": "error",
+
+ "space-before-function-paren": ["error", {
+ anonymous: "never",
+ named: "never",
+ asyncArrow: "always"
+ }],
+
+ "space-in-parens": "error",
+ "space-infix-ops": "error",
+ "space-unary-ops": "error",
+ "spaced-comment": "error",
+ yoda: "error",
+ "default-case-last": "error",
+
+ "switch-colon-spacing": ["error", {
+ after: true,
+ before: false
+ }]
+ }
+}];
\ No newline at end of file
diff --git a/package.json b/package.json
index d2d148e..8b679e5 100644
--- a/package.json
+++ b/package.json
@@ -10,5 +10,11 @@
"dotenv": "^16.3.1",
"meklog": "^1.0.2"
},
- "type": "module"
+ "type": "module",
+ "devDependencies": {
+ "@eslint/js": "^9.7.0",
+ "@types/eslint__js": "^8.42.3",
+ "eslint": "^9.7.0",
+ "globals": "^15.8.0"
+ }
}
diff --git a/src/bot.js b/src/bot.js
index 7752304..8575812 100644
--- a/src/bot.js
+++ b/src/bot.js
@@ -1,12 +1,22 @@
-import { Client, Events, GatewayIntentBits, MessageType, Partials } from "discord.js";
+import {
+ Client,
+ Events,
+ GatewayIntentBits,
+ MessageType,
+ Partials,
+ REST,
+ Routes
+} from "discord.js";
import { Logger, LogLevel } from "meklog";
import dotenv from "dotenv";
import axios from "axios";
+import commands from "./commands/commands.js";
dotenv.config();
const model = process.env.MODEL;
const servers = process.env.OLLAMA.split(",").map(url => ({ url: new URL(url), available: true }));
+const stableDiffusionServers = process.env.STABLE_DIFFUSION.split(",").map(url => ({ url: new URL(url), available: true }));
const channels = process.env.CHANNELS.split(",");
if (servers.length == 0) {
@@ -82,6 +92,48 @@ async function makeRequest(path, method, data) {
throw error;
}
+async function makeStableDiffusionRequest(path, method, data) {
+ while (stableDiffusionServers.filter(server => server.available).length == 0) {
+ // wait until a server is available
+ await new Promise(res => setTimeout(res, 1000));
+ }
+
+ let error = null;
+ // randomly loop through the servers available, don't shuffle the actual array because we want to be notified of any updates
+ let order = new Array(stableDiffusionServers.length).fill().map((_, i) => i);
+ if (randomServer) order = shuffleArray(order);
+ for (const j in order) {
+ if (!order.hasOwnProperty(j)) continue;
+ const i = order[j];
+ // try one until it succeeds
+ try {
+ // make a request to stable diffusion
+ if (!stableDiffusionServers[i].available) continue;
+ const url = new URL(stableDiffusionServers[i].url); // don't modify the original URL
+
+ stableDiffusionServers[i].available = false;
+
+ if (path.startsWith("/")) path = path.substring(1);
+ if (!url.pathname.endsWith("/")) url.pathname += "/"; // safety
+ url.pathname += path;
+ log(LogLevel.Debug, `Making stable diffusion request to ${url}`);
+ const result = await axios({
+ method, url, data
+ });
+ stableDiffusionServers[i].available = true;
+ return result.data;
+ } catch (err) {
+ stableDiffusionServers[i].available = true;
+ error = err;
+ logError(error);
+ }
+ }
+ if (!error) {
+ throw new Error("No servers available");
+ }
+ throw error;
+}
+
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
@@ -96,9 +148,16 @@ const client = new Client({
]
});
+const rest = new REST({ version: "10" }).setToken(process.env.TOKEN);
+
client.once(Events.ClientReady, async () => {
await client.guilds.fetch();
client.user.setPresence({ activities: [], status: "online" });
+ await rest.put(Routes.applicationCommands(client.user.id), {
+ body: commands
+ });
+
+ log(LogLevel.Info, "Successfully reloaded application slash (/) commands.");
});
const messages = {};
@@ -160,7 +219,7 @@ function getBoolean(str) {
}
function parseJSONMessage(str) {
- return str.split(/[\r\n]+/g).map(function(line) {
+ return str.split(/[\r\n]+/g).map(line => {
const result = JSON.parse(`"${line}"`);
if (typeof result !== "string") throw new "Invalid syntax in .env file";
return result;
@@ -184,7 +243,7 @@ const useInitialPrompt = getBoolean(process.env.USE_INITIAL_PROMPT) && !!initial
const requiresMention = getBoolean(process.env.REQUIRES_MENTION);
async function replySplitMessage(replyMessage, content) {
- const responseMessages = splitText(content, 2000).map(content => ({ content }));
+ const responseMessages = splitText(content, 2000).map(text => ({ content: text }));
const replyMessages = [];
for (let i = 0; i < responseMessages.length; ++i) {
@@ -252,7 +311,7 @@ client.on(Events.MessageCreate, async message => {
// deal with commands first before passing to LLM
let userInput = message.content
- .replace(new RegExp("^\s*" + myMention.source, ""), "").trim();
+ .replace(new RegExp("^s*" + myMention.source, ""), "").trim();
// may change this to slash commands in the future
// i'm using regular text commands currently because the bot interacts with text content anyway
@@ -291,11 +350,16 @@ client.on(Events.MessageCreate, async message => {
break;
case "ping":
// get ms difference
- const beforeTime = Date.now();
- const reply = await message.reply({ content: "Ping" });
- const afterTime = Date.now();
- const difference = afterTime - beforeTime;
- await reply.edit({ content: `Ping: ${difference}ms` });
+ try {
+ const beforeTime = Date.now();
+ const reply = await message.reply({ content: "Ping" });
+ const afterTime = Date.now();
+ const difference = afterTime - beforeTime;
+ await reply.edit({ content: `Ping: ${difference}ms` });
+ } catch (error) {
+ logError(error);
+ await message.reply({ content: "Error, please check the console" });
+ }
break;
case "":
break;
@@ -337,6 +401,23 @@ client.on(Events.MessageCreate, async message => {
if (userInput.length == 0) return;
+ // Process text files if attached
+ if (message.attachments.size > 0) {
+ const textAttachments = Array.from(message.attachments, ([, value]) => value).filter(att => att.contentType.startsWith("text"));
+ if (textAttachments.length > 0) {
+ try {
+ await Promise.all(textAttachments.map(async (att, i) => {
+ const response = await axios.get(att.url);
+ userInput += `\n${i + 1}. File - ${att.name}:\n${response.data}`;
+ }));
+ } catch (error) {
+ log(LogLevel.Error, `Failed to download text files: ${error}`);
+ await message.reply({ content: "Failed to download text files" });
+ return; // Stop processing if file download fails
+ }
+ }
+ }
+
// create conversation
if (messages[channelID] == null) {
messages[channelID] = { amount: 0, last: null };
@@ -352,6 +433,7 @@ client.on(Events.MessageCreate, async message => {
try {
await message.channel.sendTyping();
} catch (error) {
+ logError(error);
if (typingInterval != null) {
clearInterval(typingInterval);
}
@@ -425,10 +507,60 @@ client.on(Events.MessageCreate, async message => {
try {
// return error
await message.reply({ content: "Error, please check the console" });
- } catch (ignored) {}
+ } catch (ignored) {
+ logError(ignored);
+ }
}
logError(error);
}
});
+client.on(Events.InteractionCreate, async (interaction) => {
+ if (!interaction.isCommand()) return;
+
+ const { commandName, options } = interaction;
+
+ switch (commandName) {
+ case "text2img":
+ try {
+ const prompt = options.getString("prompt");
+ const width = options.getNumber("width") || 256;
+ const height = options.getNumber("height") || 256;
+ const steps = options.getNumber("steps") || 10;
+ const batch_count = options.getNumber("batch_count") || 1;
+ const batch_size = options.getNumber("batch_size") || 1;
+ const enhance_prompt = (options.getBoolean("enhance_prompt") && true) ? "yes" : "no";
+
+ await interaction.deferReply();
+ const stableDiffusionResponse = await makeStableDiffusionRequest(
+ "/sdapi/v1/txt2img",
+ "post",
+ {
+ prompt,
+ width,
+ height,
+ steps,
+ num_inference_steps: steps,
+ batch_count,
+ batch_size,
+ enhance_prompt
+ }
+ );
+ const images = stableDiffusionResponse.images.map((image) =>
+ Buffer.from(image, "base64")
+ );
+ await interaction.editReply({
+ content: `Here are images from prompt \`${prompt}\``,
+ files: images
+ });
+ } catch (error) {
+ logError(error);
+ await interaction.editReply({
+ content: "Error, please check the console"
+ });
+ }
+ break;
+ }
+});
+
client.login(process.env.TOKEN);
diff --git a/src/commands/commands.js b/src/commands/commands.js
new file mode 100644
index 0000000..01edf15
--- /dev/null
+++ b/src/commands/commands.js
@@ -0,0 +1,3 @@
+import text2img from "./text2img.js";
+
+export default [text2img];
diff --git a/src/commands/text2img.js b/src/commands/text2img.js
new file mode 100644
index 0000000..c8f6097
--- /dev/null
+++ b/src/commands/text2img.js
@@ -0,0 +1,56 @@
+import { SlashCommandBuilder } from "discord.js";
+
+const text2img = new SlashCommandBuilder()
+ .setName("text2img")
+ .setDescription("Convert text to image")
+ .addStringOption((option) =>
+ option.setName("prompt").setDescription("Text to convert").setRequired(true)
+ )
+ .addNumberOption((option) =>
+ option
+ .setName("width")
+ .setDescription("Width of the image")
+ .setRequired(false)
+ .setMinValue(128)
+ .setMaxValue(1024)
+ )
+ .addNumberOption((option) =>
+ option
+ .setName("height")
+ .setDescription("Height of the image")
+ .setRequired(false)
+ .setMinValue(128)
+ .setMaxValue(1024)
+ )
+ .addNumberOption((option) =>
+ option
+ .setName("steps")
+ .setDescription("Number of steps")
+ .setRequired(false)
+ .setMinValue(5)
+ .setMaxValue(20)
+ )
+ .addNumberOption((option) =>
+ option
+ .setName("batch_count")
+ .setDescription("Batch count")
+ .setRequired(false)
+ .setMinValue(1)
+ .setMaxValue(4)
+ )
+ .addNumberOption((option) =>
+ option
+ .setName("batch_size")
+ .setDescription("Batch size")
+ .setRequired(false)
+ .setMinValue(1)
+ .setMaxValue(5)
+ )
+ .addBooleanOption((option) =>
+ option
+ .setName("enhance_prompt")
+ .setDescription("Enhance prompt")
+ .setRequired(false)
+ );
+
+export default text2img;