Files
gott/Services/TelegramService.cs
T
Meleki 4fa0111c33 feat: Erweiterte Model-Auswahl für Agents
- Model-Optionen: sonnet-4-5, sonnet-4-6, opus-4-5, opus-4-6, haiku-4-5
- ChatEndpoint + TelegramService: Model-Mapping für alle Varianten
- Agent-Formular: Dropdown mit allen Model-Versionen
- ui.js: prettyModelName() vereinfacht für neue Model-Werte
2026-03-03 09:22:47 +01:00

855 lines
32 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Gott.Models;
namespace Gott.Services;
public class TelegramService
{
private readonly string _botToken;
private readonly string _webhookSecret;
private readonly string _anthropicToken;
private readonly IHttpClientFactory _httpFactory;
private readonly DatabaseService _db;
private readonly ToolService _toolService;
private readonly MemoryService _memoryService;
private readonly SkillService _skillService;
private readonly int _maxIterations;
public TelegramService(
string botToken,
string webhookSecret,
string anthropicToken,
IHttpClientFactory httpFactory,
DatabaseService db,
ToolService toolService,
MemoryService memoryService,
SkillService skillService,
int maxIterations = 100)
{
_botToken = botToken;
_webhookSecret = webhookSecret;
_anthropicToken = anthropicToken;
_httpFactory = httpFactory;
_db = db;
_toolService = toolService;
_memoryService = memoryService;
_skillService = skillService;
_maxIterations = maxIterations;
}
public string WebhookSecret => _webhookSecret;
public bool IsConfigured => !string.IsNullOrEmpty(_botToken);
// ── Webhook-Update verarbeiten ──
public async Task HandleUpdate(JsonElement update)
{
if (!update.TryGetProperty("message", out var message)) return;
if (!message.TryGetProperty("chat", out var chat)) return;
if (!message.TryGetProperty("text", out var textEl)) return;
var chatId = chat.GetProperty("id").GetInt64();
var text = textEl.GetString()?.Trim() ?? "";
if (string.IsNullOrEmpty(text)) return;
// Commands
if (text.StartsWith("/"))
{
await HandleCommand(chatId, text);
return;
}
// Normale Nachricht → an Claude weiterleiten
await HandleMessage(chatId, text);
}
// ── Commands ──
private async Task HandleCommand(long chatId, string text)
{
var parts = text.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var cmd = parts[0].ToLowerInvariant().Split('@')[0]; // /command@botname → /command
var arg = parts.Length > 1 ? parts[1].Trim() : "";
switch (cmd)
{
case "/start":
await SendMessage(chatId,
"👋 *Willkommen bei Gott\\!*\n\n" +
"Verknüpfe mich mit einer Session:\n" +
"1\\. Öffne im Web\\-UI eine Session\n" +
"2\\. Klicke auf 🔗 *Telegram*\n" +
"3\\. Sende mir den Code: `/link CODE`\n\n" +
"Befehle:\n" +
"/link CODE – Session verknüpfen\n" +
"/session – Session wechseln\n" +
"/new – Neue Session erstellen\n" +
"/clear – History archivieren\n" +
"/status – Aktuelle Verknüpfung\n" +
"/unlink – Verknüpfung aufheben",
parseMode: "MarkdownV2");
break;
case "/link":
await HandleLink(chatId, arg);
break;
case "/session":
await HandleSessionSwitch(chatId, arg);
break;
case "/new":
await HandleNewSession(chatId, arg);
break;
case "/clear":
await HandleClear(chatId);
break;
case "/status":
await HandleStatus(chatId);
break;
case "/unlink":
await HandleUnlink(chatId);
break;
default:
await SendMessage(chatId, "❓ Unbekannter Befehl. Sende /start für Hilfe.");
break;
}
}
private async Task HandleLink(long chatId, string code)
{
if (string.IsNullOrEmpty(code))
{
await SendMessage(chatId, "Bitte sende: `/link CODE`\n\nDen Code findest du im Web\\-UI unter 🔗 Telegram\\.", parseMode: "MarkdownV2");
return;
}
var link = await _db.RedeemTelegramLinkCode(code.ToUpperInvariant(), chatId);
if (link == null)
{
await SendMessage(chatId, "❌ Code ungültig oder abgelaufen. Bitte generiere einen neuen im Web-UI.");
return;
}
var session = await _db.GetSession(link.SessionId);
var sessionName = session?.Name ?? link.SessionId;
await SendMessage(chatId, $"✅ Verknüpft mit Session: *{EscapeMd(sessionName)}*\n\nDu kannst jetzt einfach drauflos schreiben\\!", parseMode: "MarkdownV2");
}
private async Task HandleSessionSwitch(long chatId, string arg)
{
var link = await _db.GetTelegramLink(chatId);
if (link == null)
{
await SendMessage(chatId, "❌ Nicht verknüpft. Sende /link CODE um zu starten.");
return;
}
// Wenn eine Session-Nummer angegeben wurde
if (!string.IsNullOrEmpty(arg))
{
var sessions = (await _db.GetUserSessions(link.UserId)).ToList();
if (int.TryParse(arg, out var idx) && idx >= 1 && idx <= sessions.Count)
{
var target = sessions[idx - 1];
var agent = !string.IsNullOrEmpty(target.AgentId) ? await _db.GetAgent(target.AgentId) : null;
await _db.UpdateTelegramSession(chatId, target.SessionId, target.AgentId);
await SendMessage(chatId, $"✅ Gewechselt zu: *{EscapeMd(target.Name ?? target.SessionId)}*", parseMode: "MarkdownV2");
return;
}
// Vielleicht ein Name?
var match = sessions.FirstOrDefault(s =>
(s.Name ?? "").Contains(arg, StringComparison.OrdinalIgnoreCase));
if (match != null)
{
await _db.UpdateTelegramSession(chatId, match.SessionId, match.AgentId);
await SendMessage(chatId, $"✅ Gewechselt zu: *{EscapeMd(match.Name ?? match.SessionId)}*", parseMode: "MarkdownV2");
return;
}
await SendMessage(chatId, "❌ Session nicht gefunden.");
return;
}
// Ohne Argument: Liste anzeigen
var allSessions = (await _db.GetUserSessions(link.UserId)).ToList();
if (allSessions.Count == 0)
{
await SendMessage(chatId, "Keine Sessions vorhanden. Erstelle eine mit /new");
return;
}
var sb = new StringBuilder("📋 *Deine Sessions:*\n\n");
for (int i = 0; i < allSessions.Count && i < 20; i++)
{
var s = allSessions[i];
var marker = s.SessionId == link.SessionId ? " ◀️" : "";
sb.AppendLine($"`{i + 1}` {EscapeMd(s.Name ?? s.SessionId)}{marker}");
}
sb.AppendLine($"\nWechseln: `/session NUMMER`");
await SendMessage(chatId, sb.ToString(), parseMode: "MarkdownV2");
}
private async Task HandleNewSession(long chatId, string name)
{
var link = await _db.GetTelegramLink(chatId);
if (link == null)
{
await SendMessage(chatId, "❌ Nicht verknüpft. Sende /link CODE um zu starten.");
return;
}
if (string.IsNullOrWhiteSpace(name))
name = $"Telegram {DateTime.UtcNow:dd.MM.yyyy HH:mm}";
var session = await _db.CreateSession(link.UserId, name: name, agentId: link.AgentId);
await _db.UpdateTelegramSession(chatId, session.SessionId, link.AgentId);
await SendMessage(chatId, $"✅ Neue Session erstellt: *{EscapeMd(name)}*", parseMode: "MarkdownV2");
}
private async Task HandleClear(long chatId)
{
var link = await _db.GetTelegramLink(chatId);
if (link == null)
{
await SendMessage(chatId, "❌ Nicht verknüpft.");
return;
}
var archive = await _db.CreateArchive(link.SessionId);
if (archive == null)
{
await SendMessage(chatId, "History ist bereits leer.");
return;
}
await SendMessage(chatId, $"✅ History archiviert ({archive.MessageCount} Nachrichten).");
}
private async Task HandleStatus(long chatId)
{
var link = await _db.GetTelegramLink(chatId);
if (link == null)
{
await SendMessage(chatId, "❌ Nicht verknüpft. Sende /link CODE um zu starten.");
return;
}
var session = await _db.GetSession(link.SessionId);
var agent = !string.IsNullOrEmpty(link.AgentId) ? await _db.GetAgent(link.AgentId) : null;
var sb = new StringBuilder("📊 *Status:*\n\n");
sb.AppendLine($"Session: *{EscapeMd(session?.Name ?? link.SessionId)}*");
if (agent != null)
sb.AppendLine($"Agent: *{EscapeMd(agent.Name)}* \\({EscapeMd(agent.Model)}\\)");
await SendMessage(chatId, sb.ToString(), parseMode: "MarkdownV2");
}
private async Task HandleUnlink(long chatId)
{
await _db.DeleteTelegramLink(chatId);
await SendMessage(chatId, "✅ Verknüpfung aufgehoben. Sende /link CODE um erneut zu verknüpfen.");
}
// ── Nachricht an Claude weiterleiten (Non-Streaming) ──
private async Task HandleMessage(long chatId, string userText)
{
var link = await _db.GetTelegramLink(chatId);
if (link == null)
{
await SendMessage(chatId, "❌ Nicht verknüpft. Sende /start für Hilfe.");
return;
}
var session = await _db.GetSession(link.SessionId);
if (session == null)
{
await SendMessage(chatId, "❌ Session nicht mehr verfügbar. Nutze /session zum Wechseln.");
return;
}
var agent = !string.IsNullOrEmpty(link.AgentId) ? await _db.GetAgent(link.AgentId) : null;
if (agent == null && !string.IsNullOrEmpty(session.AgentId))
agent = await _db.GetAgent(session.AgentId);
if (agent == null)
{
await SendMessage(chatId, "❌ Kein Agent konfiguriert. Bitte weise der Session im Web-UI einen Agent zu.");
return;
}
// "Typing" anzeigen
await SendChatAction(chatId, "typing");
try
{
var reply = await ChatNonStreaming(session, agent, userText, chatId);
await SendLongMessage(chatId, reply);
}
catch (Exception ex)
{
await SendMessage(chatId, $"❌ Fehler: {ex.Message}");
}
}
// ── Non-Streaming Claude Chat ──
private async Task<string> ChatNonStreaming(SessionRecord session, AgentRecord agent, string userText, long chatId)
{
var modelId = agent.Model switch
{
"sonnet" => "claude-sonnet-4-6",
"sonnet-4-5" => "claude-sonnet-4-5",
"sonnet-4-6" => "claude-sonnet-4-6",
"opus" => "claude-opus-4-6",
"opus-4-5" => "claude-opus-4-5",
"opus-4-6" => "claude-opus-4-6",
"haiku" => "claude-haiku-4-5",
"haiku-4-5" => "claude-haiku-4-5",
_ => "claude-sonnet-4-6"
};
// History aus DB laden (aktive Chains)
var history = await BuildHistory(session.SessionId);
// User-Nachricht anhängen
history.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = userText
});
// System-Prompt
var markedMessages = await _db.GetMarkedMessages(session.SessionId);
var agentMemoryDir = _memoryService.GetAgentMemoryDir(agent.AgentId);
var agentSkills = await _db.GetAgentSkills(agent.AgentId);
var user = await _db.GetUser(session.UserId);
var systemPromptText = SystemPromptBuilder.Build(
agent, session, markedMessages, _memoryService, agentMemoryDir,
agentSkills, _skillService, userPersona: user?.GetEffectivePersona());
var systemPrompt = new List<object>();
if (!string.IsNullOrWhiteSpace(systemPromptText))
systemPrompt.Add(new { type = "text", text = systemPromptText });
// Chain erstellen
var chainId = Guid.NewGuid().ToString("N");
var workspace = session.Workspace;
if (string.IsNullOrEmpty(workspace) && agentSkills.Any())
workspace = "/tmp";
var toolChoice = agent.ToolChoice ?? "any";
var hasWorkspace = !string.IsNullOrEmpty(workspace);
var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId);
var toolDefs = (hasWorkspace || includeMemory)
? _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory)
: Array.Empty<object>();
var toolNames = toolDefs.Length > 0
? JsonSerializer.Serialize(toolDefs.Select(td =>
{
var json = JsonSerializer.Serialize(td);
var el = JsonSerializer.Deserialize<JsonElement>(json);
return el.GetProperty("name").GetString();
}).ToArray())
: null;
await _db.CreateChain(new ChainRecord
{
ChainId = chainId,
SessionId = session.SessionId,
AgentId = agent.AgentId,
Model = modelId,
SystemPrompt = systemPromptText,
ToolChoice = (hasWorkspace || includeMemory) ? toolChoice : null,
ToolNames = toolNames
});
// User-Nachricht speichern
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId,
Iteration = 0,
Role = "user",
Type = "text",
Content = userText
});
await _db.UpdateSessionTimestamp(session.SessionId);
if (!string.IsNullOrEmpty(workspace))
Directory.CreateDirectory(workspace);
// Iterativer Claude-Loop (Non-Streaming)
var sw = System.Diagnostics.Stopwatch.StartNew();
var usage = new TokenUsage();
int iterations = 0;
var finalText = new StringBuilder();
try
{
while (iterations < _maxIterations)
{
iterations++;
var payload = new Dictionary<string, object>
{
["model"] = modelId,
["max_tokens"] = 16384,
["messages"] = history,
["stream"] = false
};
if (systemPrompt.Count > 0)
payload["system"] = systemPrompt;
var hasTools = hasWorkspace || includeMemory;
var hasWebSearch = !string.IsNullOrEmpty(agent.WebSearchVersion);
if (hasTools || hasWebSearch)
{
var tools = new List<object>();
if (hasTools)
tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory));
if (hasWebSearch)
tools.Add(new { type = agent.WebSearchVersion, name = "web_search", max_uses = 5 });
payload["tools"] = tools;
var effectiveChoice = (toolChoice == "any" && iterations == 1) ? "any" : "auto";
payload["tool_choice"] = new { type = effectiveChoice };
}
var client = CreateAnthropicClient();
var json = JsonSerializer.Serialize(payload);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
var httpResp = await client.SendAsync(
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent });
var respBody = await httpResp.Content.ReadAsStringAsync();
if (!httpResp.IsSuccessStatusCode)
{
await SaveError(chainId, iterations, $"API {(int)httpResp.StatusCode}: {respBody}");
await FinalizeChain(chainId, usage, iterations, "error", sw);
throw new Exception($"Claude API Fehler: {(int)httpResp.StatusCode}");
}
var resp = JsonSerializer.Deserialize<JsonElement>(respBody);
// Usage tracken
if (resp.TryGetProperty("usage", out var u))
{
if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64();
if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64();
if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64();
if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64();
}
var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
var content = resp.GetProperty("content");
// Content verarbeiten
var assistantContent = new List<object>();
var toolResults = new List<object>();
var executedTools = new List<ToolResult>();
foreach (var block in content.EnumerateArray())
{
var blockType = block.GetProperty("type").GetString() ?? "";
if (blockType == "text")
{
var text = block.GetProperty("text").GetString() ?? "";
assistantContent.Add(new { type = "text", text });
finalText.Append(text);
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "assistant", Type = "text",
Content = text
});
}
else if (blockType == "tool_use")
{
var toolId = block.GetProperty("id").GetString() ?? "";
var toolName = block.GetProperty("name").GetString() ?? "";
var toolInput = block.GetProperty("input");
assistantContent.Add(new { type = "tool_use", id = toolId, name = toolName, input = toolInput });
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "assistant", Type = "tool_use",
ToolName = toolName, ToolCallId = toolId,
Content = toolInput.ToString()
});
// Tool ausführen
string toolResult;
bool isError = false;
try
{
toolResult = await _toolService.ExecuteTool(toolName, toolInput, session.Workspace, _memoryService, agent.AgentId);
}
catch (Exception ex)
{
toolResult = $"Fehler: {ex.Message}";
isError = true;
}
executedTools.Add(new ToolResult(toolId, toolResult, isError));
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "user", Type = "tool_result",
ToolCallId = toolId, Content = toolResult, IsError = isError
});
const int MaxToolResultChars = 20_000;
var truncResult = toolResult.Length > MaxToolResultChars
? toolResult[..MaxToolResultChars] + $"\n\n[... gekürzt, {toolResult.Length - MaxToolResultChars} weitere Zeichen ...]"
: toolResult;
toolResults.Add(new
{
type = "tool_result",
tool_use_id = toolId,
content = truncResult,
is_error = isError
});
}
}
// Assistant-Antwort in History aufnehmen
history.Add(new Dictionary<string, object>
{
["role"] = "assistant",
["content"] = assistantContent
});
if (stopReason != "tool_use")
{
await FinalizeChain(chainId, usage, iterations, stopReason, sw);
break;
}
// Tool-Results zurücksenden
history.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = toolResults
});
// Typing-Indikator aufrechterhalten
await SendChatAction(chatId, "typing");
}
if (iterations >= _maxIterations)
{
await FinalizeChain(chainId, usage, iterations, "max_iterations", sw);
finalText.Append("\n\n⚠️ Max Iterationen erreicht.");
}
}
catch (Exception) when (finalText.Length > 0)
{
// Partial result
}
return finalText.Length > 0 ? finalText.ToString() : "🤷 Keine Antwort erhalten.";
}
// ── History aus DB aufbauen ──
private async Task<List<Dictionary<string, object>>> BuildHistory(string sessionId)
{
var chains = await _db.GetActiveChains(sessionId);
var messages = new List<Dictionary<string, object>>();
foreach (var chain in chains.Reverse())
{
var chainMsgs = (await _db.GetChainMessages(chain.ChainId)).ToList();
foreach (var msg in chainMsgs)
{
if (msg.Role == "user" && msg.Type == "text")
{
messages.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = msg.Content ?? ""
});
}
else if (msg.Role == "assistant" && msg.Type == "text")
{
// Aufeinanderfolgende Assistant-Text-Blöcke zusammenfassen
if (messages.Count > 0 && messages[^1]["role"] as string == "assistant")
{
var existing = messages[^1]["content"];
if (existing is List<object> blocks)
{
blocks.Add(new { type = "text", text = msg.Content ?? "" });
}
else if (existing is string s)
{
messages[^1]["content"] = new List<object>
{
new { type = "text", text = s },
new { type = "text", text = msg.Content ?? "" }
};
}
}
else
{
messages.Add(new Dictionary<string, object>
{
["role"] = "assistant",
["content"] = msg.Content ?? ""
});
}
}
else if (msg.Role == "assistant" && msg.Type == "tool_use")
{
JsonElement inputEl;
try { inputEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
var toolBlock = new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl };
if (messages.Count > 0 && messages[^1]["role"] as string == "assistant")
{
var existing = messages[^1]["content"];
if (existing is List<object> blocks)
{
blocks.Add(toolBlock);
}
else if (existing is string s)
{
messages[^1]["content"] = new List<object>
{
new { type = "text", text = s },
toolBlock
};
}
}
else
{
messages.Add(new Dictionary<string, object>
{
["role"] = "assistant",
["content"] = new List<object> { toolBlock }
});
}
}
else if (msg.Role == "user" && msg.Type == "tool_result")
{
var toolResultBlock = new
{
type = "tool_result",
tool_use_id = msg.ToolCallId,
content = msg.Content ?? "",
is_error = msg.IsError
};
if (messages.Count > 0 && messages[^1]["role"] as string == "user" &&
messages[^1]["content"] is List<object> existingResults)
{
existingResults.Add(toolResultBlock);
}
else
{
messages.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = new List<object> { toolResultBlock }
});
}
}
}
}
return messages;
}
// ── Telegram API ──
public async Task SendMessage(long chatId, string text, string parseMode = null)
{
var payload = new Dictionary<string, object>
{
["chat_id"] = chatId,
["text"] = text
};
if (!string.IsNullOrEmpty(parseMode))
payload["parse_mode"] = parseMode;
await CallTelegramApi("sendMessage", payload);
}
private async Task SendLongMessage(long chatId, string text)
{
// Telegram-Limit: 4096 Zeichen
const int maxLen = 4096;
if (text.Length <= maxLen)
{
await SendMessage(chatId, text);
return;
}
// An Zeilengrenzen splitten
var remaining = text;
while (remaining.Length > 0)
{
string chunk;
if (remaining.Length <= maxLen)
{
chunk = remaining;
remaining = "";
}
else
{
var splitAt = remaining.LastIndexOf('\n', maxLen - 1);
if (splitAt < maxLen / 2) splitAt = maxLen;
chunk = remaining[..splitAt];
remaining = remaining[splitAt..].TrimStart('\n');
}
await SendMessage(chatId, chunk);
if (remaining.Length > 0)
await Task.Delay(100); // Rate-Limiting
}
}
private async Task SendChatAction(long chatId, string action)
{
await CallTelegramApi("sendChatAction", new Dictionary<string, object>
{
["chat_id"] = chatId,
["action"] = action ?? "typing"
});
}
private async Task CallTelegramApi(string method, object payload)
{
try
{
var client = _httpFactory.CreateClient();
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
await client.PostAsync($"https://api.telegram.org/bot{_botToken}/{method}", content);
}
catch
{
// Telegram API Fehler nicht propagieren
}
}
// ── Helpers ──
private HttpClient CreateAnthropicClient()
{
var client = _httpFactory.CreateClient();
client.DefaultRequestHeaders.Add("x-api-key", _anthropicToken);
client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
return client;
}
private async Task SaveError(string chainId, int iteration, string error)
{
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iteration,
Role = "system", Type = "error",
Content = error, IsError = true
});
}
private async Task FinalizeChain(string chainId, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw)
{
await _db.UpdateChain(new ChainRecord
{
ChainId = chainId,
InputTokens = usage.InputTokens,
OutputTokens = usage.OutputTokens,
CacheCreationTokens = usage.CacheCreationTokens,
CacheReadTokens = usage.CacheReadTokens,
Iterations = iterations,
StopReason = stopReason,
DurationMs = sw.ElapsedMilliseconds
});
}
private static string EscapeMd(string text)
{
if (string.IsNullOrEmpty(text)) return "";
// MarkdownV2 escape: _ * [ ] ( ) ~ ` > # + - = | { } . !
var chars = new[] { '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' };
var sb = new StringBuilder(text.Length);
foreach (var c in text)
{
if (Array.IndexOf(chars, c) >= 0) sb.Append('\\');
sb.Append(c);
}
return sb.ToString();
}
// ── Webhook registrieren ──
public async Task<string> RegisterWebhook(string baseUrl)
{
if (!IsConfigured) return "Telegram Bot nicht konfiguriert.";
var url = $"{baseUrl.TrimEnd('/')}/api/telegram/webhook";
var payload = new Dictionary<string, object>
{
["url"] = url,
["secret_token"] = _webhookSecret,
["allowed_updates"] = new[] { "message" }
};
var client = _httpFactory.CreateClient();
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var resp = await client.PostAsync($"https://api.telegram.org/bot{_botToken}/setWebhook", content);
return await resp.Content.ReadAsStringAsync();
}
public async Task<string> GetWebhookInfo()
{
if (!IsConfigured) return "Telegram Bot nicht konfiguriert.";
var client = _httpFactory.CreateClient();
var resp = await client.GetAsync($"https://api.telegram.org/bot{_botToken}/getWebhookInfo");
return await resp.Content.ReadAsStringAsync();
}
public async Task<string> DeleteWebhook()
{
if (!IsConfigured) return "Telegram Bot nicht konfiguriert.";
var client = _httpFactory.CreateClient();
var resp = await client.PostAsync($"https://api.telegram.org/bot{_botToken}/deleteWebhook", null);
return await resp.Content.ReadAsStringAsync();
}
}
// TokenUsage und ToolResult werden aus AnthropicService.cs verwendet