Files
gott/Services/TelegramService.cs
T
Meleki 011b26c37f feat(telegram): Zugriffskontrolle mit Chat-ID-Whitelist
- TelegramService: IsChatIdAllowed() prüft Owner + Whitelist
- /start von unbekannter ChatId: Zugriff verweigert + ChatId angezeigt
- Owner wird bei Zugriffsanfrage benachrichtigt (Username + ChatId)
- Alle Commands/Messages prüfen Whitelist vor Verarbeitung
- Neues Feld telegram_allowed_chat_ids in sessions-Tabelle
- Frontend: Input für erlaubte Chat-IDs in Session-Telegram-Settings
- API: allowedChatIds in Save/Get durchgereicht
2026-03-06 13:04:02 +01:00

361 lines
14 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;
}
// ── 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"))
{
if (!IsChatIdAllowed(session, chatId))
{
// Telegram-Username aus dem Update extrahieren
var username = "";
if (message.TryGetProperty("from", out var from))
{
if (from.TryGetProperty("username", out var uname))
username = uname.GetString() ?? "";
else if (from.TryGetProperty("first_name", out var fname))
username = fname.GetString() ?? "";
}
await SendMessage(chatId, botToken,
"🔒 Zugriff verweigert.\n\n" +
"Du bist nicht für diesen Bot freigegeben. " +
"Bitte kontaktiere den Inhaber und teile deine Chat-ID mit:\n\n" +
$"📋 Deine Chat-ID: `{chatId}`",
parseMode: "Markdown");
// Owner benachrichtigen (wenn chatId gesetzt)
if (!string.IsNullOrEmpty(session.TelegramChatId))
{
var ownerChatId = long.Parse(session.TelegramChatId);
var requesterInfo = string.IsNullOrEmpty(username) ? chatId.ToString() : $"@{username} ({chatId})";
await SendMessage(ownerChatId, botToken,
$"🔔 *Zugriffsanfrage*\n\n" +
$"Benutzer {EscapeMd(requesterInfo)} möchte diesen Bot nutzen\\.\n\n" +
$"Chat\\-ID: `{chatId}`\n\n" +
"Zum Freigeben: Chat\\-ID in den Session\\-Einstellungen unter _Erlaubte Chat\\-IDs_ hinzufügen\\.",
parseMode: "MarkdownV2");
}
return;
}
// Erste Verbindung: chatId als Owner setzen (wenn noch keine gesetzt)
if (string.IsNullOrEmpty(session.TelegramChatId))
{
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;
}
// Alle anderen Befehle/Nachrichten: Zugriff prüfen
if (!IsChatIdAllowed(session, chatId))
{
await SendMessage(chatId, botToken, "🔒 Du bist nicht für diesen Bot freigegeben. Sende /start für mehr Informationen.");
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 (!IsChatIdAllowed(session, chatId))
{
await SendMessage(chatId, botToken, "🔒 Du bist nicht für diesen Bot freigegeben. Sende /start für mehr Informationen.");
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 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 bool IsChatIdAllowed(SessionRecord session, long chatId)
{
var chatIdStr = chatId.ToString();
// Owner (die primäre verknüpfte chatId) ist immer erlaubt
if (session.TelegramChatId == chatIdStr) return true;
// Wenn noch kein Owner gesetzt und keine Whitelist → erster User wird Owner
if (string.IsNullOrEmpty(session.TelegramChatId) && string.IsNullOrEmpty(session.TelegramAllowedChatIds))
return true;
// Whitelist prüfen
if (!string.IsNullOrEmpty(session.TelegramAllowedChatIds))
{
var allowed = session.TelegramAllowedChatIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var id in allowed)
{
if (id == chatIdStr) return true;
}
}
return false;
}
private static string EscapeMd(string text)
{
if (string.IsNullOrEmpty(text)) return "";
foreach (var c in new[] { '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' })
text = text.Replace(c.ToString(), "\\" + c);
return text;
}
}