271 lines
7.3 KiB
JavaScript
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);
|
|
});
|