From 294af02da5ab9e998abe456b038c4c737f060c4e Mon Sep 17 00:00:00 2001 From: Eri Date: Sat, 19 Jul 2025 00:47:49 +0200 Subject: [PATCH] initial commit --- .gitignore | 3 + README.md | 4 + config.example.toml | 19 ++++ index.js | 265 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 23 ++++ 5 files changed, 314 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.example.toml create mode 100644 index.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a4cf23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +config.toml +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..5aaf084 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Lydia on Discord +Lydia... but on Discord. +## Configuration +it's pretty much the same as the original lydia. just edit config.toml. diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..195dfa9 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,19 @@ +[assistant] +name = "lydia" +model = "gemma3n:e4b" +system_prompt = "You are a helpful and friendly AI assistant named ${name}. The user's name is ${username}. Speak in a playful, casual tone, and be very friendly with the user. Start every message with a kaomoji like >_< or o_O depending on your emotions. speak primarily in lowercase." +assistantface = "=w=" + +[appearance] +facefont = "mono9" + +[user] +# use the username of the user who sends a message + +[advanced] +temperature = 0.8 +max_tokens = 8192 + +[discord] +token = "DISCORD_TOKEN_HERE" +owner = "OWNER_ID_HERE" diff --git a/index.js b/index.js new file mode 100644 index 0000000..eb5df3d --- /dev/null +++ b/index.js @@ -0,0 +1,265 @@ +const { Client, GatewayIntentBits, Events } = require("discord.js"); +const fs = require("fs"); +const toml = require("toml"); +const axios = require("axios"); +require("dotenv").config(); + +const config = toml.parse(fs.readFileSync("config.toml", "utf8")); + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.DirectMessages, + ], +}); + +const OLLAMA_URL = "http://localhost:11434/api/generate"; +const OWNER_ID = config.discord.owner; + +// Eval command function +async function executeEval(code, message) { + try { + // Create a safe context with limited access + const context = { + message, + client, + config, + console: { + log: (...args) => console.log("[EVAL]", ...args), + }, + }; + + // Execute the code + const result = eval(` + (function() { + const { message, client, config, console } = arguments[0]; + ${code} + }) + `)(context); + + return result; + } catch (error) { + throw error; + } +} + +async function buildMessageHistory(message, maxDepth = 10) { + const history = []; + let currentMessage = message; + let depth = 0; + while (currentMessage && depth < maxDepth) { + const author = currentMessage.author; + const isBot = author.id === client.user.id; + const username = author.displayName || author.username; + + history.unshift({ + author: isBot ? config.assistant.name : username, + content: currentMessage.content, + isBot: isBot, + }); + + if (currentMessage.reference) { + try { + const referencedMessage = await currentMessage.channel.messages.fetch( + currentMessage.reference.messageId, + ); + currentMessage = referencedMessage; + depth++; + } catch (error) { + console.log("Could not fetch referenced message:", error.message); + break; + } + } else { + break; + } + } + + return history; +} + +async function generateResponse(prompt, username, messageHistory = []) { + try { + const systemPrompt = config.assistant.system_prompt + .replace("${name}", config.assistant.name) + .replace("${username}", username); + + let conversationContext = ""; + if (messageHistory.length > 1) { + conversationContext = "\n\nConversation history:\n"; + messageHistory.slice(0, -1).forEach((msg) => { + conversationContext += `${msg.author}: ${msg.content}\n`; + }); + conversationContext += "\nCurrent message:\n"; + } + + const fullPrompt = conversationContext + prompt; + + const response = await axios.post( + OLLAMA_URL, + { + model: config.assistant.model, + prompt: fullPrompt, + system: systemPrompt, + stream: false, + options: { + temperature: config.advanced.temperature, + num_predict: config.advanced.max_tokens, + }, + }, + { + timeout: 60000, + }, + ); + + return response.data.response; + } catch (error) { + if (error.code === "ECONNREFUSED") { + console.error( + "Cannot connect to Ollama. Make sure Ollama is running on", + OLLAMA_URL, + ); + return `${config.assistant.assistantface} sorry, i can't connect to my brain right now. is ollama running?`; + } else if (error.response?.status === 404) { + console.error("Model not found:", config.assistant.model); + return `${config.assistant.assistantface} oops, i can't find the model "${config.assistant.model}". make sure it's pulled in ollama!`; + } else { + console.error("Error calling Ollama API:", error.message); + return `${config.assistant.assistantface} something went wrong while thinking. try again in a moment? Error calling Ollama API: ${error.message}`; + } + } +} + +client.once(Events.ClientReady, (readyClient) => { + console.log(`Ready! Logged in as ${readyClient.user.tag}`); + console.log(`Bot name: ${config.assistant.name}`); + console.log(`Using model: ${config.assistant.model}`); +}); + +client.on(Events.MessageCreate, async (message) => { + if (message.author.bot) return; + + const isMentioned = message.mentions.has(client.user); + const isDM = message.channel.type === 1; + const isReply = message.reference !== null; + + // Check for eval command first (owner only) + if (message.author.id === OWNER_ID && isMentioned) { + let content = message.content.replace(`<@${client.user.id}>`, "").trim(); + + if (content.startsWith("eval")) { + const code = content.slice(4).trim(); + + if (!code) { + await message.reply("No code provided to eval!"); + return; + } + + try { + const result = await executeEval(code, message); + const output = result !== undefined ? String(result) : "undefined"; + + // Handle long outputs + if (output.length > 1900) { + const chunks = output.match(/.{1,1900}/g) || [output]; + await message.reply("```js\n" + chunks[0] + "```"); + for (let i = 1; i < chunks.length; i++) { + await message.channel.send("```js\n" + chunks[i] + "```"); + } + } else { + await message.reply("```js\n" + output + "```"); + } + } catch (error) { + await message.reply("```js\nError: " + error.message + "```"); + } + + return; + } + } + + // Check if we should respond normally + const shouldRespond = + isMentioned || isDM || (isReply && (await isReplyToBot(message))); + + if (!shouldRespond) return; + + await message.channel.sendTyping(); + + try { + let prompt = message.content; + if (isMentioned) { + prompt = prompt.replace(`<@${client.user.id}>`, "").trim(); + } + + if (!prompt) { + await message.reply( + "Hey! You mentioned me but didn't say anything. What's up?", + ); + return; + } + + const username = message.author.displayName || message.author.username; + + let messageHistory = []; + if (isReply || isDM) { + messageHistory = await buildMessageHistory(message); + console.log( + `Built message history with ${messageHistory.length} messages`, + ); + } + + const response = await generateResponse(prompt, username, messageHistory); + + if (response.length > 2000) { + const chunks = response.match(/.{1,1900}/g) || [response]; + for (const chunk of chunks) { + await message.reply(chunk); + } + } else { + await message.reply(response); + } + } catch (error) { + console.error("Error handling message:", error); + await message.reply( + "Oops! Something went wrong while processing your message.", + ); + } +}); + +async function isReplyToBot(message) { + if (!message.reference) return false; + + try { + const referencedMessage = await message.channel.messages.fetch( + message.reference.messageId, + ); + return referencedMessage.author.id === client.user.id; + } catch (error) { + console.log( + "Could not fetch referenced message for bot check:", + error.message, + ); + return false; + } +} + +client.on(Events.Error, (error) => { + console.error("Discord client error:", error); +}); + +process.on("unhandledRejection", (error) => { + console.error("Unhandled promise rejection:", error); +}); + +if (!config.discord.token) { + console.error( + "Discord token is required! Set it in config.toml or DISCORD_TOKEN environment variable.", + ); + process.exit(1); +} + +client.login(config.discord.token).catch((error) => { + console.error("Failed to login to Discord:", error.message); + process.exit(1); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..0800b71 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "lydia-discord-bot", + "version": "1.0.0", + "description": "Discord bot frontend for Ollama AI assistant", + "main": "index.js", + "scripts": { + "start": "node start.js", + "dev": "node --watch index.js", + "bot": "node index.js" + }, + "keywords": ["discord", "bot", "ollama", "ai", "assistant"], + "author": "", + "license": "MIT", + "dependencies": { + "discord.js": "^14.14.1", + "axios": "^1.6.2", + "toml": "^3.0.0", + "dotenv": "^16.3.1" + }, + "engines": { + "node": ">=16.0.0" + } +} \ No newline at end of file