Anticlaw / src /bot.ts
Luis Milke
Add Telegram commands and fix dashboard crash
3e56aaa
import { Bot } from 'grammy';
import { env } from './config.js';
import { handleMessage } from './agent.js';
export const bot = new Bot(env.TELEGRAM_BOT_TOKEN);
// Security: Whitelist middleware
bot.use(async (ctx, next) => {
if (ctx.from?.id !== env.TELEGRAM_USER_ID) {
console.log(`Blocked interaction from unauthorized user ID: ${ctx.from?.id}`);
return; // Silently ignore as per constraints
}
await next();
});
import { getAllMemories } from './memory/db.js';
import os from 'os';
import { clearConversationHistory } from './agent.js';
import { currentModel, setModel } from './llm.js';
bot.command('memory', async (ctx) => {
const memories = await getAllMemories();
if (memories.length === 0) {
await ctx.reply('📭 Die Datenbank ist noch komplett leer.');
return;
}
let response = '🧠 **Gedächtnis-Datenbank:**\n\n';
for (const mem of memories) {
const dateStr = new Date(mem.timestamp).toLocaleString('de-DE');
response += `• \`${dateStr}\`: ${mem.content}\n`;
}
if (response.length > 4000) {
response = response.substring(0, 4000) + '\n... [Abgeschnitten]';
}
await ctx.reply(response, { parse_mode: 'Markdown' });
});
bot.command('start', async (ctx) => {
await ctx.reply('👋 Hallo! Ich bin Gravity Claw. Schreibe mir eine Nachricht oder nutze /help für eine Liste meiner Befehle.');
});
bot.command('help', async (ctx) => {
const helpText = `
🤖 **Gravity Claw Commands** 🤖
/help - Zeigt diese Hilfeseite
/memory - Zeigt alle persistenten Erinnerungen an
/clear - Löscht den aktuellen KI-Chatverlauf
/ping - Prüft die Latenz des Bots
/sysinfo - Zeigt Systeminformationen des Host-PCs an
/uptime - Zeigt, wie lange der Bot schon läuft
/id - Zeigt deine Telegram-ID
/date - Zeigt das aktuelle Datum und die Uhrzeit des Servers
/model [name] - Zeigt oder ändert das aktive Ollama KI-Modell
/echo [text] - Der Bot wiederholt deinen Text
/roll - Würfelt eine Zahl von 1 bis 100
/soul - Zeigt die aktuelle soul.md an (Persönlichkeit/Ziele)
/heartbeat - Zeigt die aktuelle heartbeat.md an (System-Status)
`;
await ctx.reply(helpText, { parse_mode: 'Markdown' });
});
bot.command('clear', async (ctx) => {
// Clear the current short-term conversation memory (keep system prompt)
clearConversationHistory();
await ctx.reply('🧹 Der aktuelle Chatverlauf wurde gelöscht. Ich fange frisch an!');
});
bot.command('ping', async (ctx) => {
const start = Date.now();
const msg = await ctx.reply('🏓 Pong!');
const diff = Date.now() - start;
await ctx.api.editMessageText(ctx.chat.id, msg.message_id, `🏓 Pong! Latenz: ${diff}ms`);
});
bot.command('sysinfo', async (ctx) => {
const cpu = os.cpus()[0]?.model || 'Unknown CPU';
const ramTotal = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2);
const ramFree = (os.freemem() / 1024 / 1024 / 1024).toFixed(2);
const platform = os.platform();
await ctx.reply(`💻 **Host System:**\n\nCPU: ${cpu}\nRAM: ${ramFree} GB frei von ${ramTotal} GB\nOS: ${platform}`, { parse_mode: 'Markdown' });
});
let startTime = Date.now();
bot.command('uptime', async (ctx) => {
const diff = Date.now() - startTime;
const hours = Math.floor(diff / 3600000);
const minutes = Math.floor((diff % 3600000) / 60000);
const seconds = Math.floor((diff % 60000) / 1000);
await ctx.reply(`⏳ Ich bin seit ${hours}h ${minutes}m ${seconds}s online.`);
});
bot.command('id', async (ctx) => {
await ctx.reply(`👤 Deine Telegram User ID lautet: \`${ctx.from?.id}\``, { parse_mode: 'Markdown' });
});
bot.command('date', async (ctx) => {
await ctx.reply(`📅 Server Zeit: ${new Date().toLocaleString('de-DE')}`);
});
bot.command('echo', async (ctx) => {
const text = ctx.message?.text?.replace('/echo ', '');
if (!text || text === '/echo') return;
await ctx.reply(text);
});
bot.command('roll', async (ctx) => {
const roll = Math.floor(Math.random() * 100) + 1;
await ctx.reply(`🎲 Du hast eine **${roll}** gewürfelt!`, { parse_mode: 'Markdown' });
});
bot.command('model', async (ctx) => {
const text = ctx.message?.text?.replace('/model', '').trim();
if (!text) {
await ctx.reply(`🧠 Aktuelles Modell: \`${currentModel}\`\n\nNutze \`/model <modellname>\` um es zu ändern (z.B. \`/model llama3.2\`).`, { parse_mode: 'Markdown' });
return;
}
setModel(text);
await ctx.reply(`✅ System-Modell wurde erfolgreich auf \`${text}\` geändert!`, { parse_mode: 'Markdown' });
});
bot.command('soul', async (ctx) => {
try {
const { executeReadFile } = await import('./tools/fs.js');
const content = await executeReadFile('soul.md');
if (content.includes('Error')) {
await ctx.reply(`📝 *soul.md nicht gefunden oder leer*\nIch erstelle gerade eine neue Identität beim nächsten Denken...`, { parse_mode: 'Markdown' });
} else {
const shortened = content.length > 3900 ? content.substring(0, 3900) + '...' : content;
await ctx.reply(`🌌 *Meine Soul (soul.md):*\n\n${shortened}`, { parse_mode: 'Markdown' });
}
} catch (e: any) {
await ctx.reply(`Fehler beim Lesen der Soul: ${e.message}`);
}
});
bot.command('heartbeat', async (ctx) => {
try {
const { executeReadFile } = await import('./tools/fs.js');
const content = await executeReadFile('heartbeat.md');
if (content.includes('Error')) {
await ctx.reply(`🫀 *heartbeat.md nicht gefunden*\nMein Herz hat im neuen Server noch nicht geschlagen...`, { parse_mode: 'Markdown' });
} else {
const shortened = content.length > 3900 ? content.substring(0, 3900) + '...' : content;
await ctx.reply(`🫀 *Aktueller System-Status (heartbeat.md):*\n\n${shortened}`, { parse_mode: 'Markdown' });
}
} catch (e: any) {
await ctx.reply(`Fehler beim Lesen des Heartbeats: ${e.message}`);
}
});
bot.on('message:text', async (ctx) => {
// Show typing indicator
await ctx.replyWithChatAction('typing');
try {
const responseText = await handleMessage(
ctx.message.text,
async (toolName) => {
let emoji = '🤔'; // Default thinking
if (toolName.includes('memory')) emoji = '✍';
else if (toolName.includes('search') || toolName.includes('browser')) emoji = '👀';
else if (toolName.includes('shell') || toolName.includes('file')) emoji = '👨‍💻';
else if (toolName.includes('time') || toolName.includes('schedule')) emoji = '⚡';
try {
// React to the user's message directly instead of sending a new text message
await ctx.react(emoji as any);
} catch (e) {
console.log(`Could not react with ${emoji} to message.`);
}
}
);
await ctx.reply(responseText);
} catch (error) {
console.error('Error handling message:', error);
await ctx.reply('❌ An error occurred while processing your request.');
}
});
bot.on('message_reaction', async (ctx) => {
const newReactions = ctx.messageReaction.new_reaction;
if (newReactions.length > 0) {
const reaction = newReactions.find(r => r.type === 'emoji') as any;
if (reaction && reaction.emoji) {
try {
// Mirror the user's reaction back onto the same message
await ctx.api.setMessageReaction(
ctx.chat?.id || env.TELEGRAM_USER_ID,
ctx.messageReaction.message_id,
[{ type: 'emoji', emoji: reaction.emoji }]
);
} catch (e) {
console.log(`Could not mirror reaction:`, e);
}
}
}
});
export const startBot = async () => {
const webhookUrl = process.env.WEBHOOK_URL;
if (webhookUrl) {
// Run in Webhook Mode (Production / Cloud)
try {
await bot.api.setWebhook(`${webhookUrl}/api/telegram-webhook`);
console.log(`🤖 Gravity Claw (@${(await bot.api.getMe()).username}) Webhook set to: ${webhookUrl}/api/telegram-webhook`);
console.log(`🔒 Secure mode active. Locked to user ID: ${env.TELEGRAM_USER_ID}`);
} catch (e: any) {
console.error(`❌ Failed to set Telegram Webhook (DNS/Network error):`, e.message);
console.log(`⚠️ Server will continue running for iOS App, but Telegram bot might be unresponsive limits.`);
}
} else {
// Run in Polling Mode (Local Dev)
bot.start({
onStart: (botInfo) => {
console.log(`🤖 Gravity Claw (@${botInfo.username}) started successfully!`);
console.log(`🔒 Secure mode active. Locked to user ID: ${env.TELEGRAM_USER_ID}`);
console.log(`⏳ Using polling mode (No webhook configured)`);
}
});
}
};