215 lines
6.4 KiB
JavaScript
215 lines
6.4 KiB
JavaScript
|
|
import { Telegraf } from 'telegraf';
|
||
|
|
import { config } from 'dotenv';
|
||
|
|
import axios from 'axios';
|
||
|
|
|
||
|
|
config();
|
||
|
|
|
||
|
|
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
||
|
|
const OPENCODE_API_URL = process.env.OPENCODE_API_URL || 'http://127.0.0.1:5050';
|
||
|
|
const ALLOWED_CHAT_ID = process.env.TELEGRAM_ALLOWED_CHAT_ID ? String(process.env.TELEGRAM_ALLOWED_CHAT_ID) : null;
|
||
|
|
const messageQueue = [];
|
||
|
|
let isProcessing = false;
|
||
|
|
let currentTask = 'Idle';
|
||
|
|
|
||
|
|
if (!BOT_TOKEN) {
|
||
|
|
console.error('Error: TELEGRAM_BOT_TOKEN not set in .env file');
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function getSession() {
|
||
|
|
try {
|
||
|
|
const r = await axios.get(`${OPENCODE_API_URL}/session`, { timeout: 10000 });
|
||
|
|
if (r.data && r.data.length > 0) {
|
||
|
|
return r.data[0].id;
|
||
|
|
}
|
||
|
|
} catch (e) {}
|
||
|
|
try {
|
||
|
|
const r = await axios.post(`${OPENCODE_API_URL}/session`, {}, { timeout: 10000 });
|
||
|
|
if (r.data && r.data.id) {
|
||
|
|
return r.data.id;
|
||
|
|
}
|
||
|
|
} catch (e) {}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function sendToOpencode(message) {
|
||
|
|
const sessionId = await getSession();
|
||
|
|
if (!sessionId) {
|
||
|
|
return "Error: Could not connect to opencode session.";
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const r = await axios.post(
|
||
|
|
`${OPENCODE_API_URL}/session/${sessionId}/message`,
|
||
|
|
{ parts: [{ type: "text", text: `[Telegram] ${message}` }] },
|
||
|
|
{ timeout: 1200000 }
|
||
|
|
);
|
||
|
|
if (r.data && r.data.parts) {
|
||
|
|
const textParts = r.data.parts
|
||
|
|
.filter(p => p.type === "text" && p.text)
|
||
|
|
.map(p => p.text);
|
||
|
|
return textParts.join("\n") || "Message sent, no text response.";
|
||
|
|
}
|
||
|
|
return "Message sent.";
|
||
|
|
} catch (error) {
|
||
|
|
if (error.code === 'ECONNREFUSED') {
|
||
|
|
return "Can't connect to opencode. Is it running?";
|
||
|
|
} else if (error.code === 'ETIMEDOUT') {
|
||
|
|
return "opencode took too long to respond. Please try again.";
|
||
|
|
}
|
||
|
|
return `Error: ${error.message}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function processQueue() {
|
||
|
|
while (messageQueue.length > 0) {
|
||
|
|
const item = messageQueue.shift();
|
||
|
|
isProcessing = true;
|
||
|
|
try {
|
||
|
|
const reply = await sendToOpencode(item.message);
|
||
|
|
await item.ctx.telegram.editMessageText(
|
||
|
|
item.chatId,
|
||
|
|
item.messageId,
|
||
|
|
null,
|
||
|
|
`🔄 Processed from queue\n\n${reply.substring(0, 4000)}`
|
||
|
|
);
|
||
|
|
} catch (e) {
|
||
|
|
await item.ctx.telegram.editMessageText(
|
||
|
|
item.chatId,
|
||
|
|
item.messageId,
|
||
|
|
null,
|
||
|
|
`Error: ${e.message}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
isProcessing = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const bot = new Telegraf(BOT_TOKEN);
|
||
|
|
|
||
|
|
bot.start((ctx) => {
|
||
|
|
ctx.reply(
|
||
|
|
'opencode-dispatch bot\n\n' +
|
||
|
|
'Send any message and opencode will process it.\n' +
|
||
|
|
`Server: ${OPENCODE_API_URL}`
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
bot.help((ctx) => {
|
||
|
|
ctx.reply(
|
||
|
|
'How to use:\n\n' +
|
||
|
|
'1. Make sure opencode is running\n' +
|
||
|
|
'2. Send me any message\n' +
|
||
|
|
'3. I\'ll forward it to opencode and relay the response\n\n' +
|
||
|
|
'Commands: /start, /help, /status, /working, /clear'
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
bot.command('status', async (ctx) => {
|
||
|
|
const sessionId = await getSession();
|
||
|
|
let healthy = false;
|
||
|
|
try {
|
||
|
|
const r = await axios.get(`${OPENCODE_API_URL}/global/health`, { timeout: 5000 });
|
||
|
|
healthy = r.ok;
|
||
|
|
} catch (e) {}
|
||
|
|
ctx.reply(
|
||
|
|
`Server: ${OPENCODE_API_URL}\n` +
|
||
|
|
`opencode: ${healthy ? '✅' : '❌'}\n` +
|
||
|
|
`Session: ${sessionId || 'none'}\n` +
|
||
|
|
`Queue: ${messageQueue.length} messages`
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
bot.command('clear', (ctx) => {
|
||
|
|
if (isProcessing) {
|
||
|
|
ctx.reply('❌ Can\'t clear queue while processing. Wait for current task to finish.');
|
||
|
|
} else {
|
||
|
|
messageQueue.length = 0;
|
||
|
|
ctx.reply('✅ Queue cleared.');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
bot.command('working', (ctx) => {
|
||
|
|
if (isProcessing) {
|
||
|
|
ctx.reply(`🔄 Currently working on:\n"${currentTask}"\n\nQueue: ${messageQueue.length} messages`);
|
||
|
|
} else {
|
||
|
|
ctx.reply('✅ Currently idle. No task in progress.');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
bot.on('text', async (ctx) => {
|
||
|
|
const chatId = String(ctx.chat.id);
|
||
|
|
if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) {
|
||
|
|
ctx.reply('❌ This bot is not authorized to respond to you.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const userMessage = ctx.message.text;
|
||
|
|
|
||
|
|
if (isProcessing) {
|
||
|
|
const sent = await ctx.reply(
|
||
|
|
'⏳ opencode is busy. Your message has been added to the queue.\n' +
|
||
|
|
'I\'ll respond when ready. Use /status to check queue position.'
|
||
|
|
);
|
||
|
|
messageQueue.push({
|
||
|
|
message: userMessage,
|
||
|
|
chatId: ctx.chat.id,
|
||
|
|
messageId: sent.message_id,
|
||
|
|
ctx: ctx
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
currentTask = userMessage.length > 50 ? userMessage.substring(0, 50) + '...' : userMessage;
|
||
|
|
const sent = await ctx.reply('🔄 Processing...');
|
||
|
|
isProcessing = true;
|
||
|
|
try {
|
||
|
|
const reply = await sendToOpencode(userMessage);
|
||
|
|
await ctx.telegram.editMessageText(
|
||
|
|
ctx.chat.id,
|
||
|
|
sent.message_id,
|
||
|
|
null,
|
||
|
|
reply.substring(0, 4000)
|
||
|
|
);
|
||
|
|
} catch (e) {
|
||
|
|
await ctx.telegram.editMessageText(
|
||
|
|
ctx.chat.id,
|
||
|
|
sent.message_id,
|
||
|
|
null,
|
||
|
|
`Error: ${e.message}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
isProcessing = false;
|
||
|
|
currentTask = 'Idle';
|
||
|
|
|
||
|
|
if (messageQueue.length > 0) {
|
||
|
|
processQueue();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
bot.on('voice', (ctx) => {
|
||
|
|
const chatId = String(ctx.chat.id);
|
||
|
|
if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) return;
|
||
|
|
ctx.reply('Voice messages not yet supported. Send text.');
|
||
|
|
});
|
||
|
|
|
||
|
|
bot.on('document', (ctx) => {
|
||
|
|
const chatId = String(ctx.chat.id);
|
||
|
|
if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) return;
|
||
|
|
ctx.reply('File handling not yet supported. Send text.');
|
||
|
|
});
|
||
|
|
|
||
|
|
bot.on('photo', (ctx) => {
|
||
|
|
const chatId = String(ctx.chat.id);
|
||
|
|
if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) return;
|
||
|
|
ctx.reply('Image handling not yet supported. Send text.');
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('opencode-dispatch bot starting...');
|
||
|
|
console.log(`Connecting to opencode at: ${OPENCODE_API_URL}`);
|
||
|
|
console.log('Press Ctrl+C to stop');
|
||
|
|
|
||
|
|
bot.launch();
|
||
|
|
|
||
|
|
process.once('SIGINT', () => bot.stop('SIGINT'));
|
||
|
|
process.once('SIGTERM', () => bot.stop('SIGTERM'));
|