# ClawdDotNet – Prompt-Anhang: Tool "TelegramClient"
Dieser Abschnitt ergänzt die bestehenden Prompt-Anhänge und definiert das Tool
`TelegramClient`, das über die Telegram Client API (MTProto) auf den persönlichen
Telegram-Account des Nutzers zugreift. Dieses Tool ist NICHT der bereits vorhandene
Telegram Bot — es nutzt die User-API und kann damit auch Nachrichten aus privaten
Gruppen lesen, in denen der Nutzer Mitglied ist.
**Nur Lese-Zugriff. Kein Senden von Nachrichten.**
---
## Voraussetzungen
### Telegram API Credentials
Der Nutzer muss einmalig auf https://my.telegram.org/apps eine App registrieren.
Ergebnis: `api_id` (Integer) und `api_hash` (String). Diese Werte repräsentieren
die Anwendung (nicht den User) und werden in der InstanceConfig gespeichert.
### Erstmalige Authentifizierung
Beim allerersten Start muss der Nutzer sich interaktiv authentifizieren:
1. Telefonnummer eingeben
2. Verifizierungscode eingeben (kommt per Telegram-App, SMS oder Anruf)
3. Optional: 2FA-Passwort eingeben
Danach wird eine Session-Datei gespeichert. Alle weiteren Starts verwenden
diese Session automatisch — kein erneuter Login nötig.
### NuGet
```xml
```
---
## Architektur: Shared Client, Read-Only Access
### Warum ein Shared Client?
Die Telegram Client API erlaubt pro Telefonnummer nur EINE aktive MTProto-Verbindung.
Mehrere Agent-Runs dürfen NICHT jeweils einen eigenen WTelegram.Client instanziieren —
das würde die Session invalidieren und den Login auf dem echten Telegram-Client killen.
Lösung: Ein einziger `WTelegram.Client` wird im Host instanziiert und als Singleton
an alle Agenten weitergegeben. Das Tool selbst ist stateless und greift über den
Shared Client auf Telegram zu.
```
Host (Program.cs)
└─ TelegramClientManager (Singleton)
└─ WTelegram.Client (eine Instanz pro Prozess)
├─ Agent A: TelegramClient-Tool → liest Gruppe "Aktien-Chat"
├─ Agent B: TelegramClient-Tool → liest Gruppe "Krypto-Signals"
└─ Agent C: TelegramClient-Tool → liest DMs
```
### Concurrency
WTelegram.Client ist NICHT thread-safe für gleichzeitige API-Calls.
Der `TelegramClientManager` muss alle Aufrufe über einen `SemaphoreSlim(1,1)`
serialisieren. Da wir nur lesen und die Calls schnell sind (<500ms), ist
die Serialisierung kein Bottleneck.
---
## TelegramClientManager
**Datei: `Host/Services/TelegramClientManager.cs`**
Verwaltet die einzige WTelegram.Client-Instanz. Wird im Host als Singleton registriert.
```csharp
namespace ClawdDotNet.Host.Services;
using WTelegram;
using TL;
public sealed class TelegramClientManager : IAsyncDisposable
{
private Client? _client;
private User? _self;
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly ILogger _logger;
// Config-Werte aus InstanceConfig
private readonly int _apiId;
private readonly string _apiHash;
private readonly string _phoneNumber;
private readonly string _sessionPath;
private readonly string? _2faPassword;
// Event für interaktive Login-Aufforderung (Code-Eingabe via UI)
public event Func>? OnLoginCodeRequired;
public event Func>? On2FAPasswordRequired;
public bool IsConnected => _client?.User != null;
public User? Self => _self;
public TelegramClientManager(
Config.InstanceConfig config,
ILogger logger)
{
_logger = logger;
var tgConfig = config.TelegramClient
?? throw new InvalidOperationException("TelegramClient config missing in InstanceConfig");
_apiId = tgConfig.ApiId;
_apiHash = tgConfig.ApiHash;
_phoneNumber = tgConfig.PhoneNumber;
_sessionPath = Path.Combine(config.WorkingDirectory, $"telegram_{config.InstanceId}.session");
_2faPassword = tgConfig.Password2FA;
}
public async Task ConnectAsync(CancellationToken ct)
{
// WTelegram.Client mit Config-Callback instanziieren
_client = new Client(ConfigCallback, _sessionPath);
// Logging an ILogger umleiten
Helpers.Log = (lvl, msg) =>
_logger.Log((Microsoft.Extensions.Logging.LogLevel)lvl, "WTelegram: {Message}", msg);
_self = await _client.LoginUserIfNeeded();
_logger.LogInformation(
"Telegram: logged in as {Name} (id {Id})",
_self.first_name, _self.id);
}
private string? ConfigCallback(string what) => what switch
{
"api_id" => _apiId.ToString(),
"api_hash" => _apiHash,
"phone_number" => _phoneNumber,
"session_pathname" => _sessionPath,
// Interaktiver Code — wird über Event an die UI weitergeleitet
"verification_code" => OnLoginCodeRequired != null
? OnLoginCodeRequired("Bitte Telegram-Verifizierungscode eingeben:").Result
: throw new InvalidOperationException(
"Verification code required but no UI handler registered. " +
"Connect OnLoginCodeRequired to prompt the user."),
// 2FA-Passwort — aus Config oder interaktiv
"password" => _2faPassword
?? (On2FAPasswordRequired != null
? On2FAPasswordRequired().Result
: throw new InvalidOperationException(
"2FA password required but not configured.")),
_ => null // Defaults für alles andere
};
/// Alle Dialoge (Chats, Gruppen, Kanäle, DMs) auflisten
public async Task GetAllDialogsAsync(CancellationToken ct)
{
await _gate.WaitAsync(ct);
try { return await _client!.Messages_GetAllDialogs(); }
finally { _gate.Release(); }
}
/// Alle Gruppen/Kanäle auflisten (ohne DMs)
public async Task GetAllChatsAsync(CancellationToken ct)
{
await _gate.WaitAsync(ct);
try { return await _client!.Messages_GetAllChats(); }
finally { _gate.Release(); }
}
/// Nachrichten aus einem Chat/Kanal/Gruppe lesen
/// peer: Chat-ID oder Username
/// minId: nur Nachrichten neuer als diese ID (für Delta-Abfragen)
/// limit: max. Anzahl Nachrichten
public async Task GetMessagesAsync(
InputPeer peer, int minId = 0, int limit = 50, CancellationToken ct = default)
{
await _gate.WaitAsync(ct);
try
{
return await _client!.Messages_GetHistory(
peer, offset_id: 0, offset_date: default,
add_offset: 0, limit: limit, max_id: 0, min_id: minId, hash: 0);
}
finally { _gate.Release(); }
}
/// Peer über Username oder Chat-ID auflösen
public async Task ResolveUsernameAsync(string username, CancellationToken ct)
{
await _gate.WaitAsync(ct);
try { return await _client!.Contacts_ResolveUsername(username.TrimStart('@')); }
finally { _gate.Release(); }
}
/// Peer über bekannte Chat-ID auflösen (benötigt vorherigen GetAllChats/Dialogs Aufruf)
public InputPeer? GetInputPeerFromCache(long chatId)
=> _client!.GetInputPeerID(chatId);
public async ValueTask DisposeAsync()
{
_client?.Dispose();
_gate.Dispose();
}
}
```
---
## TelegramClientTool — das IAgentTool
**Datei: `ClawdDotNet.Tools.TelegramClient/TelegramClientTool.cs`**
```csharp
namespace ClawdDotNet.Tools.TelegramClient;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Host.Services; // TelegramClientManager
using TL;
using System.Text.Json;
public sealed class TelegramClientTool : IAgentTool
{
// Manager wird per DI injiziert (Singleton im Host)
private readonly TelegramClientManager _tg;
public TelegramClientTool(TelegramClientManager tg) => _tg = tg;
public string Name => "TelegramClient";
public string Description => """
Liest Nachrichten aus dem persönlichen Telegram-Account des Nutzers.
Zugriff auf alle Chats, Gruppen und Kanäle in denen der Nutzer Mitglied ist.
NUR LESEN — kein Senden, kein Löschen, kein Bearbeiten.
Aktionen: list_chats, read_messages, read_new
""";
public JsonElement InputSchema => JsonDocument.Parse("""
{
"type": "object",
"required": ["action"],
"properties": {
"action": {
"type": "string",
"enum": ["list_chats", "read_messages", "read_new"],
"description": "list_chats: alle Chats/Gruppen/Kanäle auflisten. read_messages: letzte N Nachrichten aus einem Chat lesen. read_new: nur neue Nachrichten seit letztem Abruf."
},
"chatId": {
"type": "integer",
"description": "Chat-ID aus list_chats Ergebnis. Erforderlich für read_messages und read_new."
},
"username": {
"type": "string",
"description": "Alternativ zu chatId: @username einer Gruppe/Person auflösen."
},
"limit": {
"type": "integer",
"description": "Max. Anzahl Nachrichten (default: 30, max: 100)"
}
}
}
""").RootElement;
public async Task ExecuteAsync(
JsonElement input, AgentToolContext ctx, CancellationToken ct)
{
// ---- Permission-Check: Welche Chats darf dieser Agent lesen? ----
var config = ctx.ToolConfig.GetValueOrDefault("TelegramClient")
as Dictionary ?? new();
var allowedChats = config.GetValueOrDefault("allowedChatIds")
as List; // null = alle erlaubt
var allowedUsernames = config.GetValueOrDefault("allowedUsernames")
as List;
if (!_tg.IsConnected)
return new ToolResult(false, "",
"Telegram-Client ist nicht verbunden. Bitte zuerst authentifizieren.");
var action = input.GetProperty("action").GetString()!;
return action switch
{
"list_chats" => await ListChatsAsync(allowedChats, ct),
"read_messages" => await ReadMessagesAsync(input, allowedChats, config, ctx, ct),
"read_new" => await ReadNewAsync(input, allowedChats, config, ctx, ct),
_ => new ToolResult(false, "", $"Unknown action: {action}")
};
}
private async Task ListChatsAsync(
List? allowedChats, CancellationToken ct)
{
var dialogs = await _tg.GetAllDialogsAsync(ct);
var chatList = new List