lydia_on_discord/index.js
2025-08-14 19:52:57 +02:00

271 lines
7.3 KiB
JavaScript

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;
async function executeEval(code, message) {
try {
const context = {
message,
client,
config,
console: {
log: (...args) => console.log("[EVAL]", ...args),
},
};
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 = [],
timeout,
) {
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: config.advanced.timeout,
},
);
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;
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";
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;
}
}
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,
config.advanced.timeout,
);
if (response.length > 2000) {
const chunks = [];
for (let i = 0; i < response.length; i += 1900) {
chunks.push(response.slice(i, i + 1900));
}
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. Error handling message:",
error,
);
}
});
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.");
process.exit(1);
}
client.login(config.discord.token).catch((error) => {
console.error("Failed to login to Discord:", error.message);
process.exit(1);
});