using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Dapper; using Gott.Models; using Microsoft.Data.Sqlite; namespace Gott.Services; public partial class DatabaseService { // ── OpenClaw Import ── public async Task CreateChainForImport(string chainId, string sessionId, string agentId, string model, int inputTokens, int outputTokens, int cacheCreationTokens, int cacheReadTokens, string createdAt) { using var conn = CreateConnection(); await conn.ExecuteAsync(@" INSERT INTO chains (chain_id, session_id, agent_id, model, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, iterations, stop_reason, duration_ms, plan_mode, created_at) VALUES (@chainId, @sessionId, @agentId, @model, @inputTokens, @outputTokens, @cacheCreationTokens, @cacheReadTokens, 0, 'end_turn', 0, 0, @createdAt) ", new { chainId, sessionId, agentId, model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, createdAt }); } public async Task CreateMessageForImport(string chainId, string role, string type, string content, string createdAt, string tag) { using var conn = CreateConnection(); await conn.ExecuteAsync(@" INSERT INTO messages (chain_id, role, type, content, is_error, iteration, attachment_count, tag, created_at) VALUES (@chainId, @role, @type, @content, 0, 0, 0, @tag, @createdAt) ", new { chainId, role, type, content, tag, createdAt }); } public async Task DeleteOpenClawImport(string sessionId) { using var conn = CreateConnection(); using var tx = conn.BeginTransaction(); // Get all chain_ids that belong to this session and have imported messages var importedChainIds = (await conn.QueryAsync(@" SELECT DISTINCT c.chain_id FROM chains c INNER JOIN messages m ON m.chain_id = c.chain_id WHERE c.session_id = @sessionId AND m.tag = 'openclaw-import' ", new { sessionId }, tx)).ToList(); if (importedChainIds.Count > 0) { // Delete messages for these chains await conn.ExecuteAsync( "DELETE FROM messages WHERE chain_id IN @chainIds", new { chainIds = importedChainIds }, tx); // Delete the chains themselves await conn.ExecuteAsync( "DELETE FROM chains WHERE chain_id IN @chainIds", new { chainIds = importedChainIds }, tx); } tx.Commit(); } // ── Telegram Bots ── private void MigrateTelegramBotsFromSessions(SqliteConnection conn) { // 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> 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(@" 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, chatId, now }); } 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(); return await conn.QuerySingleOrDefaultAsync( "SELECT * FROM sessions WHERE telegram_bot_id = @botId AND telegram_chat_id = @chatIdStr AND is_deleted = 0", new { botId, chatIdStr }); } public async Task> FindUnlinkedSessionsForBot(string botId) { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM sessions WHERE telegram_bot_id = @botId AND (telegram_chat_id IS NULL OR telegram_chat_id = '') AND is_deleted = 0", new { botId }); } // ── Cronjobs ── public async Task CreateCronjob(CronjobRecord cronjob) { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); cronjob.CronjobId = Guid.NewGuid().ToString("N"); cronjob.CreatedAt = now; cronjob.UpdatedAt = now; await conn.ExecuteAsync(@" INSERT INTO cronjobs (cronjob_id, user_id, session_id, name, cron_expression, prompt, notify_session_id, is_enabled, next_run_at, created_at, updated_at) VALUES (@CronjobId, @UserId, @SessionId, @Name, @CronExpression, @Prompt, @NotifySessionId, @IsEnabled, @NextRunAt, @CreatedAt, @UpdatedAt) ", cronjob); return cronjob; } public async Task> GetCronjobs(string userId) { using var conn = CreateConnection(); return await conn.QueryAsync(@" SELECT c.*, s.name AS session_name, ns.name AS notify_session_name FROM cronjobs c LEFT JOIN sessions s ON c.session_id = s.session_id LEFT JOIN sessions ns ON c.notify_session_id = ns.session_id WHERE c.user_id = @userId ORDER BY c.name", new { userId }); } public async Task GetCronjob(string cronjobId) { using var conn = CreateConnection(); return await conn.QuerySingleOrDefaultAsync(@" SELECT c.*, s.name AS session_name, ns.name AS notify_session_name FROM cronjobs c LEFT JOIN sessions s ON c.session_id = s.session_id LEFT JOIN sessions ns ON c.notify_session_id = ns.session_id WHERE c.cronjob_id = @cronjobId", new { cronjobId }); } public async Task UpdateCronjob(CronjobRecord cronjob) { using var conn = CreateConnection(); cronjob.UpdatedAt = DateTime.UtcNow.ToString("o"); await conn.ExecuteAsync(@" UPDATE cronjobs SET name = @Name, session_id = @SessionId, cron_expression = @CronExpression, prompt = @Prompt, notify_session_id = @NotifySessionId, is_enabled = @IsEnabled, next_run_at = @NextRunAt, updated_at = @UpdatedAt WHERE cronjob_id = @CronjobId AND user_id = @UserId", cronjob); } public async Task DeleteCronjob(string cronjobId, string userId) { using var conn = CreateConnection(); await conn.ExecuteAsync( "DELETE FROM cronjobs WHERE cronjob_id = @cronjobId AND user_id = @userId", new { cronjobId, userId }); } public async Task UpdateCronjobRun(string cronjobId, string status, string error, string nextRunAt) { using var conn = CreateConnection(); await conn.ExecuteAsync(@" UPDATE cronjobs SET last_run_at = @now, last_run_status = @status, last_run_error = @error, next_run_at = @nextRunAt, updated_at = @now WHERE cronjob_id = @cronjobId", new { cronjobId, status, error, nextRunAt, now = DateTime.UtcNow.ToString("o") }); } public async Task> GetDueCronjobs() { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); return await conn.QueryAsync(@" SELECT c.*, s.name AS session_name, ns.name AS notify_session_name FROM cronjobs c LEFT JOIN sessions s ON c.session_id = s.session_id LEFT JOIN sessions ns ON c.notify_session_id = ns.session_id WHERE c.is_enabled = 1 AND c.next_run_at IS NOT NULL AND c.next_run_at <= @now", new { now }); } // ── API Keys ── private void MigrateAnthropicTokenFromConfig(SqliteConnection conn) { // Nur migrieren wenn noch keine Anthropic-Keys existieren var count = conn.ExecuteScalar("SELECT COUNT(*) FROM api_keys WHERE provider = 'anthropic'"); if (count > 0) return; // Token aus appsettings.Production.json lesen var configPath = Path.Combine(Path.GetDirectoryName(_connectionString.Replace("Data Source=", "")) ?? "", "..", "appsettings.Production.json"); // Normaler Pfad: neben der DB-Datei im übergeordneten Ordner var basePath = Path.GetDirectoryName(_connectionString.Replace("Data Source=", "")); if (basePath != null) { var prodPath = Path.Combine(basePath, "..", "appsettings.Production.json"); if (File.Exists(prodPath)) { try { var json = File.ReadAllText(prodPath); using var doc = System.Text.Json.JsonDocument.Parse(json); if (doc.RootElement.TryGetProperty("AnthropicToken", out var tokenEl)) { var token = tokenEl.GetString(); if (!string.IsNullOrWhiteSpace(token)) { var now = DateTime.UtcNow.ToString("o"); conn.Execute(@" INSERT INTO api_keys (key_id, provider, name, api_key, is_public, created_at, updated_at) VALUES (@keyId, 'anthropic', 'Migriert aus Config', @token, 1, @now, @now)", new { keyId = Guid.NewGuid().ToString("N"), token, now }); Console.WriteLine("[Migration] AnthropicToken aus appsettings.Production.json → api_keys migriert"); } } } catch { /* Config nicht lesbar – ignorieren */ } } } } public async Task> GetApiKeys() { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM api_keys ORDER BY provider, name"); } public async Task> GetApiKeysByProvider(string provider) { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM api_keys WHERE provider = @provider ORDER BY name", new { provider }); } public async Task> GetPublicApiKeys() { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM api_keys WHERE is_public = 1 ORDER BY provider, name"); } public async Task GetApiKey(string keyId) { using var conn = CreateConnection(); return await conn.QuerySingleOrDefaultAsync( "SELECT * FROM api_keys WHERE key_id = @keyId", new { keyId }); } public async Task CreateApiKey(ApiKeyRecord record) { using var conn = CreateConnection(); await conn.ExecuteAsync(@" INSERT INTO api_keys (key_id, provider, name, api_key, is_public, status, plan, usage_7d, usage_5h, checked_at, source_url, created_at, updated_at) VALUES (@KeyId, @Provider, @Name, @ApiKey, @IsPublic, @Status, @Plan, @Usage7d, @Usage5h, @CheckedAt, @SourceUrl, @CreatedAt, @UpdatedAt)", record); return record.KeyId; } public async Task UpdateApiKey(string keyId, string name, string apiKey = null) { using var conn = CreateConnection(); if (!string.IsNullOrWhiteSpace(apiKey)) { await conn.ExecuteAsync( "UPDATE api_keys SET name = @name, api_key = @apiKey, updated_at = @now WHERE key_id = @keyId", new { keyId, name, apiKey, now = DateTime.UtcNow.ToString("o") }); } else { await conn.ExecuteAsync( "UPDATE api_keys SET name = @name, updated_at = @now WHERE key_id = @keyId", new { keyId, name, now = DateTime.UtcNow.ToString("o") }); } } public async Task UpdateApiKeyMeta(string keyId, string status, string plan, double? usage7d, double? usage5h) { using var conn = CreateConnection(); await conn.ExecuteAsync(@" UPDATE api_keys SET status = @status, plan = @plan, usage_7d = @usage7d, usage_5h = @usage5h, checked_at = @now, updated_at = @now WHERE key_id = @keyId", new { keyId, status, plan, usage7d, usage5h, now = DateTime.UtcNow.ToString("o") }); } public async Task UpdateApiKeyMetaByToken(string apiKey, string status, string plan, double? usage7d, double? usage5h) { using var conn = CreateConnection(); await conn.ExecuteAsync(@" UPDATE api_keys SET status = @status, plan = @plan, usage_7d = @usage7d, usage_5h = @usage5h, checked_at = @now, updated_at = @now WHERE api_key = @apiKey", new { apiKey, status, plan, usage7d, usage5h, now = DateTime.UtcNow.ToString("o") }); } public async Task SetApiKeyPublic(string keyId, bool isPublic) { using var conn = CreateConnection(); await conn.ExecuteAsync( "UPDATE api_keys SET is_public = @isPublic, updated_at = @now WHERE key_id = @keyId", new { keyId, isPublic, now = DateTime.UtcNow.ToString("o") }); } public async Task DeleteApiKey(string keyId) { using var conn = CreateConnection(); // Scanned-Keys die auf diesen API-Key verweisen zurücksetzen await conn.ExecuteAsync( "UPDATE scanned_keys SET adopted_api_key_id = NULL WHERE adopted_api_key_id = @keyId", new { keyId }); // Agent-Key-Zuordnungen die auf diesen Key verweisen löschen await conn.ExecuteAsync( "DELETE FROM agent_api_keys WHERE key_id = @keyId", new { keyId }); await conn.ExecuteAsync("DELETE FROM api_keys WHERE key_id = @keyId", new { keyId }); } // ── Scanned Keys ── public async Task ScannedKeyExists(string apiKey) { using var conn = CreateConnection(); return await conn.ExecuteScalarAsync( "SELECT COUNT(*) FROM scanned_keys WHERE api_key = @apiKey", new { apiKey }) > 0; } public async Task ApiKeyExistsInKeys(string apiKey) { using var conn = CreateConnection(); return await conn.ExecuteScalarAsync( "SELECT COUNT(*) FROM api_keys WHERE api_key = @apiKey", new { apiKey }) > 0; } public async Task CreateScannedKey(ScannedKeyRecord record) { using var conn = CreateConnection(); await conn.ExecuteAsync(@" INSERT OR IGNORE INTO scanned_keys (scanned_key_id, api_key, source, source_url, type, status, plan, usage_7d, usage_5h, scanned_at, checked_at) VALUES (@ScannedKeyId, @ApiKey, @Source, @SourceUrl, @Type, @Status, @Plan, @Usage7d, @Usage5h, @ScannedAt, @CheckedAt)", record); } public async Task UpdateScannedKeyStatus(string scannedKeyId, string status, string plan, double? usage7d, double? usage5h) { using var conn = CreateConnection(); await conn.ExecuteAsync(@" UPDATE scanned_keys SET status = @status, plan = @plan, usage_7d = @usage7d, usage_5h = @usage5h, checked_at = @now WHERE scanned_key_id = @scannedKeyId", new { scannedKeyId, status, plan, usage7d, usage5h, now = DateTime.UtcNow.ToString("o") }); } public async Task> GetScannedKeys() { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM scanned_keys WHERE deleted = 0 ORDER BY scanned_at DESC"); } public async Task> GetScannedKeysByType(string type) { using var conn = CreateConnection(); return await conn.QueryAsync( "SELECT * FROM scanned_keys WHERE deleted = 0 AND type = @type ORDER BY scanned_at DESC", new { type }); } public async Task> GetScannedKeysSummary() { using var conn = CreateConnection(); return await conn.QueryAsync( @"SELECT type, COUNT(*) as total, SUM(CASE WHEN adopted_api_key_id IS NOT NULL THEN 1 ELSE 0 END) as adopted, SUM(CASE WHEN status = 'valid' AND adopted_api_key_id IS NULL THEN 1 ELSE 0 END) as valid, SUM(CASE WHEN status = 'invalid' THEN 1 ELSE 0 END) as invalid, SUM(CASE WHEN status = 'unchecked' THEN 1 ELSE 0 END) as unchecked FROM scanned_keys WHERE deleted = 0 GROUP BY type ORDER BY total DESC"); } public async Task GetScannedKey(string scannedKeyId) { using var conn = CreateConnection(); return await conn.QuerySingleOrDefaultAsync( "SELECT * FROM scanned_keys WHERE scanned_key_id = @scannedKeyId", new { scannedKeyId }); } public async Task SetScannedKeyAdopted(string scannedKeyId, string apiKeyId) { using var conn = CreateConnection(); await conn.ExecuteAsync( "UPDATE scanned_keys SET adopted_api_key_id = @apiKeyId WHERE scanned_key_id = @scannedKeyId", new { scannedKeyId, apiKeyId }); } public async Task DeleteScannedKey(string scannedKeyId) { using var conn = CreateConnection(); await conn.ExecuteAsync("UPDATE scanned_keys SET deleted = 1 WHERE scanned_key_id = @scannedKeyId", new { scannedKeyId }); } // ── Scanner Logs ── public async Task CreateScannerLog(string type, string message, string scanner = "event", string details = null) { using var conn = CreateConnection(); // Sicherstellen dass die details-Spalte existiert (Migration) try { await conn.ExecuteAsync(@"ALTER TABLE scanner_logs ADD COLUMN details TEXT"); } catch { /* Spalte existiert bereits */ } await conn.ExecuteAsync(@" INSERT INTO scanner_logs (timestamp, type, message, scanner, details) VALUES (@timestamp, @type, @message, @scanner, @details)", new { timestamp = DateTime.UtcNow.ToString("o"), type, message, scanner, details }); } public async Task> GetScannerLogs(int limit = 200, int offset = 0, string type = null, string scanner = null) { using var conn = CreateConnection(); var where = "WHERE 1=1"; if (!string.IsNullOrEmpty(type)) where += " AND type = @type"; if (!string.IsNullOrEmpty(scanner)) where += " AND scanner = @scanner"; return await conn.QueryAsync($@" SELECT id, timestamp, type, message, scanner, details FROM scanner_logs {where} ORDER BY id DESC LIMIT @limit OFFSET @offset", new { limit, offset, type, scanner }); } public async Task GetScannerLogCount(string type = null, string scanner = null) { using var conn = CreateConnection(); var where = "WHERE 1=1"; if (!string.IsNullOrEmpty(type)) where += " AND type = @type"; if (!string.IsNullOrEmpty(scanner)) where += " AND scanner = @scanner"; return await conn.ExecuteScalarAsync( $"SELECT COUNT(*) FROM scanner_logs {where}", new { type, scanner }); } // ── OAuth Tokens ── /// /// Erstellt einen neuen API-Key-Eintrag für OAuth-Tokens. /// public async Task CreateOAuthApiKey(string provider, string name, string accessToken, string refreshToken, DateTime expiresAt, string oauthProvider) { using var conn = CreateConnection(); var keyId = Guid.NewGuid().ToString("N"); var now = DateTime.UtcNow.ToString("o"); await conn.ExecuteAsync(@" INSERT INTO api_keys (key_id, provider, name, api_key, is_public, status, plan, refresh_token, access_token_expires_at, oauth_provider, created_at, updated_at) VALUES (@keyId, @provider, @name, @accessToken, 0, 'valid', NULL, @refreshToken, @expiresAt, @oauthProvider, @now, @now)", new { keyId, provider, name, accessToken, refreshToken, expiresAt = expiresAt.ToString("o"), oauthProvider, now }); return keyId; } /// /// Aktualisiert Access-Token und Ablaufzeit eines OAuth-Keys. /// public async Task UpdateOAuthTokens(string keyId, string newAccessToken, DateTime newExpiresAt) { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); await conn.ExecuteAsync(@" UPDATE api_keys SET api_key = @newAccessToken, access_token_expires_at = @expiresAt, updated_at = @now WHERE key_id = @keyId", new { keyId, newAccessToken, expiresAt = newExpiresAt.ToString("o"), now }); } /// /// Holt alle OAuth-Keys die bald ablaufen (innerhalb von refreshBeforeMinutes). /// public async Task> GetOAuthKeysNeedingRefresh(int refreshBeforeMinutes = 60) { using var conn = CreateConnection(); var threshold = DateTime.UtcNow.AddMinutes(refreshBeforeMinutes).ToString("o"); return await conn.QueryAsync(@" SELECT * FROM api_keys WHERE refresh_token IS NOT NULL AND refresh_token != '' AND access_token_expires_at IS NOT NULL AND access_token_expires_at != '' AND access_token_expires_at < @threshold", new { threshold }); } /// /// Setzt den Status eines API-Keys auf "expired". /// public async Task SetApiKeyExpired(string keyId) { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); await conn.ExecuteAsync( "UPDATE api_keys SET status = 'expired', updated_at = @now WHERE key_id = @keyId", new { keyId, now }); } }