feat: Telegram Bot Integration

- TelegramService: Non-Streaming Claude Chat mit Tool-Loop,
  Commands (/start, /link, /session, /new, /clear, /status, /unlink),
  Nachrichten-Splitting bei >4096 Zeichen, Typing-Indicator,
  History-Aufbau aus DB, Token-Usage-Tracking
- TelegramEndpoint: Webhook (POST, no-auth), Link-Code-Generierung
  (POST, auth), Webhook-Registrierung + Info (Admin)
- DatabaseService: telegram_links Tabelle mit Link-Code-Flow
  (6-stelliger Code, 5 Min Ablauf), GetUserSessions
- AuthMiddleware: /api/telegram/webhook von Auth ausgenommen
- Session-Verknüpfung via Link-Code (Web-UI generiert Code,
  Telegram /link CODE löst ein)
- TelegramLinkRecord Model
This commit is contained in:
Meleki committed 2026-03-03 06:30:05 +01:00
1 parent 81714b1f4c
commit 0dd7c38751
7 files changed
+1108 -5

No files matched your search

+840
View File
@@ -0,0 +1,840 @@
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
{
"opus" => "claude-opus-4-6",
"sonnet" => "claude-sonnet-4-6",
"haiku" => "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();
}
}
// TokenUsage und ToolResult werden aus AnthropicService.cs verwendet