Skip to content

feat(telegram): add Telegram channel integration - #1905

Open
salma-marei wants to merge 22 commits into
netclaw-dev:devfrom
salma-marei:telegram-integration
Open

salma-marei wants to merge 22 commits into
netclaw-dev:devfrom
salma-marei:telegram-integration

Conversation

@salma-marei

Copy link
Copy Markdown

Summary

Add Telegram as a supported NetClaw channel.

Changes

  • Receive Telegram messages, photos, albums, and files.
  • Send generated files to Telegram.
  • Support private chats and group mention rules.
  • Add Telegram tool-approval buttons.
  • Support proactive messages to approved users and groups.
  • Add Telegram to netclaw init and netclaw config.
  • Add per-user and per-group audience controls.
  • Validate bot tokens and chat IDs through the Telegram API.
  • Add Telegram health and doctor checks.
  • Restore Telegram chat sessions after daemon restarts.
  • Format headings, lists, quotes, code, and strikethrough.
  • Show Telegram typing indicators.
  • Update the configuration schema and operational documentation.

Validation

  • Full solution build passes with 0 warnings and 0 errors.
  • All 55 Telegram-focused tests pass.
  • Slopwatch passes with 0 issues.
  • Copyright-header verification passes.
  • git diff --check passes.
  • Manual end-to-end testing passes for:
    • Personal and Public private chats
    • Team groups with mention-only behavior
    • Tool approvals
    • Inbound and outbound files
    • Proactive private and group messages
    • Formatting
    • Restart recovery
    • Health and doctor checks

Local test notes

The full local suite reported three unrelated failures:

  • Two memory-prompt snapshot tests
  • One reminder retry test

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.

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

@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

@salma-marei

Copy link
Copy Markdown
Author

@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!

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

@copilot resolve the merge conflicts in this pull request

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

@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.

@salma-marei

Copy link
Copy Markdown
Author

@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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants