feat(telegram): add Telegram channel integration - #1905
salma-marei wants to merge 22 commits into
Conversation
|
@salma-marei thank you for your contribution! this is very exciting - I will review this as soon as I am able, which might take a few days |
Thank you! |
|
@copilot resolve the merge conflicts in this pull request |
Aaronontheweb
left a comment
There was a problem hiding this comment.
Thank you for this substantial contribution. We want Netclaw to support Telegram and additional transport channels.
I merged the current dev branch into this branch. I also repaired the session-storage integration. You do not need to resolve that repository drift.
The solution now builds. The focused checks passed:
- 39 Telegram actor tests
- 31 Telegram CLI tests
- 22 channel registry tests
- Slopwatch
- Copyright headers
The implementation already uses several important shared parts. These include ChannelGatewayActor, ChannelConversationActor, IChannelOutboundClient, the channel registry, common ACL results, and the attachment ingress pipeline. This is good alignment.
The principal gap is shared behavior proof. Telegram has useful standalone tests, but it does not use the channel contract fixtures. Please add fixtures for ACL, gateway, route policy, session behavior, health, shutdown, and proactive output. Keep Telegram-specific tests for unique behavior.
Telegram.Bot may own poll recovery after startup. Therefore, Telegram does not need to copy the WebSocket lifecycle actor exactly. It still needs equivalent startup cleanup, fatal-versus-transient failure behavior, health state, and deterministic proof.
I added focused inline comments with examples and acceptance checks. Please ask if a current Netclaw pattern is unclear.
| protected override string EventIdOf(TelegramInboundMessage message) => | ||
| $"{message.ChatId}:{message.MessageId}"; | ||
|
|
||
| protected override string ThreadKeyOf(TelegramInboundMessage message) => "chat"; |
There was a problem hiding this comment.
Telegram supports forum topics through MessageThreadId. This constant makes every topic in one group use the same Netclaw session.
A conversation in topic A can then affect context and replies in topic B.
Please preserve MessageThreadId in the inbound model and outbound calls. A clear rejection for topic messages is also safe for the first version.
A test with one ChatId and two topic IDs should produce two session IDs.
|
|
||
| var token = options.BotToken.RequireValid("Telegram bot token"); | ||
| _stopSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); | ||
| _client = new TelegramBotClient(token.Value, cancellationToken: _stopSource.Token); |
There was a problem hiding this comment.
This construction hides the SDK client from tests. Telegram.Bot exposes ITelegramBotClient, so a fake can control startup, messages, errors, and downloads.
Please inject a small client factory or wrapper. Then add tests for a failed first start and a successful second start.
This seam will also make the shared health and shutdown fixtures practical.
| parseMode: ParseMode.Html, | ||
| cancellationToken: cancellationToken).ConfigureAwait(false); | ||
| } | ||
| catch (ApiRequestException) |
There was a problem hiding this comment.
This catch treats every Telegram API error as an HTML error. A bad token, rate limit, or bad destination causes a second send.
Please retry plain text only for the documented entity-parse error. Other errors should keep the original failure.
Add one parse-error test and one authorization-error test.
| { | ||
| var telegramFile = await client.GetFile(file.FileId, cancellationToken).ConfigureAwait(false); | ||
| await using var stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None); | ||
| await client.DownloadFile(telegramFile, stream, cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
The byte limit applies after the complete file reaches disk. A false or absent provider size can exceed the local policy before rejection.
Please stop the copy when total bytes exceed maxBytes. StreamingAttachmentDownloader shows the current bounded-copy pattern.
Add a fake oversized stream. Verify that the copy stops and the partial file disappears.
| _logger.LogInformation("Telegram channel connected."); | ||
| Console.WriteLine("Telegram channel connected. The bot is ready for messages."); | ||
| } | ||
| catch (Exception ex) |
There was a problem hiding this comment.
Telegram.Bot can manage poll recovery after startup. This code does not need to copy the WebSocket lifecycle actor exactly.
The initial start path still needs supervision. If GetMe fails, TelegramTransport keeps _client, this method swallows the error, and no retry starts.
Please clean partial state and classify fatal versus transient failures. Prove that a transient fault retries and a fatal token fault stays offline.
SlackChannel provides the closest supervisor pattern. Discord and Mattermost show the shared actor model.
| Enter | ||
| Wait+Screen@10s /Which channels would you like to connect/ | ||
| Wait+Screen@10s /Slack/ | ||
| Wait+Screen@10s /Telegram/ |
There was a problem hiding this comment.
This line proves only that the picker shows Telegram. It does not exercise typed input, save, re-entry, or persisted values.
Please add one native Telegram configuration flow and a semantic assertion. Verify the canonical chat ID and encrypted token.
Please add Telegram to the headless typed-key cases too.
|
@salma-marei I had Codex write the above review comments after I discussed the results with it in detail. Overall I think this is a great start and I wanted to give you feedback in case you want to learn the Netclaw internals and address these implementation issues yourself - if you'd prefer to have me take a run at it, please let me know. Happy to do either and I appreciate your contribution. |
|
@Aaronontheweb Thank you for the feedback! I’ll take a look at the comments and I’d like to try addressing them myself. |
- Add ToRichHtml that emits bordered table markup for the rich message API - Send header rows as th cells so Telegram styles them as headers - Route table replies through SendRichMessage with flattened fallback - Log loudly when Telegram rejects a rich table message - Keep classic ToHtml output byte-identical for table-free replies
Telegram.Bot exposes its API methods as extension methods and its polling events only on the concrete TelegramBotClient, so tests cannot fake the SDK type directly. Add a small Netclaw-owned client interface with a production adapter, created per start attempt through an injected factory whose polling-lifetime token the transport owns. StartAsync now commits client and CTS state only after GetMe and event subscriptions succeed; a failed start disposes the temporary CTS and rethrows the original exception, leaving the transport startable again. Addresses the maintainer's PR netclaw-dev#1905 inline comment about the hidden SDK construction blocking the shared health and shutdown fixtures.
The formatted-send fallback treated every ApiRequestException as an HTML formatting failure: a bad token, rate limit, or bad destination triggered a second doomed send and the original failure was lost. Restrict the plain-text retry to Telegram's documented entity-parse rejection (HTTP 400 with a "can't parse entities" description). All other API errors keep the original exception. Covers the auth and parse-error acceptance checks from the PR netclaw-dev#1905 review.
The byte ceiling was checked only after the SDK had copied the whole file to disk, so a false or absent provider size let oversized attachments land fully in the staging directory before rejection. Wrap the destination in a bounded write-stream that rejects the crossing chunk mid-copy (same semantics as StreamingAttachmentDownloader), keep the provider-reported size as a fast-fail precheck only, and preserve the delete-partial-file cleanup. This commit also lands the messageThreadId plumbing on the transport, the client interface, and the inbound model that the forum-topic session support (next commit) builds on, since the signatures are shared. Covers the oversized-stream and success-path acceptance checks from the PR netclaw-dev#1905 review.
StartAsync swallowed every connect failure, leaving the channel offline until a daemon restart. Classify startup failures through a conservative classifier (401 unauthorized and missing configuration are fatal; network, rate-limit, and unknown failures are transient) and retry transient faults in the background with exponential backoff driven by TimeProvider. The supervisor ends on the first successful start — Telegram.Bot owns poll recovery afterwards — and a fatal found during retry stays offline. StopAsync cancels and awaits the retry task before transport disposal. Gateway creation and event subscription are idempotent across retries. Covers the transient-retry and fatal-token acceptance checks from the PR netclaw-dev#1905 review.
ThreadKeyOf collapsed every message in a group onto one session, so a conversation in topic A leaked context into topic B. Key sessions by MessageThreadId (falling back to the historical "chat" key so existing non-topic session ids are unchanged), route callback queries back to their originating topic's session, and carry the session's topic id through text, approval, document, and error replies. Proactive and reminder sessions stay chat-scoped and never invent a topic. The acceptance flow proves one chat with two topics produces two session ids, that reusing a topic reuses its session, and that replies carry the originating topic id back to Telegram.
ApplyCredentials handled Slack, Discord, and Mattermost but had no Telegram case: a rotated Telegram bot token was validated, then silently dropped while the UI reported "Credential changes saved." Add the missing case so the rotation lands on the step model and persists through the normal encrypted secrets write.
Extend the headless typed-key channel tests with Telegram: first-time setup and credential rotation now cover the Telegram adapter, the typed non-canonical chat id -001001234 is proven to persist as the probe- canonical -1001234, and the persisted bot token is proven to be stored encrypted (ciphertext, decrypting back to the typed value). Add the native config-telegram.tape management flow (typed rotation, typed allowed-user edit, re-entry, persisted-state assertions) and register it in the light smoke suite.
Summary
Add Telegram as a supported NetClaw channel.
Changes
netclaw initandnetclaw config.Validation
git diff --checkpasses.Local test notes
The full local suite reported three unrelated failures:
This branch does not modify those tests or features.
The native Linux and macOS smoke suites were not available in the Windows development environment. GitHub Actions will run them.