Files
gott/Services/TelegramService.cs
T
Meleki 0120bb8f73 Telegram: Bot=Session statt User-Level, Chat-Code-Refactoring
- Telegram-Config (Bot-Token, Webhook-Secret, ChatId) von User-Settings
  in sessions-Tabelle verschoben → jede Session hat ihren eigenen Bot
- Link-Code-System komplett entfernt (TelegramLinkRecord gelöscht)
- /start verknüpft automatisch, kein /link mehr nötig
- Alle Endpoints session-basiert: /api/sessions/{id}/telegram/*
- Webhook-URL pro Session: /api/telegram/webhook/{sessionId}

Refactoring: Doppelten Chat-Code eliminiert
- Neuer ChatService: PrepareContext, BuildHistoryFromDb, ResolveModelId
- Neues ChatContext-Model als gemeinsame Datenstruktur
- AnthropicService.ChatNonStreaming() für Telegram (statt eigenem Loop)
- TelegramService um ~180 Zeilen reduziert, kein eigener Claude-Loop mehr
- ChatEndpoint nutzt ChatService.PrepareContext() statt eigener Setup-Logik
- Model-Mapping nur noch an einer Stelle (3x → 1x)

Frontend
- Telegram-Section von Settings-Page in Session-Bearbeitungsseite verschoben
- Webhook-Secret readonly, wird einmalig generiert und persistent gespeichert
- Status wird automatisch geladen, Register/Delete je nach Webhook-Status
- Toast-Notification-System (showToast) in ui.js
- Link-Code-Modal und Telegram-Menüpunkt im Dashboard entfernt
- Telegram-Badge auf Session-Cards (konfiguriert/verbunden)

Bugfix: Bearer statt x-api-key für OAuth-Tokens im TelegramService
2026-03-03 21:46:59 +01:00

298 lines
11 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 string _anthropicToken;
private readonly IHttpClientFactory _httpFactory;
private readonly DatabaseService _db;
private readonly ToolService _toolService;
private readonly MemoryService _memoryService;
private readonly ChatService _chatService;
private readonly int _maxIterations;
public TelegramService(
string anthropicToken,
IHttpClientFactory httpFactory,
DatabaseService db,
ToolService toolService,
MemoryService memoryService,
ChatService chatService,
int maxIterations = 100)
{
_anthropicToken = anthropicToken;
_httpFactory = httpFactory;
_db = db;
_toolService = toolService;
_memoryService = memoryService;
_chatService = chatService;
_maxIterations = maxIterations;
}
// ── Bot-Token / Secret aus Session laden ──
public string GetBotTokenForSession(SessionRecord session)
{
return session?.TelegramBotToken;
}
public string GetWebhookSecretForSession(SessionRecord session)
{
return session?.TelegramWebhookSecret;
}
// ── Webhook-Update verarbeiten ──
public async Task HandleUpdate(JsonElement update, SessionRecord session)
{
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;
var botToken = session.TelegramBotToken;
if (string.IsNullOrEmpty(botToken)) return;
if (text.StartsWith("/start"))
{
await _db.SetSessionTelegramChatId(session.SessionId, chatId.ToString());
var sessionName = session.Name ?? session.SessionId;
await SendMessage(chatId, 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\n" +
"/unlink – Verknüpfung aufheben",
parseMode: "MarkdownV2");
return;
}
if (text.StartsWith("/"))
{
await HandleCommand(chatId, text, botToken, session);
return;
}
await HandleMessage(chatId, text, botToken, session);
}
// ── 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;
case "/unlink":
await _db.ClearSessionTelegramChatId(session.SessionId);
await SendMessage(chatId, botToken, "✅ Verknüpfung aufgehoben. Sende /start um erneut zu verbinden.");
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)
{
if (session.TelegramChatId != chatId.ToString())
{
await SendMessage(chatId, botToken, "❌ Bitte sende zuerst /start um dich mit dieser Session zu verbinden.");
return;
}
// ChatService für Setup nutzen (userId = null → kein Auth-Check, Session ist bereits authentifiziert via Webhook-Secret)
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
{
// History aus DB laden + User-Nachricht anhängen
var history = await _chatService.BuildHistoryFromDb(session.SessionId);
history.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = userText
});
// User-Nachricht in DB speichern
await _db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId,
Iteration = 0,
Role = "user",
Type = "text",
Content = userText
});
// AnthropicService für Non-Streaming-Chat nutzen
var anthropic = new AnthropicService(_httpFactory, _anthropicToken, _toolService, _db, _memoryService);
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.TelegramBotToken) || string.IsNullOrEmpty(session.TelegramChatId))
return;
var chatId = long.Parse(session.TelegramChatId);
await SendMessage(chatId, session.TelegramBotToken, 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 Session) ──
public async Task<string> RegisterWebhook(string baseUrl, string sessionId)
{
var session = await _db.GetSession(sessionId);
if (session == null || string.IsNullOrEmpty(session.TelegramBotToken))
return JsonSerializer.Serialize(new { ok = false, error = "Kein Bot-Token für diese Session konfiguriert" });
var webhookUrl = $"{baseUrl.TrimEnd('/')}/api/telegram/webhook/{sessionId}";
var client = _httpFactory.CreateClient();
var payload = new Dictionary<string, object>
{
["url"] = webhookUrl,
["allowed_updates"] = new[] { "message" }
};
if (!string.IsNullOrEmpty(session.TelegramWebhookSecret))
payload["secret_token"] = session.TelegramWebhookSecret;
var resp = await client.PostAsync(
$"https://api.telegram.org/bot{session.TelegramBotToken}/setWebhook",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
return await resp.Content.ReadAsStringAsync();
}
public async Task<string> GetWebhookInfo(string sessionId)
{
var session = await _db.GetSession(sessionId);
if (session == null || string.IsNullOrEmpty(session.TelegramBotToken))
return JsonSerializer.Serialize(new { ok = false, error = "Kein Bot-Token konfiguriert" });
var client = _httpFactory.CreateClient();
var resp = await client.GetAsync($"https://api.telegram.org/bot{session.TelegramBotToken}/getWebhookInfo");
return await resp.Content.ReadAsStringAsync();
}
public async Task<string> DeleteWebhook(string sessionId)
{
var session = await _db.GetSession(sessionId);
if (session == null || string.IsNullOrEmpty(session.TelegramBotToken))
return JsonSerializer.Serialize(new { ok = false, error = "Kein Bot-Token konfiguriert" });
var client = _httpFactory.CreateClient();
var resp = await client.PostAsync(
$"https://api.telegram.org/bot{session.TelegramBotToken}/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;
}
}