- IAgentTool.ReadsForeignContent: Werkzeuge mit Aussenkontakt erklaeren sich selbst (WebFetch, WebMonitor, DirectAPI, Mail, RocketChat, Telegram, TelegramClient, SocialMediaManager, FTP, Database); AgentConfig.TreatAsForeignReader kann nur verschaerfen - ReportingLine: Stufe aus den Werkzeugen, "wer steht darueber", Pruefung des Wegs nach oben (nie Aenderungen an Aufgaben darueber, nur eine Ebene hoch, Fremdinhalt-Stufe gar kein Freitext) und Rahmung als untrusted_content - Engine: Nachricht und Spawn nach oben geprueft, Text der Fremdinhalt-Stufe kommt gerahmt an - Taskboard: task_create/update/comment, reopen und die Dubletten-Notiz folgen dem Berichtsweg - Dispatcher: Aufgaben von Agenten der Fremdinhalt-Stufe gehen gerahmt in den Lauf - TaskReports: Erledigt-Signal und Systemmeldung (in Pruefung, blockiert, abgebrochen) an den Auftraggeber, je Zustand einmal, gesammelt in einer offenen Aufgabe je Empfaenger - OrgBriefing nennt den Berichtsweg; WebUI zeigt Stufe und Schalter am Agenten Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
8.1 KiB
C#
220 lines
8.1 KiB
C#
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";
|
|
|
|
/// <summary>#715: Brings text from outside into the context — messages from anyone.</summary>
|
|
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<ToolResult> 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<ToolJobDefinition> GetJobDefinitions() =>
|
|
[
|
|
new("telegram_poll", "Telegram Polling",
|
|
"Periodically checks for new Telegram messages and wakes the agent on new messages.")
|
|
];
|
|
|
|
public async Task<ToolJobResult> ExecuteJobAsync(
|
|
string jobTypeId,
|
|
IReadOnlyDictionary<string, object?> 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<ToolResult> 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<ToolResult> 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<string, object?> 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);
|
|
}
|