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
This commit is contained in:
Meleki committed 2026-03-08 19:27:05 +01:00
1 parent 3b7bdb7a20
commit ec73ccd71e
13 files changed
+907 -371

No files matched your search

+221 -71
View File
@@ -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<JsonElement>(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<object>();
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<JsonElement>(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<JsonElement>(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<JsonElement>(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<object>();
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<JsonElement>(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 });
}
}
+1 -3
View File
@@ -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; }
+21
View File
@@ -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; }
}
+159 -17
View File
@@ -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<SessionRecord> GetSessionByTelegramChatId(long chatId)
private void MigrateTelegramBotsFromSessions(SqliteConnection conn)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<SessionRecord>(
"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<dynamic>(
"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<dynamic>(
"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<IEnumerable<TelegramBotRecord>> GetTelegramBots(string userId)
{
using var conn = CreateConnection();
return await conn.QueryAsync<TelegramBotRecord>(
"SELECT * FROM telegram_bots WHERE user_id = @userId ORDER BY created_at DESC",
new { userId });
}
public async Task<IEnumerable<TelegramBotRecord>> GetAvailableTelegramBots(string userId)
{
using var conn = CreateConnection();
return await conn.QueryAsync<TelegramBotRecord>(
"SELECT * FROM telegram_bots WHERE user_id = @userId OR is_public = 1 ORDER BY created_at DESC",
new { userId });
}
public async Task<TelegramBotRecord> GetTelegramBot(string botId)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<TelegramBotRecord>(
"SELECT * FROM telegram_bots WHERE bot_id = @botId",
new { botId });
}
public async Task<TelegramBotRecord> 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<int> GetTelegramBotSessionCount(string botId)
{
using var conn = CreateConnection();
return await conn.ExecuteScalarAsync<int>(
"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<IEnumerable<SessionRecord>> GetSessionsByTelegramBot(string botId)
{
using var conn = CreateConnection();
return await conn.QueryAsync<SessionRecord>(
"SELECT * FROM sessions WHERE telegram_bot_id = @botId AND is_deleted = 0",
new { botId });
}
public async Task<SessionRecord> 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<SessionRecord>(
"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<IEnumerable<SessionRecord>> GetUserSessions(string userId)
{
using var conn = CreateConnection();
+49 -132
View File
@@ -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<string, object>
{
@@ -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<string> RegisterWebhook(string baseUrl, string sessionId)
public async Task<string> 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<string, object>
{
@@ -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<string> GetWebhookInfo(string sessionId)
public async Task<string> 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<string> DeleteWebhook(string sessionId)
public async Task<string> 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 "";
+14
View File
@@ -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;
}
+62 -14
View File
@@ -83,6 +83,13 @@
</div>
<div class="card-grid" id="cronjobCards"></div>
</section>
<section>
<div class="section-header">
<h2><i class="fa fa-paper-plane"></i> Telegram Bots</h2>
<button id="newTelegramBotBtn" class="section-add-btn"><i class="fa fa-plus"></i> Neuer Bot</button>
</div>
<div class="card-grid" id="telegramBotCards"></div>
</section>
<section id="sharedLinksSection" hidden>
<div class="section-header">
<h2>Geteilte Links</h2>
@@ -143,21 +150,18 @@
<label>Benutzerdefinierte Befehle</label>
<div class="custom-commands" id="customCommandsList"></div>
<button type="button" id="addCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
<label><i class="fa fa-paper-plane"></i> <b>Telegram Bot</b></label>
<span>Erstelle einen Bot via <a href="https://t.me/BotFather" target="_blank">@BotFather</a></span>
<label>Bot-Token</label>
<input type="text" id="sessionTelegramToken" placeholder="123456:ABC-DEF..." autocomplete="off">
<label>Webhook-Secret</label>
<input type="text" id="sessionTelegramSecret" readonly style="opacity: var(--semi-transparent); ">
<label>Erlaubte Chat-IDs</label>
<input type="text" id="sessionTelegramAllowed" placeholder="Kommagetrennt, z.B. 123456,789012">
<small class="form-hint">Der verknüpfte Owner ist automatisch erlaubt. Weitere Chat-IDs hier eintragen.</small>
<label>Status</label>
<div id="sessionTelegramStatusBox" class="telegram-status-box">Kein Status verfügbar</div>
<div class="telegram-actions">
<button type="button" id="sessionTelegramRegisterBtn" hidden>Webhook registrieren</button>
<button type="button" id="sessionTelegramDeleteBtn" class="danger-btn" hidden>Webhook entfernen</button>
<label><i class="fa fa-paper-plane"></i> <b>Telegram</b></label>
<label>Telegram Bot</label>
<div id="telegramBotSelectCard" class="agent-select-card">
<div class="agent-select-empty"><i class="fa fa-paper-plane"></i> Kein Bot zugewiesen</div>
</div>
<input type="hidden" id="telegramBotSelect" value="">
<label>Chat-ID</label>
<div class="input-with-btn">
<input type="text" id="sessionTelegramChatId" placeholder="Wird automatisch bei /start gesetzt" autocomplete="off" readonly>
<button type="button" id="sessionTelegramUnlink" class="icon-btn danger-btn" title="Verknüpfung lösen"><i class="fa fa-chain-broken"></i></button>
</div>
<small class="form-hint">Wird automatisch gesetzt wenn jemand /start an den Bot sendet.</small>
</div>
</div>
<div class="form-actions">
@@ -306,6 +310,49 @@
</div>
</template>
<template id="tpl-telegram-bot">
<div class="page form-page">
<header>
<button class="back-btn icon-btn" data-page="dashboard"><i class="fa fa-arrow-left"></i></button>
<h1 id="telegramBotPageTitle">Neuer Telegram Bot</h1>
</header>
<div class="page-content">
<div class="form">
<label>Name</label>
<input type="text" id="tgBotName" placeholder="z.B. Mein Hauptbot">
<label>Bot-Token</label>
<input type="text" id="tgBotToken" placeholder="123456:ABC-DEF..." autocomplete="off">
<small class="form-hint">Erstelle einen Bot via <a href="https://t.me/BotFather" target="_blank">@BotFather</a></small>
<label class="checkbox-label">
<input type="checkbox" id="tgBotPublic">
<span>Public – andere User können diesen Bot in ihren Sessions verwenden</span>
</label>
<div id="tgBotSecretRow" hidden>
<label>Webhook-Secret</label>
<div class="input-with-btn">
<input type="text" id="tgBotSecret" readonly>
<button type="button" id="tgBotCopySecret" class="icon-btn" title="Kopieren"><i class="fa fa-clipboard"></i></button>
</div>
</div>
<div id="tgBotWebhookSection" hidden>
<label><i class="fa fa-plug"></i> <b>Webhook</b></label>
<div id="tgBotWebhookStatus" class="form-status-box"></div>
<div class="form-actions-inline">
<button type="button" id="tgBotWebhookRegister" class="positive-btn"><i class="fa fa-plug"></i> Registrieren</button>
<button type="button" id="tgBotWebhookInfo"><i class="fa fa-info-circle"></i> Status prüfen</button>
<button type="button" id="tgBotWebhookDelete" class="danger-btn"><i class="fa fa-unlink"></i> Entfernen</button>
</div>
</div>
<div class="form-actions">
<button id="tgBotSaveBtn">Speichern</button>
<button id="tgBotCancelBtn">Abbrechen</button>
<button id="tgBotDeleteBtn" class="danger-btn" hidden>Löschen</button>
</div>
</div>
</div>
</div>
</template>
<template id="tpl-chat">
<div class="page chat-page">
<header>
@@ -575,6 +622,7 @@
<script src="js/avatar-editor.js"></script>
<script src="js/pages/dashboard.js"></script>
<script src="js/pages/cronjob-page.js"></script>
<script src="js/pages/telegram-bot-page.js"></script>
<script src="js/pages/agent-page.js"></script>
<script src="js/pages/session-page.js"></script>
<script src="js/pages/chat-page.js"></script>
+68 -15
View File
@@ -284,29 +284,61 @@ async function apiChangePassword(currentPassword, newPassword) {
return resp.json();
}
// ── Telegram API (pro Session) ──
// ── Telegram Bot API ──
async function apiSessionTelegramGet(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram`);
if (!resp.ok) return { configured: false };
async function apiGetTelegramBots() {
const resp = await fetch('/api/telegram/bots');
return resp.json();
}
async function apiSessionTelegramSave(sessionId, botToken, webhookSecret, allowedChatIds) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram`, {
method: 'PUT',
async function apiGetTelegramBot(botId) {
const resp = await fetch(`/api/telegram/bots/${botId}`);
if (!resp.ok) return null;
return resp.json();
}
async function apiCreateTelegramBot(name, botToken) {
const resp = await fetch('/api/telegram/bots', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ botToken, webhookSecret, allowedChatIds })
body: JSON.stringify({ name, botToken })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Telegram-Config speichern fehlgeschlagen');
throw new Error(err.error || 'Bot erstellen fehlgeschlagen');
}
return resp.json();
}
async function apiSessionTelegramRegisterWebhook(sessionId, baseUrl) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram/register-webhook`, {
async function apiGetAvailableTelegramBots() {
const resp = await fetch('/api/telegram/bots/available');
return resp.json();
}
async function apiUpdateTelegramBot(botId, name, botToken, isPublic) {
const resp = await fetch(`/api/telegram/bots/${botId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, botToken, isPublic })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Bot speichern fehlgeschlagen');
}
return resp.json();
}
async function apiDeleteTelegramBot(botId) {
const resp = await fetch(`/api/telegram/bots/${botId}`, { method: 'DELETE' });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Bot löschen fehlgeschlagen');
}
return resp.json();
}
async function apiTelegramBotRegisterWebhook(botId, baseUrl) {
const resp = await fetch(`/api/telegram/bots/${botId}/register-webhook`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ baseUrl })
@@ -315,18 +347,39 @@ async function apiSessionTelegramRegisterWebhook(sessionId, baseUrl) {
return resp.json();
}
async function apiSessionTelegramWebhookInfo(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram/webhook-info`);
async function apiTelegramBotWebhookInfo(botId) {
const resp = await fetch(`/api/telegram/bots/${botId}/webhook-info`);
if (!resp.ok) throw new Error('Webhook-Info fehlgeschlagen');
return resp.json();
}
async function apiSessionTelegramDeleteWebhook(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram/webhook`, { method: 'DELETE' });
async function apiTelegramBotDeleteWebhook(botId) {
const resp = await fetch(`/api/telegram/bots/${botId}/webhook`, { method: 'DELETE' });
if (!resp.ok) throw new Error('Webhook-Löschung fehlgeschlagen');
return resp.json();
}
// ── Session Telegram Config ──
async function apiSessionTelegramGet(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram`);
if (!resp.ok) return { configured: false };
return resp.json();
}
async function apiSessionTelegramSave(sessionId, telegramBotId, chatId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ telegramBotId, chatId })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Telegram-Config speichern fehlgeschlagen');
}
return resp.json();
}
async function apiGetSettings() {
const resp = await fetch('/api/settings');
if (!resp.ok) return {};
+41
View File
@@ -46,6 +46,47 @@ function buildSessionCard(session, agent) {
}
// ── Telegram Bot Card Helper ──
function buildTelegramBotCard(bot) {
const card = document.createElement('div');
card.className = 'card card-compact';
const badges = [];
if (bot.isPublic) {
badges.push('<span class="badge badge-feature"><i class="fa fa-globe"></i> Public</span>');
}
if (bot.isOwner === false) {
badges.push('<span class="badge badge-off"><i class="fa fa-user"></i> Fremd</span>');
}
if (bot.sessionCount > 0) {
badges.push(`<span class="badge badge-feature"><i class="fa fa-comment"></i> ${bot.sessionCount} ${bot.sessionCount === 1 ? 'Session' : 'Sessions'}</span>`);
}
// Meta-Items nur anzeigen wenn Daten vorhanden
const metaParts = [];
if (bot.botToken) {
const maskedToken = '•••' + bot.botToken.slice(-4);
metaParts.push(`<span class="card-meta-item"><i class="fa fa-key"></i> ${esc(maskedToken)}</span>`);
}
if (bot.createdAt) {
metaParts.push(`<span class="card-meta-item"><i class="fa fa-clock-o"></i> ${formatDate(bot.createdAt)}</span>`);
}
card.innerHTML = `
<div class="card-body">
<div class="card-title-row">
<span class="card-title"><i class="fa fa-paper-plane"></i> ${esc(bot.name)}</span>
${badges.join('')}
</div>
${metaParts.length ? `<div class="card-meta">${metaParts.join('')}</div>` : ''}
</div>
`;
return card;
}
// ── Agent Card Helper ──
// Erzeugt ein .card-Element für einen Agent (wiederverwendbar für Dashboard + Picker).
+14
View File
@@ -172,6 +172,20 @@ function _openPickerModal(title, items, currentId, onSelect, options) {
});
}
// ── Telegram Bot Picker Modal ──
function openTelegramBotPickerModal(bots, currentBotId, onSelect) {
var items = bots.map(function(b) {
b._pickerId = b.botId;
return b;
});
_openPickerModal('Telegram Bot auswählen', items, currentBotId, onSelect, {
noneLabel: 'Kein Bot',
noneIcon: 'fa-paper-plane',
buildCard: function(b) { return buildTelegramBotCard(b); }
});
}
// ── OpenClaw Import Modal ──
async function showOpenClawImportModal(sessionId) {
+20 -2
View File
@@ -35,8 +35,8 @@ registerPage('dashboard', {
// Telegram-Config parallel pro Session laden (sessions werden gleich geladen)
const [agents, sessions, cronjobs, shares] = await Promise.all([
apiGetAgents(), apiGetSessions(), apiGetCronjobs(), apiListShares()
const [agents, sessions, cronjobs, shares, telegramBots] = await Promise.all([
apiGetAgents(), apiGetSessions(), apiGetCronjobs(), apiListShares(), apiGetTelegramBots()
]);
const agentMap = {};
for (const a of agents) agentMap[a.agentId] = a;
@@ -253,6 +253,24 @@ registerPage('dashboard', {
// Neuer Cronjob Button
document.getElementById('newCronjobBtn').addEventListener('click', () => navigateTo('cronjob'));
// Telegram Bots
const tgGrid = document.getElementById('telegramBotCards');
tgGrid.innerHTML = '';
for (const bot of telegramBots) {
const card = buildTelegramBotCard(bot);
// Card klickbar → direkt zur Edit-Seite
card.classList.add('clickable');
card.addEventListener('click', () => navigateTo('telegram-bot', { id: bot.botId }));
tgGrid.appendChild(card);
}
document.getElementById('newTelegramBotBtn').addEventListener('click', () => {
navigateTo('telegram-bot');
});
// Shared Links
const sharedSection = document.getElementById('sharedLinksSection');
const sharedGrid = document.getElementById('sharedLinksCards');
+53 -117
View File
@@ -71,7 +71,7 @@ registerPage('session', {
// Telegram-Section nur für existierende Sessions
if (!isNew) {
initSessionTelegram(sessionId);
initSessionTelegram(sessionId, session);
}
// Agents laden + Skills parallel
@@ -175,16 +175,14 @@ registerPage('session', {
await apiCreateSession(data);
} else {
await apiUpdateSession(sessionId, data);
// Telegram-Config mitspeichern (falls Section sichtbar)
var tgToken = document.getElementById('sessionTelegramToken');
var tgSecret = document.getElementById('sessionTelegramSecret');
if (tgToken) {
try {
await saveTelegramConfig(sessionId, tgToken, tgSecret);
} catch(e) {
showToast('Session gespeichert, aber Telegram-Fehler: ' + e.message, 'error');
return;
}
// Telegram-Config mitspeichern
try {
var tgBotId = document.getElementById('telegramBotSelect').value || '';
var tgChatId = document.getElementById('sessionTelegramChatId').value.trim();
await apiSessionTelegramSave(sessionId, tgBotId, tgChatId);
} catch(e) {
showToast('Session gespeichert, aber Telegram-Fehler: ' + e.message, 'error');
return;
}
}
navigateTo('dashboard');
@@ -223,121 +221,59 @@ function openAgentPickerModal(agents, agentSkillsMap, currentId, onSelect) {
});
}
function initSessionTelegram(sessionId) {
var tokenInput = document.getElementById('sessionTelegramToken');
var secretInput = document.getElementById('sessionTelegramSecret');
var allowedInput = document.getElementById('sessionTelegramAllowed');
var registerBtn = document.getElementById('sessionTelegramRegisterBtn');
var deleteBtn = document.getElementById('sessionTelegramDeleteBtn');
var statusBox = document.getElementById('sessionTelegramStatusBox');
function initSessionTelegram(sessionId, session) {
var botSelectCard = document.getElementById('telegramBotSelectCard');
var botSelect = document.getElementById('telegramBotSelect');
var chatIdInput = document.getElementById('sessionTelegramChatId');
var unlinkBtn = document.getElementById('sessionTelegramUnlink');
// Vorhandene Config laden + Status abrufen
apiSessionTelegramGet(sessionId).then(async function(config) {
if (config.botToken) tokenInput.value = config.botToken;
if (config.allowedChatIds) allowedInput.value = config.allowedChatIds;
if (config.webhookSecret) {
secretInput.value = config.webhookSecret;
} else {
// Secret einmalig generieren und sofort persistent speichern
var newSecret = crypto.randomUUID().replace(/-/g, '');
secretInput.value = newSecret;
await apiSessionTelegramSave(sessionId, config.botToken || '', newSecret);
}
// Status automatisch abrufen wenn Token konfiguriert ist
if (config.botToken) {
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
} else {
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
}
// Unlink-Button
unlinkBtn.addEventListener('click', function() {
if (!chatIdInput.value) return;
if (!confirm('Telegram-Verknüpfung wirklich lösen?')) return;
chatIdInput.value = '';
showToast('Chat-ID entfernt. Speichern nicht vergessen.', 'info');
});
// Config laden
apiSessionTelegramGet(sessionId).then(function(config) {
if (config.chatId) chatIdInput.value = config.chatId;
if (config.telegramBotId) botSelect.value = config.telegramBotId;
// Bot-Picker initialisieren
initTelegramBotPicker(botSelectCard, botSelect, config.telegramBotId);
}).catch(function() {
showTelegramUnconfigured(statusBox, registerBtn, deleteBtn);
});
// Webhook registrieren
registerBtn.addEventListener('click', async function() {
var baseUrl = prompt('Base-URL der App:', window.location.origin);
if (!baseUrl) return;
await withButtonLoading(registerBtn, 'Wird registriert...', async () => {
try {
await saveTelegramConfig(sessionId, tokenInput, secretInput);
var res = await apiSessionTelegramRegisterWebhook(sessionId, baseUrl.trim());
if (res.ok) {
showToast('Webhook registriert!', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
}
});
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
});
// Webhook entfernen
deleteBtn.addEventListener('click', async function() {
if (!confirm('Webhook wirklich entfernen?')) return;
await withButtonLoading(deleteBtn, 'Wird entfernt...', async () => {
try {
var res = await apiSessionTelegramDeleteWebhook(sessionId);
if (res.ok) {
showToast('Webhook entfernt', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
}
});
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
initTelegramBotPicker(botSelectCard, botSelect, null);
});
}
function showTelegramUnconfigured(statusBox, registerBtn, deleteBtn) {
statusBox.innerHTML = '<p>⚠️ Kein Bot-Token konfiguriert. Speichere zuerst die Session mit einem Token.</p>';
statusBox.hidden = false;
registerBtn.hidden = true;
deleteBtn.hidden = true;
}
async function refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn) {
statusBox.hidden = true;
async function initTelegramBotPicker(cardEl, selectEl, currentBotId) {
var bots = [];
try {
var info = await apiSessionTelegramWebhookInfo(sessionId);
var result = info.result || {};
var url = result.url || '';
var pending = result.pending_update_count || 0;
var lastError = result.last_error_message;
var active = !!url;
var result = await apiGetAvailableTelegramBots();
if (Array.isArray(result)) bots = result;
} catch(e) { /* ignore */ }
var botMap = {};
for (var i = 0; i < bots.length; i++) botMap[bots[i].botId] = bots[i];
var html = '';
if (active) {
html += '<p>✅ <strong>Webhook aktiv</strong></p>';
html += '<p><strong>URL:</strong> ' + esc(url) + '</p>';
html += '<p><strong>Ausstehend:</strong> ' + pending + '</p>';
if (lastError) html += '<p class="tg-error"><strong>Letzter Fehler:</strong> ' + esc(lastError) + '</p>';
} else {
html += '<p>⚠️ <strong>Webhook nicht registriert</strong></p>';
}
statusBox.innerHTML = html;
statusBox.hidden = false;
// Buttons ein/ausblenden je nach Status
registerBtn.hidden = active;
deleteBtn.hidden = !active;
} catch(e) {
statusBox.innerHTML = '<p class="tg-error">Status konnte nicht abgerufen werden: ' + esc(e.message) + '</p>';
statusBox.hidden = false;
registerBtn.hidden = false;
deleteBtn.hidden = true;
function renderBotInCard(bot) {
var inner = bot ? buildTelegramBotCard(bot) : null;
_renderCardPreview(cardEl, inner, 'fa-paper-plane', 'Kein Bot zugewiesen', 'tg-bot-select-change');
}
}
async function saveTelegramConfig(sessionId, tokenInput, secretInput) {
var token = tokenInput.value.trim();
var secret = secretInput.value.trim();
var allowedInput = document.getElementById('sessionTelegramAllowed');
var allowed = allowedInput ? allowedInput.value.trim() : '';
await apiSessionTelegramSave(sessionId, token, secret, allowed);
if (currentBotId && botMap[currentBotId]) {
selectEl.value = currentBotId;
renderBotInCard(botMap[currentBotId]);
} else {
renderBotInCard(null);
}
cardEl.addEventListener('click', function() {
openTelegramBotPickerModal(bots, selectEl.value, function(selectedBotId) {
selectEl.value = selectedBotId || '';
renderBotInCard(selectedBotId ? botMap[selectedBotId] : null);
});
});
}
+184
View File
@@ -0,0 +1,184 @@
// ── Telegram Bot Page ──
registerPage('telegram-bot', {
async init(params) {
var isEdit = params && params.id;
var bot = null;
var titleEl = document.getElementById('telegramBotPageTitle');
var nameInput = document.getElementById('tgBotName');
var tokenInput = document.getElementById('tgBotToken');
var secretRow = document.getElementById('tgBotSecretRow');
var secretInput = document.getElementById('tgBotSecret');
var copySecretBtn = document.getElementById('tgBotCopySecret');
var webhookSection = document.getElementById('tgBotWebhookSection');
var webhookStatus = document.getElementById('tgBotWebhookStatus');
var webhookRegisterBtn = document.getElementById('tgBotWebhookRegister');
var webhookInfoBtn = document.getElementById('tgBotWebhookInfo');
var webhookDeleteBtn = document.getElementById('tgBotWebhookDelete');
var publicCheckbox = document.getElementById('tgBotPublic');
var saveBtn = document.getElementById('tgBotSaveBtn');
var cancelBtn = document.getElementById('tgBotCancelBtn');
var deleteBtn = document.getElementById('tgBotDeleteBtn');
if (isEdit) {
bot = await apiGetTelegramBot(params.id);
if (!bot) {
showToast('Bot nicht gefunden', 'error');
navigateTo('dashboard');
return;
}
titleEl.textContent = bot.name || 'Telegram Bot';
nameInput.value = bot.name || '';
tokenInput.value = bot.botToken || '';
publicCheckbox.checked = bot.isPublic || false;
deleteBtn.hidden = false;
// Webhook-Secret anzeigen
if (bot.webhookSecret) {
secretRow.hidden = false;
secretInput.value = bot.webhookSecret;
}
// Webhook-Section anzeigen
webhookSection.hidden = false;
_loadWebhookStatus(bot.botId, webhookStatus);
} else {
titleEl.textContent = 'Neuer Telegram Bot';
deleteBtn.hidden = true;
}
// Copy Secret
copySecretBtn.addEventListener('click', async function() {
try {
await navigator.clipboard.writeText(secretInput.value);
showToast('Kopiert!');
} catch(e) {
showToast('Kopieren fehlgeschlagen', 'error');
}
});
// Webhook registrieren
webhookRegisterBtn.addEventListener('click', async function() {
var baseUrl = prompt('Base-URL der App:', window.location.origin);
if (!baseUrl) return;
await withButtonLoading(webhookRegisterBtn, 'Registriere...', async function() {
try {
var res = await apiTelegramBotRegisterWebhook(bot.botId, baseUrl.trim());
if (res.ok) {
showToast('Webhook registriert!', 'success');
_loadWebhookStatus(bot.botId, webhookStatus);
} else {
showToast('Fehler: ' + JSON.stringify(res), 'error');
}
} catch(err) {
showToast('Fehler: ' + err.message, 'error');
}
});
});
// Webhook-Status prüfen
webhookInfoBtn.addEventListener('click', async function() {
await withButtonLoading(webhookInfoBtn, 'Prüfe...', async function() {
await _loadWebhookStatus(bot.botId, webhookStatus);
});
});
// Webhook entfernen
webhookDeleteBtn.addEventListener('click', async function() {
if (!confirm('Webhook wirklich entfernen?')) return;
await withButtonLoading(webhookDeleteBtn, 'Entferne...', async function() {
try {
var res = await apiTelegramBotDeleteWebhook(bot.botId);
if (res.ok) {
showToast('Webhook entfernt', 'success');
_loadWebhookStatus(bot.botId, webhookStatus);
} else {
showToast('Fehler: ' + JSON.stringify(res), 'error');
}
} catch(err) {
showToast('Fehler: ' + err.message, 'error');
}
});
});
// Speichern
saveBtn.addEventListener('click', async function() {
var name = nameInput.value.trim();
var token = tokenInput.value.trim();
var isPublic = publicCheckbox.checked;
if (!name || !token) {
showToast('Name und Bot-Token sind erforderlich', 'error');
return;
}
await withButtonLoading(saveBtn, 'Speichern...', async function() {
try {
if (isEdit) {
await apiUpdateTelegramBot(bot.botId, name, token, isPublic);
showToast('Bot gespeichert', 'success');
// Titel aktualisieren
titleEl.textContent = name;
} else {
var created = await apiCreateTelegramBot(name, token);
showToast('Bot erstellt', 'success');
navigateTo('telegram-bot', { id: created.botId });
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
}
});
});
// Abbrechen
cancelBtn.addEventListener('click', function() {
navigateTo('dashboard');
});
// Löschen
deleteBtn.addEventListener('click', async function() {
if (!confirm('Bot "' + (bot.name || '') + '" wirklich löschen? Alle Session-Verknüpfungen werden entfernt.')) return;
try {
await apiDeleteTelegramBot(bot.botId);
showToast('Bot gelöscht', 'success');
navigateTo('dashboard');
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
}
});
}
});
async function _loadWebhookStatus(botId, statusEl) {
try {
var info = await apiTelegramBotWebhookInfo(botId);
var result = info.result || {};
var url = result.url || '';
var pending = result.pending_update_count || 0;
var lastError = result.last_error_message || '';
var lastErrorDate = result.last_error_date ? new Date(result.last_error_date * 1000).toLocaleString('de-AT') : '';
var parts = [];
if (url) {
parts.push('<span class="status-active"><i class="fa fa-check-circle"></i> Webhook aktiv</span>');
parts.push('<span><i class="fa fa-link"></i> ' + esc(url) + '</span>');
} else {
parts.push('<span class="status-inactive"><i class="fa fa-times-circle"></i> Kein Webhook registriert</span>');
}
if (pending > 0) {
parts.push('<span><i class="fa fa-clock-o"></i> ' + pending + ' ausstehende Updates</span>');
}
if (lastError) {
parts.push('<span class="status-error"><i class="fa fa-exclamation-triangle"></i> ' + esc(lastError) + (lastErrorDate ? ' (' + lastErrorDate + ')' : '') + '</span>');
}
statusEl.innerHTML = parts.join('');
statusEl.hidden = false;
} catch(e) {
statusEl.innerHTML = '<span class="status-error"><i class="fa fa-exclamation-triangle"></i> Status konnte nicht geladen werden</span>';
statusEl.hidden = false;
}
}