commit 62d8ae1034cff5fb1616a71373445407e621a967 Author: Mikael Lövqvist Date: Sun Jan 11 23:23:53 2026 +0100 Initial commit diff --git a/discord.mjs b/discord.mjs new file mode 100644 index 0000000..51c5960 --- /dev/null +++ b/discord.mjs @@ -0,0 +1,201 @@ +// ──────────────────────────────────────────────────────────────────────────────── +// 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 System‑message 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️⃣ Allow‑list – 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; + + // Allow‑list 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 + const name = msg.content.slice('!create '.length).trim(); + if (!name) return msg.reply('Usage: `!create `'); + 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 bit‑mask + }, + ], + }); + await msg.reply(`✅ Created ${newCh.name} with the requested permissions.`); + } catch (err) { + console.error('Channel‑create 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 couldn’t reach the AI service.'); + return; + } + + + /* 5e Extract the answer text */ + const answer = runResponse?.message?.content ?? ''; + if (!answer) { + await msg.reply('Sorry, I didn’t 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); +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2572a43 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "discord.js": "^14.25.1" + } +}