Files
gott/Services/TelegramService.cs
T
Meleki ec73ccd71e Telegram Bots als eigenständige Entität mit eigener Page
- Neue Tabelle 'telegram_bots' (bot_id, name, bot_token, webhook_secret, is_public)
- Automatische Migration bestehender Session-Telegram-Configs in telegram_bots
- Sessions verweisen via telegram_bot_id auf Bot (mehrere Sessions pro Bot möglich)
- Webhook-URL: /api/telegram/webhook/{botId} statt {sessionId}
- Eigene Telegram-Bot-Page (wie Agent/Cronjob/Session) statt Modal
- Bot-Page: Name, Token, Secret, Webhook-Verwaltung (registrieren/status/entfernen), Löschen
- Dashboard: Telegram Bots Section mit Cards (Name, maskierter Token, Session-Count)
- Session-Page: Bot-Picker (wie Agent-Picker) statt Token-Input, Chat-ID readonly + Unlink-Button
- Public-Flag: Bot kann von anderen Usern in ihren Sessions ausgewählt werden
- Vereinfacht: 1 Session = max. 1 Telegram Chat, allowed_chat_ids entfällt
- /start zeigt Chat-ID an, /unlink entfernt (Unlink nur über Web-UI)
- Nachrichten-Routing: Bot + Chat-ID → Session Lookup
2026-03-08 19:27:05 +01:00

278 lines
9.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
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.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Gott.Models;
namespace Gott.Services;
public class TelegramService
{
private readonly IHttpClientFactory _httpFactory;
private readonly DatabaseService _db;
private readonly ChatService _chatService;
private readonly AnthropicService _anthropic;
private readonly int _maxIterations;
public TelegramService(
IHttpClientFactory httpFactory,
DatabaseService db,
ChatService chatService,
AnthropicService anthropic,
int maxIterations = 100)
{
_httpFactory = httpFactory;
_db = db;
_chatService = chatService;
_anthropic = anthropic;
_maxIterations = maxIterations;
}
// ── Webhook-Update verarbeiten ──
public async Task HandleUpdate(JsonElement update, TelegramBotRecord bot)
{
if (!update.TryGetProperty("message", out var message)) return;
if (!message.TryGetProperty("chat", out var chat)) return;
if (!message.TryGetProperty("text", out var textEl)) return;
var chatId = chat.GetProperty("id").GetInt64();
var text = textEl.GetString()?.Trim() ?? "";
if (string.IsNullOrEmpty(text)) return;
// Session für diesen Chat finden (1 Session = max 1 Chat)
var session = await _db.FindSessionForTelegramChat(bot.BotId, chatId);
if (text.StartsWith("/start"))
{
await HandleStart(chatId, bot, session);
return;
}
if (session == null)
{
await SendMessage(chatId, bot.BotToken,
$"🔒 Keine Session mit diesem Chat verknüpft.\n\nDeine Chat-ID: `{chatId}`\nBitte weise diese Chat-ID im Web-UI einer Session zu.",
parseMode: "Markdown");
return;
}
if (text.StartsWith("/"))
{
await HandleCommand(chatId, text, bot.BotToken, session);
return;
}
await HandleMessage(chatId, text, bot.BotToken, session);
}
private async Task HandleStart(long chatId, TelegramBotRecord bot, SessionRecord session)
{
if (session != null)
{
// Bereits verknüpft – Willkommensinfo
var sessionName = session.Name ?? session.SessionId;
await SendMessage(chatId, bot.BotToken,
$"✅ *Verbunden mit Session: {EscapeMd(sessionName)}*\n\n" +
"Du kannst jetzt einfach drauflos schreiben\\!\n\n" +
"Befehle:\n" +
"/clear – History archivieren\n" +
"/status – Aktuelle Session\\-Info",
parseMode: "MarkdownV2");
return;
}
// Nicht verknüpft – Chat-ID anzeigen
await SendMessage(chatId, bot.BotToken,
$"📋 Deine Chat-ID: `{chatId}`\n\n" +
"Bitte weise diese Chat-ID im Web-UI einer Session zu, " +
"die diesen Bot verwendet.",
parseMode: "Markdown");
}
// ── Commands ──
private async Task HandleCommand(long chatId, string text, string botToken, SessionRecord session)
{
var parts = text.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var cmd = parts[0].ToLowerInvariant().Split('@')[0];
switch (cmd)
{
case "/clear":
var archive = await _db.CreateArchive(session.SessionId);
await SendMessage(chatId, botToken, archive == null
? "History ist bereits leer."
: $"✅ History archiviert ({archive.MessageCount} Nachrichten).");
break;
case "/status":
var agent = !string.IsNullOrEmpty(session.AgentId) ? await _db.GetAgent(session.AgentId) : null;
var sb = new StringBuilder("📊 *Status:*\n\n");
sb.AppendLine($"Session: *{EscapeMd(session.Name ?? session.SessionId)}*");
if (agent != null)
sb.AppendLine($"Agent: *{EscapeMd(agent.Name)}* \\({EscapeMd(agent.Model)}\\)");
await SendMessage(chatId, botToken, sb.ToString(), parseMode: "MarkdownV2");
break;
default:
await SendMessage(chatId, botToken, "❓ Unbekannter Befehl. Sende /start für Hilfe.");
break;
}
}
// ── Nachricht an Claude weiterleiten ──
private async Task HandleMessage(long chatId, string userText, string botToken, SessionRecord session)
{
var ctx = await _chatService.PrepareContext(session.SessionId, null, _maxIterations);
if (ctx == null)
{
await SendMessage(chatId, botToken, "❌ Kein Agent konfiguriert. Bitte weise der Session im Web-UI einen Agent zu.");
return;
}
await SendChatAction(chatId, botToken, "typing");
try
{
var history = await _chatService.BuildHistoryFromDb(session.SessionId);
history.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = userText
});
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId,
Iteration = 0,
Role = "user",
Type = "text",
Content = userText
});
var reply = await _anthropic.ChatNonStreaming(ctx, history, onIteration: async (iteration) =>
{
if (iteration % 3 == 0)
await SendChatAction(chatId, botToken, "typing");
});
await SendLongMessage(chatId, botToken, reply);
}
catch (Exception ex)
{
await SendMessage(chatId, botToken, $"❌ Fehler: {ex.Message}");
}
}
// ── Notification senden (für Cronjobs) ──
public async Task SendNotification(string sessionId, string text)
{
var session = await _db.GetSession(sessionId);
if (session == null || string.IsNullOrEmpty(session.TelegramBotId) || string.IsNullOrEmpty(session.TelegramChatId))
return;
var bot = await _db.GetTelegramBot(session.TelegramBotId);
if (bot == null || string.IsNullOrEmpty(bot.BotToken)) return;
var chatId = long.Parse(session.TelegramChatId);
await SendMessage(chatId, bot.BotToken, text);
}
// ── Telegram Bot API ──
public async Task SendMessage(long chatId, string botToken, string text, string parseMode = null)
{
var client = _httpFactory.CreateClient();
var payload = new Dictionary<string, object>
{
["chat_id"] = chatId,
["text"] = text
};
if (!string.IsNullOrEmpty(parseMode))
payload["parse_mode"] = parseMode;
await client.PostAsync(
$"https://api.telegram.org/bot{botToken}/sendMessage",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
}
public async Task SendLongMessage(long chatId, string botToken, string text)
{
const int maxLen = 4096;
if (text.Length <= maxLen)
{
await SendMessage(chatId, botToken, text);
return;
}
for (var i = 0; i < text.Length; i += maxLen)
{
var chunk = text.Substring(i, Math.Min(maxLen, text.Length - i));
await SendMessage(chatId, botToken, chunk);
await Task.Delay(100);
}
}
public async Task SendChatAction(long chatId, string botToken, string action)
{
var client = _httpFactory.CreateClient();
await client.PostAsync(
$"https://api.telegram.org/bot{botToken}/sendChatAction",
new StringContent(JsonSerializer.Serialize(new { chat_id = chatId, action }), Encoding.UTF8, "application/json"));
}
// ── Webhook-Verwaltung (pro Bot) ──
public async Task<string> RegisterWebhook(string baseUrl, TelegramBotRecord bot)
{
var webhookUrl = $"{baseUrl.TrimEnd('/')}/api/telegram/webhook/{bot.BotId}";
var client = _httpFactory.CreateClient();
var payload = new Dictionary<string, object>
{
["url"] = webhookUrl,
["allowed_updates"] = new[] { "message" }
};
if (!string.IsNullOrEmpty(bot.WebhookSecret))
payload["secret_token"] = bot.WebhookSecret;
var resp = await client.PostAsync(
$"https://api.telegram.org/bot{bot.BotToken}/setWebhook",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
return await resp.Content.ReadAsStringAsync();
}
public async Task<string> GetWebhookInfo(TelegramBotRecord bot)
{
var client = _httpFactory.CreateClient();
var resp = await client.GetAsync($"https://api.telegram.org/bot{bot.BotToken}/getWebhookInfo");
return await resp.Content.ReadAsStringAsync();
}
public async Task<string> DeleteWebhook(TelegramBotRecord bot)
{
var client = _httpFactory.CreateClient();
var resp = await client.PostAsync(
$"https://api.telegram.org/bot{bot.BotToken}/deleteWebhook",
new StringContent("{}", Encoding.UTF8, "application/json"));
return await resp.Content.ReadAsStringAsync();
}
// ── Helper ──
private static string EscapeMd(string text)
{
if (string.IsNullOrEmpty(text)) return "";
foreach (var c in new[] { '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' })
text = text.Replace(c.ToString(), "\\" + c);
return text;
}
}