using System.Text.Json; using ClawdDotNet.Core.State; using ClawdDotNet.Core.Tools; using Microsoft.Extensions.Logging; using Telegram.Bot; using Telegram.Bot.Types; namespace ClawdDotNet.Tools.Telegram; public sealed class TelegramTool : IAgentTool, IToolJobProvider { public string Name => "Telegram"; /// #715: Brings text from outside into the context — messages from anyone. public bool ReadsForeignContent => true; public string Description => "Allows sending and receiving messages via Telegram."; public JsonElement InputSchema { get; } = JsonDocument.Parse(""" { "type": "object", "properties": { "action": { "type": "string", "enum": ["send_message", "get_updates"], "description": "The action to execute" }, "chatId": { "type": "string", "description": "The ID of the chat (optional if default ID is configured)" }, "text": { "type": "string", "description": "The text to send (only for send_message)" }, "limit": { "type": "integer", "description": "Maximum number of messages to fetch (only for get_updates)" } }, "required": ["action"] } """).RootElement.Clone(); // ─── IAgentTool ─── public async Task ExecuteAsync( JsonElement input, AgentToolContext context, CancellationToken ct) { var action = input.GetProperty("action").GetString() ?? throw new ArgumentException("'action' is required"); var botToken = context.ToolConfig.TryGetValue("botToken", out var bt) ? bt?.ToString() : null; if (string.IsNullOrWhiteSpace(botToken)) { return ToolResult.Fail("Configuration error: 'botToken' is not defined."); } var botClient = new TelegramBotClient(botToken); try { return action switch { "send_message" => await HandleSendMessageAsync(botClient, input, context, ct), "get_updates" => await HandleGetUpdatesAsync(botClient, input, context, ct), _ => ToolResult.Fail($"Unknown action: {action}") }; } catch (Exception ex) { context.Logger.LogError(ex, "Error in Telegram action {Action}", action); return ToolResult.Fail($"Error: {ex.Message}"); } } // ─── IToolJobProvider ─── public IReadOnlyList GetJobDefinitions() => [ new("telegram_poll", "Telegram Polling", "Periodically checks for new Telegram messages and wakes the agent on new messages.") ]; public async Task ExecuteJobAsync( string jobTypeId, IReadOnlyDictionary toolConfig, IStateStore stateStore, ILogger logger, CancellationToken ct, string? agentId = null, string? workspacePath = null) { if (jobTypeId != "telegram_poll") return ToolJobResult.NoAction($"Unknown job type: {jobTypeId}"); var botToken = toolConfig.TryGetValue("botToken", out var bt) ? bt?.ToString() : null; if (string.IsNullOrWhiteSpace(botToken)) return ToolJobResult.NoAction("No botToken configured"); var client = new TelegramBotClient(botToken); var offsetKey = "telegram_poll_offset"; var lastOffsetStr = await stateStore.GetAsync(offsetKey, ct); var lastOffset = int.TryParse(lastOffsetStr, out var o) ? o : 0; try { var updates = await client.GetUpdates( offset: lastOffset, limit: 100, cancellationToken: ct); var messages = updates .Where(u => u.Message is not null) .Where(u => IsChatIdAllowed(u.Message!.Chat.Id.ToString(), toolConfig)) .ToList(); if (messages.Count == 0) { if (updates.Length > 0) { var newOffset = updates.Max(u => u.Id) + 1; await stateStore.SetAsync(offsetKey, newOffset.ToString(), ct); } return ToolJobResult.NoAction("Polled: 0 new messages"); } var newOffsetFinal = updates.Max(u => u.Id) + 1; await stateStore.SetAsync(offsetKey, newOffsetFinal.ToString(), ct); var summary = string.Join("\n", messages.Select(u => { var from = u.Message!.From?.Username ?? u.Message.From?.FirstName ?? "Unknown"; var text = u.Message.Text ?? "(no text)"; return $"- {from}: {text}"; })); var wakeMessage = $"[Telegram] {messages.Count} new message(s) received:\n\n{summary}"; logger.LogInformation("Telegram Poll: {Count} new messages", messages.Count); return ToolJobResult.Wake(wakeMessage, $"Polled: {messages.Count} new messages"); } catch (Exception ex) { logger.LogError(ex, "Telegram polling failed"); return ToolJobResult.NoAction($"Polling error: {ex.Message}"); } } // ─── Private Helpers ─── private async Task HandleSendMessageAsync( ITelegramBotClient client, JsonElement input, AgentToolContext context, CancellationToken ct) { var chatIdStr = input.TryGetProperty("chatId", out var ci) ? ci.GetString() : null; var text = input.TryGetProperty("text", out var t) ? t.GetString() : null; if (string.IsNullOrWhiteSpace(chatIdStr)) { chatIdStr = context.ToolConfig.TryGetValue("defaultChatId", out var dci) ? dci?.ToString() : null; } if (string.IsNullOrWhiteSpace(chatIdStr) || string.IsNullOrWhiteSpace(text)) { return ToolResult.Fail("'chatId' (or default ID in config) and 'text' are required for send_message."); } if (!IsChatIdAllowed(chatIdStr, context.ToolConfig)) { return ToolResult.Fail($"Security error: Access to ChatId '{chatIdStr}' is not allowed."); } await client.SendMessage(new ChatId(chatIdStr), text, cancellationToken: ct); return ToolResult.Ok($"Message successfully sent to {chatIdStr}."); } private async Task HandleGetUpdatesAsync( ITelegramBotClient client, JsonElement input, AgentToolContext context, CancellationToken ct) { var limit = input.TryGetProperty("limit", out var l) ? l.GetInt32() : 10; var updates = await client.GetUpdates(limit: limit, cancellationToken: ct); var allowedUpdates = updates .Where(u => u.Message != null && IsChatIdAllowed(u.Message.Chat.Id.ToString(), context.ToolConfig)) .Select(u => new { u.Id, Message = new { u.Message!.Id, ChatId = u.Message.Chat.Id, From = u.Message.From?.Username ?? u.Message.From?.FirstName, u.Message.Text, u.Message.Date } }) .ToList(); return ToolResult.Ok(JsonSerializer.Serialize(allowedUpdates, new JsonSerializerOptions { WriteIndented = true })); } private static bool IsChatIdAllowed(string chatId, IReadOnlyDictionary toolConfig) { if (toolConfig.TryGetValue("defaultChatId", out var dci) && dci?.ToString() == chatId) return true; if (!toolConfig.TryGetValue("allowedChatIds", out var val) || val is not JsonElement je) return false; var allowed = je.EnumerateArray().Select(x => x.GetString()).ToList(); return allowed.Contains(chatId); } private bool IsChatIdAllowed(string chatId, AgentToolContext context) => IsChatIdAllowed(chatId, context.ToolConfig); }