Files
silicon-duck/discord.mjs
2026-01-11 23:23:53 +01:00

201 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ────────────────────────────────────────────────────────────────────────────────
// bot.js Discord bot rewritten for Node 25+ and Ollama (via run_prompt2)
// ────────────────────────────────────────────────────────────────────────────────
import { Client, GatewayIntentBits, Partials, PermissionsBitField } from 'discord.js';
import * as SECRET from '/srv/Projekt/dicsordbot/secret.mjs';
/* -------------------------------------------------------------------------- */
/* 1⃣ Utility run_prompt2 (Ollama helper) the same you gave me */
/* -------------------------------------------------------------------------- */
const MODEL = 'gpt-oss:20b';
/**
* Query the local Ollama server.
*
* @param {string} system_prompt Systemmessage that sets the persona.
* @param {string} model Model name.
* @param {Array<{role:string,content:string}>} pieces The chat messages.
* @param {object} options Optional Ollama options.
* @returns {Promise<{status:number, response?:object, error?:string}>}
*/
export async function run_prompt2(system_prompt, model, pieces, options = { num_ctx: 16384 }) {
const payload = {
model,
messages: [
{ role: 'system', content: system_prompt },
...pieces.map(entry => ({ role: 'user', content: entry }))
],
stream: false,
options,
};
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (response.ok) {
return { status: response.status, response: await response.json() };
} else {
return { status: response.status, error: await response.text() };
}
}
/* -------------------------------------------------------------------------- */
/* 2⃣ Discord bot configuration intents & partials */
/* -------------------------------------------------------------------------- */
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
],
partials: [Partials.Message, Partials.Channel], // safety
});
/* -------------------------------------------------------------------------- */
/* 3⃣ Secrets pulled from secret.mjs */
/* -------------------------------------------------------------------------- */
const { API_TOKEN, PERMISSIONS, SERVER_ID, CHANNEL_ID } = SECRET;
/* -------------------------------------------------------------------------- */
/* 4⃣ Allowlist only these user IDs may invoke the bot */
/* -------------------------------------------------------------------------- */
const ALLOW_LIST = new Set([
267085394548490241n,
310828422362431490n,
450834412049924097n,
277582984129806337n,
622871531881627648n,
]);
/* -------------------------------------------------------------------------- */
/* 5⃣ Helpers */
/* -------------------------------------------------------------------------- */
function splitDiscordMessage(text) {
const MAX = 1900; // buffer
const chunks = [];
let start = 0;
while (start < text.length) {
let end = Math.min(start + MAX, text.length);
const nl = text.lastIndexOf('\n', end);
const sp = text.lastIndexOf(' ', end);
if (nl > start) end = nl + 1;
else if (sp > start) end = sp + 1;
chunks.push(text.slice(start, end));
start = end;
}
return chunks;
}
/* -------------------------------------------------------------------------- */
/* 6⃣ Core message handler */
/* -------------------------------------------------------------------------- */
async function handleMessage(msg) {
if (msg.author.id === client.user.id) return; // ignore self
// Only process messages from the target guild & channel
if (msg.guild?.id !== SERVER_ID) return;
//if (msg.channel.id !== CHANNEL_ID) return;
// Allowlist check
/*
if (!ALLOW_LIST.has(BigInt(msg.author.id))) {
await msg.reply(
`Currently only people in the allow list may use this bot. If you want to participate, just let <@310828422362431490> know! <:astral_yak:1098185039805681755>`
);
return;
}
*/
const mention = `<@${client.user.id}>`;
if (!msg.content.includes(mention)) return; // not a mention
/* 5a Build the system prompt (Silicon Duck persona) */
const systemPrompt = `You are Silicon Duck, a helpful, cheerful, and generally excited creative Discord bot in the server 'Mikael Lövqvist's shenanigans'. You do NOT retain any history across messages. Your tag is <@${client.user.id}>, do not tag yourself in replies. The current channel is '${msg.channel.name}'. The user is ${msg.author.username} with tag <@${msg.author.id}>. You should not produce code examples unless explicitly asked. Do not make markdown tables, discord does not support those. You are running the model ${MODEL} using Ollama and OpenWebUI. Dial back emoji use.`;
/* 5b Check for special commands that use PERMISSIONS */
if (msg.content.startsWith('!create ')) {
// !create <channelname>
const name = msg.content.slice('!create '.length).trim();
if (!name) return msg.reply('Usage: `!create <channel-name>`');
try {
const newCh = await msg.guild.channels.create({
name,
type: 0, // TextChannel
permissionOverwrites: [
{
id: msg.guild.id, // @everyone
allow: Number(BigInt(PERMISSIONS)), // use the provided bitmask
},
],
});
await msg.reply(`✅ Created ${newCh.name} with the requested permissions.`);
} catch (err) {
console.error('Channelcreate error', err);
await msg.reply('❌ Could not create the channel.');
}
return; // finished command
}
/* 5c Build the system prompt (Silicon Duck persona) */
const userMessage = msg.content;
/* 5d Query Ollama via run_prompt2 */
let runResponse;
try {
const reply = await run_prompt2(systemPrompt, MODEL, [userMessage], { num_ctx: 16384 });
if (reply.status !== 200) {
console.error('Ollama error', reply.error);
await msg.reply('Sorry, I ran into an error while thinking.');
return;
}
runResponse = reply.response;
} catch (e) {
console.error('Error calling Ollama:', e);
await msg.reply('Sorry, I couldnt reach the AI service.');
return;
}
/* 5e Extract the answer text */
const answer = runResponse?.message?.content ?? '';
if (!answer) {
await msg.reply('Sorry, I didnt receive a reply from the AI.');
return;
}
/* 5f Send the answer back to Discord (chunked) */
const chunks = splitDiscordMessage(answer);
for (const chunk of chunks) await msg.channel.send(chunk);
console.log(`Answered to ${msg.author.username} (${msg.author.id}) in ${chunks.length} chunk(s).`);
}
/* -------------------------------------------------------------------------- */
/* 7⃣ Hook up Discord events */
/* -------------------------------------------------------------------------- */
client.once('clientReady', () => {
console.log(`✅ Logged in as ${client.user.tag}`);
});
client.on('messageCreate', handleMessage);
/* -------------------------------------------------------------------------- */
/* 8⃣ Start the bot */
/* -------------------------------------------------------------------------- */
client.login(API_TOKEN).catch(err => {
console.error('❌ Failed to login', err);
process.exit(1);
});