Files
ClawdDotNet/docs/archiv/ClawdDotNet_Prompt_TelegramClient.md
T
RichardandClaude Opus 5 2853541629 Fruehjahrsputz: WinForms-Altlast entfernt, Dokumentation nachgezogen
Die Avalonia-Portierung ist abgeschlossen, damit ist die in ClawdDotNet.slnx
angekuendigte Aufgabe "WinForms-Oberflaeche entfernen" faellig. Der Stand
davor liegt unter dem Tag vor-fruehjahrsputz-2026-08.

Entfernt (56 Dateien, seit dem Herausloesen der Anwendungsschicht nicht mehr
Teil des Builds): ClawdDotNet.csproj, Program.cs, sieben frm_*-Formulare,
UI/, Models/, EmbeddedUI/, Properties/, Resources/, Services/, das alte
Anwendungssymbol und Deploy-Build.ps1 (ersetzt durch deploy/publish.py).
Dazu configs/*.json - Beispielkonfigurationen aus der Zeit vor dem
Instanzverzeichnis, auf die nur noch die alten Prompts verwiesen.

Die vier Entwicklungs-Prompts der Anfangszeit ziehen nach docs/archiv/ um,
mit README, das ihren Stand einordnet. Eine Regel darin gilt weiter - die
Pflichtfelder fetchedAt/dataAsOf/source der Internet-Tools -, deshalb
Archiv statt Loeschen; der WebSearch-Plan verweist auf den neuen Pfad.

Toter Code
- PlaceholderPageViewModel samt Ansicht: Es gibt keinen Platzhalter-Bereich
  mehr, seit alle neun Seiten portiert sind.
- Snappier als direkter Paketverweis: MongoDB.Driver loest es ohnehin auf
  dieselbe Fassung auf, der Verweis hob nichts an.

Zwei Fehler, die dabei sichtbar wurden
- Die taegliche Sicherung lief ins Leere. Die Oberflaeche bot sie an und
  schrieb Uhrzeit, Zielordner und Anzahl in die Einstellungen, aber der
  BackupScheduler wurde nirgends erzeugt. Jetzt am AppHost verdrahtet und
  in den geordneten Abbau aufgenommen.
- SettingsPageViewModel hielt die vier Sicherungs-Einstellungen doppelt.
  Aus der Ansicht waren sie laengst verschwunden, gelesen und beim
  Speichern zurueckgeschrieben wurden sie weiter: Wer die Uhrzeit auf der
  Sicherungs-Seite aenderte und danach die Einstellungen speicherte, bekam
  den alten Wert zurueck.

Pakete: keine bekannten Sicherheitsluecken mehr
- SQLitePCLRaw.bundle_e_sqlite3 auf 2.1.13 angehoben. Microsoft.Data.Sqlite
  bringt 2.1.11 mit, darin steckt GHSA-2m69-gcr7-jv3q (NU1903, hoch).
- SharpCompress bleibt als direkter Verweis stehen. Beim Aufraeumen erst
  als ungenutzt entfernt - dabei kam die von MongoDB.Driver gezogene
  Fassung 0.30.1 mit GHSA-6c8g-7p36-r338 zurueck. Der Verweis ist eine
  Anhebung, kein Ballast; das steht jetzt als Kommentar dabei.

Dokumentation
- Roadmap mit Statusblock: A1 und A3 erledigt, A2 nur zur Haelfte - Gate,
  Policy und Dienst greifen, aber keine Ansicht ruft ApproveAsync auf, ein
  gestagter Aufruf liegt unbeantwortet. Das ist jetzt Punkt 1 der Reihung.
  Rocket.Chat steht und kollidiert mit A5 (Matrix) - Entscheidung faellig.
- Avalonia-Portierungsleitfaden -> Oberflaechen-Leitfaden: kein Auftrag mehr,
  sondern Beschreibung des Stands.
- Bestandsaufnahme und Linux-Analyse als datierte Befunde gekennzeichnet;
  der teure Teil der Linux-Analyse (8.900 Zeilen WinForms) ist hinfaellig.
- Verweise auf frm_*, WebView2 und ClawdDotNet.csproj in den lebenden
  Dokumenten richtiggestellt.

Build fehlerfrei, 585 Tests gruen (6 uebersprungen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:26:14 +02:00

24 KiB
Raw Blame History

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

<PackageReference Include="WTelegramClient" Version="4.*" />

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.

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<TelegramClientManager> _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<string, Task<string>>? OnLoginCodeRequired;
    public event Func<Task<string>>?         On2FAPasswordRequired;

    public bool IsConnected => _client?.User != null;
    public User? Self       => _self;

    public TelegramClientManager(
        Config.InstanceConfig config,
        ILogger<TelegramClientManager> 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<Messages_Dialogs> 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<Messages_Chats> 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<Messages_MessagesBase> 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<IPeerInfo> 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

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<ToolResult> ExecuteAsync(
        JsonElement input, AgentToolContext ctx, CancellationToken ct)
    {
        // ---- Permission-Check: Welche Chats darf dieser Agent lesen? ----
        var config = ctx.ToolConfig.GetValueOrDefault("TelegramClient")
                     as Dictionary<string, object?> ?? new();

        var allowedChats = config.GetValueOrDefault("allowedChatIds")
                           as List<long>;   // null = alle erlaubt
        var allowedUsernames = config.GetValueOrDefault("allowedUsernames")
                               as List<string>;

        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<ToolResult> ListChatsAsync(
        List<long>? allowedChats, CancellationToken ct)
    {
        var dialogs = await _tg.GetAllDialogsAsync(ct);

        var chatList = new List<object>();
        foreach (Dialog dialog in dialogs.dialogs)
        {
            var peer = dialogs.UserOrChat(dialog);
            if (peer == null) continue;

            var chatId = dialog.Peer.ID;

            // Filter: nur erlaubte Chats anzeigen (wenn Whitelist definiert)
            if (allowedChats != null && !allowedChats.Contains(chatId))
                continue;

            var info = peer switch
            {
                User user when user.IsActive => new
                {
                    chatId     = chatId,
                    type       = "user",
                    name       = $"{user.first_name} {user.last_name}".Trim(),
                    username   = user.MainUsername,
                    unread     = dialog.UnreadCount,
                    lastMsgId  = dialog.TopMessage
                } as object,

                ChatBase chat when chat.IsActive => new
                {
                    chatId     = chatId,
                    type       = chat is Channel ch
                                   ? (ch.IsGroup ? "supergroup" : "channel")
                                   : "group",
                    name       = chat.Title,
                    username   = (chat as Channel)?.MainUsername,
                    unread     = dialog.UnreadCount,
                    lastMsgId  = dialog.TopMessage
                } as object,

                _ => null
            };

            if (info != null) chatList.Add(info);
        }

        var result = new
        {
            fetchedAt = DateTime.UtcNow,
            dataAsOf  = DateTime.UtcNow,
            source    = "telegram_client_api",
            data      = new
            {
                totalChats = chatList.Count,
                chats      = chatList
            }
        };

        return new ToolResult(true, JsonSerializer.Serialize(result));
    }

    private async Task<ToolResult> ReadMessagesAsync(
        JsonElement input, List<long>? allowedChats,
        Dictionary<string, object?> config,
        AgentToolContext ctx, CancellationToken ct)
    {
        var (peer, chatId, error) = await ResolvePeerAsync(input, allowedChats, ct);
        if (error != null) return new ToolResult(false, "", error);

        var limit = input.TryGetProperty("limit", out var l)
                    ? Math.Clamp(l.GetInt32(), 1, 100)
                    : 30;

        var messages = await _tg.GetMessagesAsync(peer!, minId: 0, limit: limit, ct: ct);

        var msgList = FormatMessages(messages);

        var result = new
        {
            fetchedAt = DateTime.UtcNow,
            dataAsOf  = DateTime.UtcNow,
            source    = $"telegram_chat_{chatId}",
            data      = new
            {
                chatId     = chatId,
                count      = msgList.Count,
                messages   = msgList
            }
        };

        return new ToolResult(true, JsonSerializer.Serialize(result));
    }

    private async Task<ToolResult> ReadNewAsync(
        JsonElement input, List<long>? allowedChats,
        Dictionary<string, object?> config,
        AgentToolContext ctx, CancellationToken ct)
    {
        var (peer, chatId, error) = await ResolvePeerAsync(input, allowedChats, ct);
        if (error != null) return new ToolResult(false, "", error);

        // Letzte bekannte Message-ID aus StateStore laden
        var stateKey = $"tgclient:{ctx.AgentId}:chat_{chatId}:lastMsgId";
        var lastIdStr = await ctx.StateStore.GetAsync(stateKey, ct);
        var lastId = int.TryParse(lastIdStr, out var id) ? id : 0;

        var limit = input.TryGetProperty("limit", out var l)
                    ? Math.Clamp(l.GetInt32(), 1, 100)
                    : 50;

        // min_id = lastId → nur Nachrichten neuer als lastId
        var messages = await _tg.GetMessagesAsync(peer!, minId: lastId, limit: limit, ct: ct);

        var msgList = FormatMessages(messages);

        // Neue Max-ID persistieren
        if (msgList.Count > 0)
        {
            var newMaxId = msgList.Max(m => m.messageId);
            await ctx.StateStore.SetAsync(stateKey, newMaxId.ToString(), ct);
        }

        var result = new
        {
            fetchedAt    = DateTime.UtcNow,
            dataAsOf     = DateTime.UtcNow,
            source       = $"telegram_chat_{chatId}",
            data         = new
            {
                chatId       = chatId,
                sinceId      = lastId,
                newCount     = msgList.Count,
                messages     = msgList
            }
        };

        return new ToolResult(true, JsonSerializer.Serialize(result));
    }

    private async Task<(InputPeer? peer, long chatId, string? error)> ResolvePeerAsync(
        JsonElement input, List<long>? allowedChats, CancellationToken ct)
    {
        long chatId = 0;
        InputPeer? peer = null;

        if (input.TryGetProperty("chatId", out var cid))
        {
            chatId = cid.GetInt64();
            if (allowedChats != null && !allowedChats.Contains(chatId))
                return (null, chatId,
                    $"Agent hat keinen Zugriff auf Chat {chatId}.");

            peer = _tg.GetInputPeerFromCache(chatId);
            if (peer == null)
            {
                // Cache befüllen durch einmaligen GetAllDialogs-Aufruf
                await _tg.GetAllDialogsAsync(ct);
                peer = _tg.GetInputPeerFromCache(chatId);
            }
        }
        else if (input.TryGetProperty("username", out var uname))
        {
            var resolved = await _tg.ResolveUsernameAsync(uname.GetString()!, ct);
            peer   = resolved?.ToInputPeer();
            chatId = peer?.ID ?? 0;

            if (allowedChats != null && !allowedChats.Contains(chatId))
                return (null, chatId,
                    $"Agent hat keinen Zugriff auf Chat @{uname.GetString()}.");
        }

        if (peer == null)
            return (null, 0, "chatId oder username muss angegeben werden.");

        return (peer, chatId, null);
    }

    private static List<FormattedMessage> FormatMessages(Messages_MessagesBase messages)
    {
        var result = new List<FormattedMessage>();

        foreach (var msgBase in messages.Messages)
        {
            var from = messages.UserOrChat(msgBase.From ?? msgBase.Peer);
            var fromName = from switch
            {
                User u    => $"{u.first_name} {u.last_name}".Trim(),
                ChatBase c => c.Title,
                _         => "Unknown"
            };

            if (msgBase is Message msg)
            {
                result.Add(new FormattedMessage(
                    messageId:  msg.ID,
                    date:       msg.Date,
                    from:       fromName,
                    fromId:     msgBase.From?.ID ?? 0,
                    text:       msg.message,
                    hasMedia:   msg.media != null,
                    mediaType:  msg.media?.GetType().Name,
                    replyToId:  (msg.reply_to as MessageReplyHeader)?.reply_to_msg_id,
                    forwardFrom: msg.fwd_from != null
                        ? msg.fwd_from.from_name ?? "forwarded"
                        : null,
                    views:      msg.views
                ));
            }
        }

        return result.OrderBy(m => m.messageId).ToList();
    }

    private sealed record FormattedMessage(
        int       messageId,
        DateTime  date,
        string    from,
        long      fromId,
        string?   text,
        bool      hasMedia,
        string?   mediaType,
        int?      replyToId,
        string?   forwardFrom,
        int?      views
    );
}

AgentConfig-Beispiel

{
  "agentId": "telegram-scout",
  "displayName": "Telegram News-Scout",
  "model": "google/gemini-flash-1.5",
  "systemPrompt": "Du überwachst Telegram-Gruppen auf relevante Finanznachrichten und Trading-Signale. Fasse neue Nachrichten zusammen und bewerte ihre Relevanz. Verwende niemals Daten ohne fetchedAt-Feld.",
  "tools": {
    "TelegramClient": {
      "allowedChatIds": [1001234567890, 1009876543210],
      "allowedUsernames": ["aktien_chat", "crypto_signals_de"]
    },
    "Database": {
      "connectionString": "...",
      "allowedTables": ["telegram_messages", "signal_archive"]
    }
  },
  "scheduler": {
    "cron": "*/15 * * * *",
    "runOnStart": true
  },
  "loopGuard": {
    "maxSteps": 10,
    "maxTokens": 30000
  }
}

Ein Agent ohne TelegramClient-Eintrag in seiner Config bekommt das Tool gar nicht erst in seinem LLM-Tool-Set angezeigt (normales Permission-Verhalten). Ein Agent MIT Config aber ohne allowedChatIds (= null) darf alle Chats lesen.


InstanceConfig-Erweiterung

// Config/InstanceConfig.cs — neues optionales Feld:

public sealed class InstanceConfig
{
    // ... bestehende Felder ...

    public TelegramClientConfig? TelegramClient { get; set; }
}

public sealed class TelegramClientConfig
{
    public int    ApiId        { get; set; }           // von https://my.telegram.org/apps
    public string ApiHash      { get; set; } = "";     // von https://my.telegram.org/apps
    public string PhoneNumber  { get; set; } = "";     // z.B. "+491701234567"
    public string? Password2FA { get; set; }           // optional, nur bei aktivierter 2FA
}
// In stock-team.json:
{
  "instanceId": "stock-01",
  "instanceName": "Aktien-Team",
  "openRouterApiKey": "sk-or-...",
  "workingDirectory": "./data/stock/",
  "webServerPort": 8081,
  "telegramClient": {
    "apiId": 12345678,
    "apiHash": "abcdef1234567890abcdef1234567890",
    "phoneNumber": "+491701234567"
  },
  "agents": [ ... ]
}

Interaktiver Login in WinForms

Der erste Login erfordert einen Verifizierungscode. Dieser wird über das bestehende Chat-UI in frm_main abgefragt — nicht über die Konsole.

In frm_main oder Program.cs beim Start:

var tgManager = provider.GetRequiredService<TelegramClientManager>();

// UI-Handler für Code-Eingabe registrieren
tgManager.OnLoginCodeRequired = async (prompt) =>
{
    // Auf UI-Thread: InputBox oder Chat-Nachricht anzeigen
    string? code = null;
    mainForm.Invoke(() =>
    {
        code = Microsoft.VisualBasic.Interaction.InputBox(
            prompt, "Telegram Verifizierung", "");
    });
    return code ?? "";
};

tgManager.On2FAPasswordRequired = async () =>
{
    string? pw = null;
    mainForm.Invoke(() =>
    {
        pw = Microsoft.VisualBasic.Interaction.InputBox(
            "Bitte 2FA-Passwort eingeben:", "Telegram 2FA", "");
    });
    return pw ?? "";
};

// Verbindung herstellen (nutzt Session-Datei wenn vorhanden)
try
{
    await tgManager.ConnectAsync(CancellationToken.None);
}
catch (Exception ex)
{
    _logger.LogError(ex, "Telegram: Login fehlgeschlagen");
    // App startet trotzdem — TelegramClient-Tool meldet "nicht verbunden"
}

Nach erfolgreichem Login wird die Session-Datei ./data/{instanceId}/telegram_{instanceId}.session gespeichert. Alle weiteren Starts loggen automatisch ein — kein Code mehr nötig.


Sicherheitsregeln

  1. NUR LESEN — Das Tool implementiert keine Sende-Funktionen. Es gibt keine send_message-Action. Der TelegramClientManager exponiert bewusst keine SendMessageAsync-Methode.

  2. Session-Datei ist sensibel — Sie enthält die Auth-Keys für den Telegram-Account. Die Datei liegt im WorkingDirectory und darf NICHT vom FileRW-Tool erreichbar sein. In der AgentConfig für FileRW darf der rootPath NIEMALS auf das WorkingDirectory zeigen wenn dort die Session-Datei liegt. Empfehlung: Session-Datei in einem Unterordner ./data/{instanceId}/sessions/ speichern, der für kein FileRW-Tool als rootPath konfiguriert ist.

  3. Chat-Whitelist pro Agent — Über allowedChatIds kann eingeschränkt werden welche Chats ein Agent lesen darf. Ein Finanzmarkt-Agent hat keinen Zugriff auf private DMs. Ein SEO-Agent hat keinen Zugriff auf Trading-Gruppen.

  4. Rate Limiting — Die Telegram Client API hat undokumentierte Rate-Limits. Bei zu vielen Requests kommt ein FLOOD_WAIT_X Error. Der TelegramClientManager muss FloodException abfangen und await Task.Delay(ex.X * 1000) warten bevor er den Call wiederholt. Empfehlung: mindestens 1 Sekunde Pause zwischen aufeinanderfolgenden API-Calls (der SemaphoreSlim allein reicht nicht).


Besonderheiten von WTelegramClient

Terminology-Mapping

In der Telegram Client API unterscheiden sich die Begriffe von der Benutzeroberfläche:

Telegram-App API-Bezeichnung C#-Typ
Gruppe (klein) Chat Chat
Gruppe (groß) Channel mit IsGroup Channel (IsGroup)
Kanal Channel ohne IsGroup Channel (!IsGroup)
Privatnachricht User User

access_hash-Problem

Telegram-API-Calls benötigen für die meisten Peers einen access_hash. Dieser wird automatisch gecacht wenn vorher Messages_GetAllDialogs() oder Messages_GetAllChats() aufgerufen wurde. Deshalb MUSS bei jedem Start (nach Login) einmalig GetAllDialogsAsync() aufgerufen werden, bevor GetMessagesAsync() funktioniert.

Session-Datei

  • Pfad konfigurierbar über session_pathname in der Config-Callback
  • Verschlüsselt (Standard-Verschlüsselung von WTelegramClient)
  • NICHT zwischen Rechnern portierbar (an Hardware gebunden)
  • Bei Session-Problemen: Datei löschen → neuer Login erforderlich

Implementierungsreihenfolge (für Claude Code)

  1. TelegramClientConfig zu InstanceConfig hinzufügen
  2. TelegramClientManager implementieren (Singleton, SemaphoreSlim, Rate-Limit-Schutz)
  3. TelegramClientTool implementieren (list_chats, read_messages, read_new)
  4. Host: Login-Flow in Program.cs / frm_main integrieren (InputBox für Code)
  5. Sicherheits-Check: Session-Pfad darf nicht in FileRW-rootPath liegen
  6. xUnit-Tests: FormatMessages-Serialisierung, Chat-Whitelist-Filter, Rate-Limit-Handling
  7. Beispiel-Config ergänzen: stock-team.json mit TelegramClient-Eintrag

Beginne mit Schritt 1 dieses Abschnitts.