diff --git a/Endpoints/TelegramEndpoint.cs b/Endpoints/TelegramEndpoint.cs index 6014b63..34af9ab 100644 --- a/Endpoints/TelegramEndpoint.cs +++ b/Endpoints/TelegramEndpoint.cs @@ -13,29 +13,33 @@ public static class TelegramEndpoint { public static void MapTelegramEndpoints(this IEndpointRouteBuilder app) { - // Webhook pro Session (von Telegram aufgerufen – keine Cookie-Auth!) - app.MapPost("/api/telegram/webhook/{sessionId}", HandleWebhook); + // Webhook pro Bot (von Telegram aufgerufen – keine Cookie-Auth!) + app.MapPost("/api/telegram/webhook/{botId}", HandleWebhook); - // Session Telegram-Config speichern - app.MapPut("/api/sessions/{sessionId}/telegram", HandleSaveTelegram); + // ── Telegram Bot CRUD ── + app.MapGet("/api/telegram/bots", HandleGetBots); + app.MapGet("/api/telegram/bots/available", HandleGetAvailableBots); + app.MapPost("/api/telegram/bots", HandleCreateBot); + app.MapGet("/api/telegram/bots/{botId}", HandleGetBot); + app.MapPut("/api/telegram/bots/{botId}", HandleUpdateBot); + app.MapDelete("/api/telegram/bots/{botId}", HandleDeleteBot); - // Session Telegram-Config abrufen - app.MapGet("/api/sessions/{sessionId}/telegram", HandleGetTelegram); + // ── Webhook-Verwaltung (pro Bot) ── + app.MapPost("/api/telegram/bots/{botId}/register-webhook", HandleRegisterWebhook); + app.MapGet("/api/telegram/bots/{botId}/webhook-info", HandleWebhookInfo); + app.MapDelete("/api/telegram/bots/{botId}/webhook", HandleDeleteWebhook); - // Webhook registrieren (pro Session) - app.MapPost("/api/sessions/{sessionId}/telegram/register-webhook", HandleRegisterWebhook); - - // Webhook-Info (pro Session) - app.MapGet("/api/sessions/{sessionId}/telegram/webhook-info", HandleWebhookInfo); - - // Webhook löschen (pro Session) - app.MapDelete("/api/sessions/{sessionId}/telegram/webhook", HandleDeleteWebhook); + // ── Session Telegram-Config ── + app.MapGet("/api/sessions/{sessionId}/telegram", HandleGetSessionTelegram); + app.MapPut("/api/sessions/{sessionId}/telegram", HandleSaveSessionTelegram); } - private static async Task HandleWebhook(string sessionId, HttpRequest req, HttpResponse res, TelegramService telegram, DatabaseService db) + // ── Webhook Handler ── + + private static async Task HandleWebhook(string botId, HttpRequest req, HttpResponse res, TelegramService telegram, DatabaseService db) { - var session = await db.GetSession(sessionId); - if (session == null || string.IsNullOrEmpty(session.TelegramBotToken)) + var bot = await db.GetTelegramBot(botId); + if (bot == null) { res.StatusCode = 404; return; @@ -43,7 +47,7 @@ public static class TelegramEndpoint // Secret-Token prüfen var secret = req.Headers["X-Telegram-Bot-Api-Secret-Token"].ToString(); - if (!string.IsNullOrEmpty(session.TelegramWebhookSecret) && secret != session.TelegramWebhookSecret) + if (!string.IsNullOrEmpty(bot.WebhookSecret) && secret != bot.WebhookSecret) { res.StatusCode = 403; return; @@ -52,11 +56,10 @@ public static class TelegramEndpoint try { var body = await JsonSerializer.DeserializeAsync(req.Body); - // Fire-and-forget – Telegram erwartet schnelle 200 OK Antwort _ = Task.Run(async () => { - try { await telegram.HandleUpdate(body, session); } - catch (Exception ex) { Console.Error.WriteLine($"[Telegram] Fehler für Session {sessionId}: {ex.Message}"); } + try { await telegram.HandleUpdate(body, bot); } + catch (Exception ex) { Console.Error.WriteLine($"[Telegram] Fehler für Bot {bot.Name}: {ex.Message}"); } }); } catch @@ -67,27 +70,203 @@ public static class TelegramEndpoint res.StatusCode = 200; } - private static async Task HandleSaveTelegram(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db) + // ── Bot CRUD ── + + private static async Task HandleGetBots(HttpRequest req, HttpResponse res, DatabaseService db) { var userId = req.HttpContext.GetUserId(); - var session = await db.GetSession(sessionId); - if (session == null || session.UserId != userId) + var bots = await db.GetTelegramBots(userId); + var result = new System.Collections.Generic.List(); + foreach (var bot in bots) + { + var sessionCount = await db.GetTelegramBotSessionCount(bot.BotId); + result.Add(new + { + botId = bot.BotId, + name = bot.Name, + botToken = bot.BotToken, + webhookSecret = bot.WebhookSecret, + isPublic = bot.IsPublic, + sessionCount, + createdAt = bot.CreatedAt, + updatedAt = bot.UpdatedAt + }); + } + await res.WriteAsJsonAsync(result); + } + + private static async Task HandleGetBot(string botId, HttpRequest req, HttpResponse res, DatabaseService db) + { + var userId = req.HttpContext.GetUserId(); + var bot = await db.GetTelegramBot(botId); + if (bot == null || bot.UserId != userId) { res.StatusCode = 404; - await res.WriteAsJsonAsync(new { error = "Session nicht gefunden" }); + await res.WriteAsJsonAsync(new { error = "Bot nicht gefunden" }); + return; + } + + var sessionCount = await db.GetTelegramBotSessionCount(bot.BotId); + await res.WriteAsJsonAsync(new + { + botId = bot.BotId, + name = bot.Name, + botToken = bot.BotToken, + webhookSecret = bot.WebhookSecret, + isPublic = bot.IsPublic, + sessionCount, + createdAt = bot.CreatedAt, + updatedAt = bot.UpdatedAt + }); + } + + private static async Task HandleCreateBot(HttpRequest req, HttpResponse res, DatabaseService db) + { + var userId = req.HttpContext.GetUserId(); + var body = await JsonSerializer.DeserializeAsync(req.Body); + + var name = body.TryGetProperty("name", out var n) ? n.GetString()?.Trim() : null; + var botToken = body.TryGetProperty("botToken", out var t) ? t.GetString()?.Trim() : null; + + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(botToken)) + { + res.StatusCode = 400; + await res.WriteAsJsonAsync(new { error = "Name und Bot-Token sind erforderlich" }); + return; + } + + var bot = await db.CreateTelegramBot(userId, name, botToken); + await res.WriteAsJsonAsync(new + { + botId = bot.BotId, + name = bot.Name, + botToken = bot.BotToken, + webhookSecret = bot.WebhookSecret, + createdAt = bot.CreatedAt + }); + } + + private static async Task HandleUpdateBot(string botId, HttpRequest req, HttpResponse res, DatabaseService db) + { + var userId = req.HttpContext.GetUserId(); + var bot = await db.GetTelegramBot(botId); + if (bot == null || bot.UserId != userId) + { + res.StatusCode = 404; + await res.WriteAsJsonAsync(new { error = "Bot nicht gefunden" }); return; } var body = await JsonSerializer.DeserializeAsync(req.Body); - var botToken = body.TryGetProperty("botToken", out var t) ? t.GetString() : null; - var webhookSecret = body.TryGetProperty("webhookSecret", out var s) ? s.GetString() : null; - var allowedChatIds = body.TryGetProperty("allowedChatIds", out var a) ? a.GetString() : session.TelegramAllowedChatIds; + var name = body.TryGetProperty("name", out var n) ? n.GetString()?.Trim() : bot.Name; + var botToken = body.TryGetProperty("botToken", out var t) ? t.GetString()?.Trim() : bot.BotToken; + var isPublic = body.TryGetProperty("isPublic", out var p) ? p.GetBoolean() : bot.IsPublic; - await db.UpdateSessionTelegram(sessionId, botToken, webhookSecret, session.TelegramChatId, allowedChatIds); + await db.UpdateTelegramBot(botId, name, botToken, isPublic); await res.WriteAsJsonAsync(new { ok = true }); } - private static async Task HandleGetTelegram(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db) + private static async Task HandleDeleteBot(string botId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram) + { + var userId = req.HttpContext.GetUserId(); + var bot = await db.GetTelegramBot(botId); + if (bot == null || bot.UserId != userId) + { + res.StatusCode = 404; + await res.WriteAsJsonAsync(new { error = "Bot nicht gefunden" }); + return; + } + + // Webhook bei Telegram löschen + try { await telegram.DeleteWebhook(bot); } catch { /* ignore */ } + + await db.DeleteTelegramBot(botId); + await res.WriteAsJsonAsync(new { ok = true }); + } + + // ── Webhook-Verwaltung ── + + private static async Task HandleRegisterWebhook(string botId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram) + { + var userId = req.HttpContext.GetUserId(); + var bot = await db.GetTelegramBot(botId); + if (bot == null || bot.UserId != userId) + { + res.StatusCode = 404; + await res.WriteAsJsonAsync(new { error = "Bot nicht gefunden" }); + return; + } + + var body = await JsonSerializer.DeserializeAsync(req.Body); + if (!body.TryGetProperty("baseUrl", out var urlEl)) + { + res.StatusCode = 400; + await res.WriteAsJsonAsync(new { error = "baseUrl required" }); + return; + } + + var result = await telegram.RegisterWebhook(urlEl.GetString(), bot); + res.ContentType = "application/json"; + await res.WriteAsync(result); + } + + private static async Task HandleWebhookInfo(string botId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram) + { + var userId = req.HttpContext.GetUserId(); + var bot = await db.GetTelegramBot(botId); + if (bot == null || bot.UserId != userId) + { + res.StatusCode = 404; + return; + } + + var result = await telegram.GetWebhookInfo(bot); + res.ContentType = "application/json"; + await res.WriteAsync(result); + } + + private static async Task HandleDeleteWebhook(string botId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram) + { + var userId = req.HttpContext.GetUserId(); + var bot = await db.GetTelegramBot(botId); + if (bot == null || bot.UserId != userId) + { + res.StatusCode = 404; + return; + } + + var result = await telegram.DeleteWebhook(bot); + res.ContentType = "application/json"; + await res.WriteAsync(result); + } + + // ── Verfügbare Bots (eigene + public) ── + + private static async Task HandleGetAvailableBots(HttpRequest req, HttpResponse res, DatabaseService db) + { + var userId = req.HttpContext.GetUserId(); + var bots = await db.GetAvailableTelegramBots(userId); + var result = new System.Collections.Generic.List(); + foreach (var bot in bots) + { + var isOwner = bot.UserId == userId; + result.Add(new + { + botId = bot.BotId, + name = bot.Name, + isOwner, + isPublic = bot.IsPublic, + createdAt = bot.CreatedAt, + // Token nur für eigene Bots anzeigen + botToken = isOwner ? bot.BotToken : null + }); + } + await res.WriteAsJsonAsync(result); + } + + // ── Session Telegram-Config ── + + private static async Task HandleGetSessionTelegram(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db) { var userId = req.HttpContext.GetUserId(); var session = await db.GetSession(sessionId); @@ -100,15 +279,13 @@ public static class TelegramEndpoint await res.WriteAsJsonAsync(new { - botToken = session.TelegramBotToken ?? "", - webhookSecret = session.TelegramWebhookSecret ?? "", + telegramBotId = session.TelegramBotId ?? "", chatId = session.TelegramChatId ?? "", - allowedChatIds = session.TelegramAllowedChatIds ?? "", - configured = !string.IsNullOrEmpty(session.TelegramBotToken) + configured = !string.IsNullOrEmpty(session.TelegramBotId) }); } - private static async Task HandleRegisterWebhook(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram) + private static async Task HandleSaveSessionTelegram(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db) { var userId = req.HttpContext.GetUserId(); var session = await db.GetSession(sessionId); @@ -120,45 +297,18 @@ public static class TelegramEndpoint } var body = await JsonSerializer.DeserializeAsync(req.Body); - if (!body.TryGetProperty("baseUrl", out var urlEl)) + var botId = body.TryGetProperty("telegramBotId", out var b) ? b.GetString() : null; + var chatId = body.TryGetProperty("chatId", out var c) ? c.GetString() : session.TelegramChatId; + + // Bot-Zuordnung setzen + if (botId != null) { - res.StatusCode = 400; - await res.WriteAsJsonAsync(new { error = "baseUrl required" }); - return; + await db.SetSessionTelegramBot(sessionId, string.IsNullOrEmpty(botId) ? null : botId); } - var result = await telegram.RegisterWebhook(urlEl.GetString(), sessionId); - res.ContentType = "application/json"; - await res.WriteAsync(result); - } + // Chat-ID aktualisieren (leerer String → NULL) + await db.UpdateSessionTelegramChat(sessionId, string.IsNullOrEmpty(chatId) ? null : chatId); - private static async Task HandleWebhookInfo(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram) - { - var userId = req.HttpContext.GetUserId(); - var session = await db.GetSession(sessionId); - if (session == null || session.UserId != userId) - { - res.StatusCode = 404; - return; - } - - var result = await telegram.GetWebhookInfo(sessionId); - res.ContentType = "application/json"; - await res.WriteAsync(result); - } - - private static async Task HandleDeleteWebhook(string sessionId, HttpRequest req, HttpResponse res, DatabaseService db, TelegramService telegram) - { - var userId = req.HttpContext.GetUserId(); - var session = await db.GetSession(sessionId); - if (session == null || session.UserId != userId) - { - res.StatusCode = 404; - return; - } - - var result = await telegram.DeleteWebhook(sessionId); - res.ContentType = "application/json"; - await res.WriteAsync(result); + await res.WriteAsJsonAsync(new { ok = true }); } } diff --git a/Models/SessionRecord.cs b/Models/SessionRecord.cs index ea1dfb8..483a428 100644 --- a/Models/SessionRecord.cs +++ b/Models/SessionRecord.cs @@ -14,10 +14,8 @@ public class SessionRecord public string CustomCommands { get; set; } public string UserId { get; set; } public bool IsDeleted { get; set; } - public string TelegramBotToken { get; set; } - public string TelegramWebhookSecret { get; set; } + public string TelegramBotId { get; set; } public string TelegramChatId { get; set; } - public string TelegramAllowedChatIds { get; set; } public double AvatarScale { get; set; } = 1.0; public double AvatarOffsetX { get; set; } public double AvatarOffsetY { get; set; } diff --git a/Models/TelegramBotRecord.cs b/Models/TelegramBotRecord.cs new file mode 100644 index 0000000..da96c70 --- /dev/null +++ b/Models/TelegramBotRecord.cs @@ -0,0 +1,21 @@ +namespace Gott.Models; + +public class TelegramBotRecord +{ + public long Id { get; set; } + public string BotId { get; set; } + public string UserId { get; set; } + public string Name { get; set; } + public string BotToken { get; set; } + public string WebhookSecret { get; set; } + public bool IsPublic { get; set; } + public string CreatedAt { get; set; } + public string UpdatedAt { get; set; } +} + +public class TelegramBotWithStats +{ + public TelegramBotRecord Bot { get; set; } + public int SessionCount { get; set; } + public bool WebhookActive { get; set; } +} diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index a03f025..7127c65 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -167,6 +167,7 @@ public class DatabaseService TryAddColumn(conn, "sessions", "telegram_webhook_secret", "TEXT"); TryAddColumn(conn, "sessions", "telegram_chat_id", "TEXT"); TryAddColumn(conn, "sessions", "telegram_allowed_chat_ids", "TEXT"); + TryAddColumn(conn, "sessions", "telegram_bot_id", "TEXT"); TryAddColumn(conn, "agents", "user_id", "TEXT"); TryAddColumn(conn, "users", "is_admin", "INTEGER NOT NULL DEFAULT 0"); TryAddColumn(conn, "users", "persona", "TEXT"); @@ -227,6 +228,25 @@ public class DatabaseService CREATE INDEX IF NOT EXISTS idx_telegram_links_code ON telegram_links(link_code); "); + conn.Execute(@" + CREATE TABLE IF NOT EXISTS telegram_bots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bot_id TEXT NOT NULL UNIQUE, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + bot_token TEXT NOT NULL, + webhook_secret TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_telegram_bots_user ON telegram_bots(user_id); + "); + + TryAddColumn(conn, "telegram_bots", "is_public", "INTEGER NOT NULL DEFAULT 0"); + + // Migration: Bestehende Session-Telegram-Daten in telegram_bots überführen + MigrateTelegramBotsFromSessions(conn); + conn.Execute(@" CREATE TABLE IF NOT EXISTS cronjobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -578,7 +598,7 @@ public class DatabaseService MessageCount = activeCount, ArchiveMessageCount = totalCount - activeCount, TotalMessageCount = totalCount, - TelegramConfigured = !string.IsNullOrEmpty(s.TelegramBotToken), + TelegramConfigured = !string.IsNullOrEmpty(s.TelegramBotId), TelegramLinked = !string.IsNullOrEmpty(s.TelegramChatId) }); } @@ -1147,37 +1167,140 @@ public class DatabaseService await conn.ExecuteAsync("DELETE FROM users WHERE user_id = @userId", new { userId }); } - // ── Telegram (Session-basiert) ── + // ── Telegram Bots ── - public async Task GetSessionByTelegramChatId(long chatId) + private void MigrateTelegramBotsFromSessions(SqliteConnection conn) { - using var conn = CreateConnection(); - return await conn.QuerySingleOrDefaultAsync( - "SELECT * FROM sessions WHERE telegram_chat_id = @chatId AND is_deleted = 0", - new { chatId = chatId.ToString() }); + // Prüfe ob es Sessions mit bot_token gibt, die noch kein telegram_bot_id haben + var sessions = conn.Query( + "SELECT session_id, user_id, name, telegram_bot_token, telegram_webhook_secret FROM sessions WHERE telegram_bot_token IS NOT NULL AND telegram_bot_token != '' AND (telegram_bot_id IS NULL OR telegram_bot_id = '')").ToList(); + + if (sessions.Count == 0) return; + + var now = DateTime.UtcNow.ToString("o"); + foreach (var s in sessions) + { + string token = s.telegram_bot_token; + string userId = s.user_id ?? ""; + string sessionName = s.name ?? "Unbenannt"; + string secret = s.telegram_webhook_secret ?? Guid.NewGuid().ToString("N"); + + // Prüfe ob für diesen Token bereits ein Bot existiert + var existing = conn.QuerySingleOrDefault( + "SELECT bot_id FROM telegram_bots WHERE bot_token = @token", new { token }); + + string botId; + if (existing != null) + { + botId = existing.bot_id; + } + else + { + botId = Guid.NewGuid().ToString(); + conn.Execute(@" + INSERT INTO telegram_bots (bot_id, user_id, name, bot_token, webhook_secret, created_at, updated_at) + VALUES (@botId, @userId, @name, @token, @secret, @now, @now)", + new { botId, userId, name = $"Bot von {sessionName}", token, secret, now }); + } + + conn.Execute( + "UPDATE sessions SET telegram_bot_id = @botId, updated_at = @now WHERE session_id = @sessionId", + new { botId, sessionId = (string)s.session_id, now }); + } } - public async Task UpdateSessionTelegram(string sessionId, string botToken, string webhookSecret, string chatId, string allowedChatIds = null) + public async Task> GetTelegramBots(string userId) { using var conn = CreateConnection(); + return await conn.QueryAsync( + "SELECT * FROM telegram_bots WHERE user_id = @userId ORDER BY created_at DESC", + new { userId }); + } + + public async Task> GetAvailableTelegramBots(string userId) + { + using var conn = CreateConnection(); + return await conn.QueryAsync( + "SELECT * FROM telegram_bots WHERE user_id = @userId OR is_public = 1 ORDER BY created_at DESC", + new { userId }); + } + + public async Task GetTelegramBot(string botId) + { + using var conn = CreateConnection(); + return await conn.QuerySingleOrDefaultAsync( + "SELECT * FROM telegram_bots WHERE bot_id = @botId", + new { botId }); + } + + public async Task CreateTelegramBot(string userId, string name, string botToken) + { + using var conn = CreateConnection(); + var botId = Guid.NewGuid().ToString(); + var secret = Guid.NewGuid().ToString("N"); + var now = DateTime.UtcNow.ToString("o"); await conn.ExecuteAsync(@" - UPDATE sessions SET telegram_bot_token = @botToken, telegram_webhook_secret = @webhookSecret, - telegram_chat_id = @chatId, telegram_allowed_chat_ids = @allowedChatIds, updated_at = @now + INSERT INTO telegram_bots (bot_id, user_id, name, bot_token, webhook_secret, created_at, updated_at) + VALUES (@botId, @userId, @name, @botToken, @secret, @now, @now)", + new { botId, userId, name, botToken, secret, now }); + return await GetTelegramBot(botId); + } + + public async Task UpdateTelegramBot(string botId, string name, string botToken, bool isPublic) + { + using var conn = CreateConnection(); + var now = DateTime.UtcNow.ToString("o"); + await conn.ExecuteAsync(@" + UPDATE telegram_bots SET name = @name, bot_token = @botToken, is_public = @isPublic, updated_at = @now + WHERE bot_id = @botId", + new { botId, name, botToken, isPublic, now }); + } + + public async Task DeleteTelegramBot(string botId) + { + using var conn = CreateConnection(); + var now = DateTime.UtcNow.ToString("o"); + // Erst alle Session-Verknüpfungen entfernen + await conn.ExecuteAsync( + "UPDATE sessions SET telegram_bot_id = NULL, updated_at = @now WHERE telegram_bot_id = @botId", + new { botId, now }); + await conn.ExecuteAsync("DELETE FROM telegram_bots WHERE bot_id = @botId", new { botId }); + } + + public async Task GetTelegramBotSessionCount(string botId) + { + using var conn = CreateConnection(); + return await conn.ExecuteScalarAsync( + "SELECT COUNT(*) FROM sessions WHERE telegram_bot_id = @botId AND is_deleted = 0", + new { botId }); + } + + public async Task SetSessionTelegramBot(string sessionId, string botId) + { + using var conn = CreateConnection(); + var now = DateTime.UtcNow.ToString("o"); + await conn.ExecuteAsync( + "UPDATE sessions SET telegram_bot_id = @botId, updated_at = @now WHERE session_id = @sessionId", + new { sessionId, botId = (object)botId ?? DBNull.Value, now }); + } + + public async Task UpdateSessionTelegramChat(string sessionId, string chatId) + { + using var conn = CreateConnection(); + var now = DateTime.UtcNow.ToString("o"); + await conn.ExecuteAsync(@" + UPDATE sessions SET telegram_chat_id = @chatId, updated_at = @now WHERE session_id = @sessionId", - new { sessionId, botToken, webhookSecret, chatId, allowedChatIds, now = DateTime.UtcNow.ToString("o") }); + new { sessionId, chatId, now }); } public async Task SetSessionTelegramChatId(string sessionId, string chatId) { using var conn = CreateConnection(); - // Erst alte Verknüpfung mit dieser chatId entfernen (ein Telegram-Chat = eine Session) - await conn.ExecuteAsync( - "UPDATE sessions SET telegram_chat_id = NULL, updated_at = @now WHERE telegram_chat_id = @chatId", - new { chatId, now = DateTime.UtcNow.ToString("o") }); - // Dann diese Session verknüpfen + var now = DateTime.UtcNow.ToString("o"); await conn.ExecuteAsync( "UPDATE sessions SET telegram_chat_id = @chatId, updated_at = @now WHERE session_id = @sessionId", - new { sessionId, chatId, now = DateTime.UtcNow.ToString("o") }); + new { sessionId, chatId, now }); } public async Task ClearSessionTelegramChatId(string sessionId) @@ -1188,6 +1311,25 @@ public class DatabaseService new { sessionId, now = DateTime.UtcNow.ToString("o") }); } + public async Task> GetSessionsByTelegramBot(string botId) + { + using var conn = CreateConnection(); + return await conn.QueryAsync( + "SELECT * FROM sessions WHERE telegram_bot_id = @botId AND is_deleted = 0", + new { botId }); + } + + public async Task FindSessionForTelegramChat(string botId, long chatId) + { + using var conn = CreateConnection(); + var chatIdStr = chatId.ToString(); + // Erst exakte Chat-ID-Verknüpfung suchen + var session = await conn.QuerySingleOrDefaultAsync( + "SELECT * FROM sessions WHERE telegram_bot_id = @botId AND telegram_chat_id = @chatIdStr AND is_deleted = 0", + new { botId, chatIdStr }); + return session; + } + public async Task> GetUserSessions(string userId) { using var conn = CreateConnection(); diff --git a/Services/TelegramService.cs b/Services/TelegramService.cs index 5688028..c7789b6 100644 --- a/Services/TelegramService.cs +++ b/Services/TelegramService.cs @@ -30,21 +30,9 @@ public class TelegramService _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) + public async Task HandleUpdate(JsonElement update, TelegramBotRecord bot) { if (!update.TryGetProperty("message", out var message)) return; if (!message.TryGetProperty("chat", out var chat)) return; @@ -55,77 +43,54 @@ public class TelegramService if (string.IsNullOrEmpty(text)) return; - var botToken = session.TelegramBotToken; - if (string.IsNullOrEmpty(botToken)) return; + // Session für diesen Chat finden (1 Session = max 1 Chat) + var session = await _db.FindSessionForTelegramChat(bot.BotId, chatId); 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"); + await HandleStart(chatId, bot, session); return; } - // Alle anderen Befehle/Nachrichten: Zugriff prüfen - if (!IsChatIdAllowed(session, chatId)) + if (session == null) { - await SendMessage(chatId, botToken, "🔒 Du bist nicht für diesen Bot freigegeben. Sende /start für mehr Informationen."); + 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, botToken, session); + await HandleCommand(chatId, text, bot.BotToken, session); return; } - await HandleMessage(chatId, text, botToken, session); + 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 ── @@ -153,11 +118,6 @@ public class TelegramService 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; @@ -168,13 +128,6 @@ public class TelegramService 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) { @@ -186,7 +139,6 @@ public class TelegramService try { - // History aus DB laden + User-Nachricht anhängen var history = await _chatService.BuildHistoryFromDb(session.SessionId); history.Add(new Dictionary { @@ -194,7 +146,6 @@ public class TelegramService ["content"] = userText }); - // User-Nachricht in DB speichern await _db.SaveMessage(new MessageRecord { ChainId = ctx.ChainId, @@ -204,7 +155,6 @@ public class TelegramService Content = userText }); - // AnthropicService für Non-Streaming-Chat nutzen var reply = await _anthropic.ChatNonStreaming(ctx, history, onIteration: async (iteration) => { if (iteration % 3 == 0) @@ -224,11 +174,14 @@ public class TelegramService 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)) + 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, session.TelegramBotToken, text); + await SendMessage(chatId, bot.BotToken, text); } // ── Telegram Bot API ── @@ -274,15 +227,11 @@ public class TelegramService new StringContent(JsonSerializer.Serialize(new { chat_id = chatId, action }), Encoding.UTF8, "application/json")); } - // ── Webhook-Verwaltung (pro Session) ── + // ── Webhook-Verwaltung (pro Bot) ── - public async Task RegisterWebhook(string baseUrl, string sessionId) + public async Task RegisterWebhook(string baseUrl, TelegramBotRecord bot) { - 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 webhookUrl = $"{baseUrl.TrimEnd('/')}/api/telegram/webhook/{bot.BotId}"; var client = _httpFactory.CreateClient(); var payload = new Dictionary { @@ -290,66 +239,34 @@ public class TelegramService ["allowed_updates"] = new[] { "message" } }; - if (!string.IsNullOrEmpty(session.TelegramWebhookSecret)) - payload["secret_token"] = session.TelegramWebhookSecret; + if (!string.IsNullOrEmpty(bot.WebhookSecret)) + payload["secret_token"] = bot.WebhookSecret; var resp = await client.PostAsync( - $"https://api.telegram.org/bot{session.TelegramBotToken}/setWebhook", + $"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 GetWebhookInfo(string sessionId) + public async Task GetWebhookInfo(TelegramBotRecord bot) { - 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"); + var resp = await client.GetAsync($"https://api.telegram.org/bot{bot.BotToken}/getWebhookInfo"); return await resp.Content.ReadAsStringAsync(); } - public async Task DeleteWebhook(string sessionId) + public async Task DeleteWebhook(TelegramBotRecord bot) { - 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", + $"https://api.telegram.org/bot{bot.BotToken}/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 ""; diff --git a/wwwroot/css/form.css b/wwwroot/css/form.css index 152470a..0d96480 100644 --- a/wwwroot/css/form.css +++ b/wwwroot/css/form.css @@ -714,3 +714,17 @@ label.skill-item { .form .fe-mde-wrap .editor-toolbar { border-radius: var(--border-radius) var(--border-radius) 0 0 !important; } + +/* ── Inline Actions (Buttons nebeneinander) ── */ + +.form-actions-inline { + display: flex; + gap: var(--badge-spacing); + flex-wrap: wrap; + margin-top: var(--badge-spacing); +} + +.form-actions-inline button { + font-size: 0.85em; + padding: 0.4em 0.8em; +} diff --git a/wwwroot/index.html b/wwwroot/index.html index 28c4049..92b583c 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -83,6 +83,13 @@
+
+
+

Telegram Bots

+ +
+
+