feat(channels)!: migrate rocketchat from in-process adapter to sidecar#5298
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94b914b761
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # reply lands in the same thread (rather than starting a child | ||
| # under the inbound message). Otherwise use the inbound `_id`, | ||
| # which Rocket.Chat accepts as the new thread's parent. | ||
| thread_id = tmid_str or (msg_id or None) |
There was a problem hiding this comment.
Preserve root-level replies for non-thread messages
_parse_message now sets thread_id to msg_id whenever inbound tmid is absent, so every top-level room message is converted into a threaded reply target. Because on_send unconditionally forwards cmd.thread_id as tmid, normal (non-thread) conversations will be posted into new threads instead of the room root, which changes delivery semantics and can hide bot replies from users who are watching only the main channel. This should only set a thread id when the inbound event was already in a thread (or when explicitly requested), not for all top-level messages.
Useful? React with 👍 / 👎.
Deploying librefang-docs with
|
| Latest commit: |
94b914b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9fe15cfd.librefang-docs-web.pages.dev |
| Branch Preview URL: | https://feat-channels-rocketchat-sid.librefang-docs-web.pages.dev |
The in-process librefang-channels::rocketchat adapter (RocketChatAdapter, 585 lines: 2 s per-room polling of GET /api/v1/channels.history with X-Auth-Token / X-User-Id PAT auth + chat.sendMessage publish) is replaced by an out-of-process Python sidecar at sdk/python/librefang/sidecar/adapters/rocketchat.py (stdlib-only). Behaviour parity: - GET /api/v1/me startup credential probe - Per-room channels.history polling at 2 s default - Empty allowlist auto-discovers via channels.list.joined - Same X-Auth-Token / X-User-Id headers - Slash-command (/cmd args) routing - Multi-bot account_id metadata injection - 4096-char message chunking - Per-room transport-error isolation - suppress_error_responses (Rocket.Chat rooms are public) Three improvements over the Rust adapter: 1. Outbound threading is now actually wired. The Rust adapter captured the inbound tmid (rocketchat.rs:297) but send() called chat.sendMessage without forwarding it, so threaded replies always landed at the room root. The sidecar surfaces the inbound _id (or inbound tmid when the user was already in a thread) as thread_id, and on_send calls POST /api/v1/chat.postMessage with tmid populated. Mirrors reddit / bluesky / mastodon. 2. Dedupe set on msg._id. The Rust adapter advanced last_timestamps by RFC3339 string compare (rocketchat.rs:280-302) and re-fetched oldest=<watermark>; with count=50 and same-ts repeats this either re-emitted duplicates or silently dropped messages on a ts boundary. The sidecar keeps the watermark for the API query but additionally dedupes locally on _id (10k cap, 5k oldest eviction). 3. Self-skip by stable user id. The Rust adapter compared u.username == own_username (rocketchat.rs:285), which silently breaks when the bot's display name rotates. The sidecar compares u._id == ROCKETCHAT_USER_ID and falls back to username only when the inbound shape omits u._id. Cascade removal: - src/rocketchat.rs deleted; lib.rs mod decl, channels-allowlist.txt entry, cargo features in librefang-channels + librefang-api (incl. membership in all-channels / all-channels-no-email) removed - RocketChatConfig struct + Default impl removed; ChannelsConfig.rocketchat field + Default entry removed; validation hook removed - channel_bridge.rs: import, spawn block, find_channel_info! arm, check_channel! arm, default-empty test assertion removed - routes/channels.rs: ChannelMeta entry + 4 match arms (is_some / serialize / len / ser) removed; SIDECAR_CATALOG rocketchat entry added - routes/config.rs ch!(rocketchat) call removed - kernel channel_sender for_each_channel_field! entry + EXPECTED name-list entry removed - CLI TUI ChannelDef entry removed (migrated-to-sidecar comment added) - types/config/mod.rs default + serde roundtrip tests updated - kernel_config_schema golden fixture regenerated - docs (en + zh): configuration pages, integrations pages, architecture pages, communication API page, security pages — [channels.rocketchat] blocks replaced with [[sidecar_channels]] redirect; channels-bridge architecture rows updated New env-var knobs: ROCKETCHAT_SERVER_URL (replaces server_url), ROCKETCHAT_USER_ID (replaces user_id), optional ROCKETCHAT_CHANNELS (comma-separated room id list, empty = auto-discover joined channels), optional ROCKETCHAT_ACCOUNT_ID for multi-bot routing, optional ROCKETCHAT_POLL_INTERVAL_SECS (default 2, floor 1). Operator action required: an existing [channels.rocketchat] block is no longer recognised — re-declare as a [[sidecar_channels]] running python3 -m librefang.sidecar.adapters.rocketchat with env vars ROCKETCHAT_SERVER_URL, ROCKETCHAT_USER_ID (in [sidecar_channels.env]) and ROCKETCHAT_TOKEN (in ~/.librefang/secrets.env). Verification: - cargo check --workspace --lib: clean - cargo clippy --workspace --all-targets -- -D warnings: clean - cargo test -p librefang-types -p librefang-channels -p librefang-kernel --lib: 843 passed - cargo test -p librefang-api --lib: 686 passed - cargo test -p librefang-api --test channels_routes_test --test sidecar_describe_test: 40 passed (38 + 2) - cd sdk/python && pytest tests/test_rocketchat_adapter.py: 54 passed
5e6d2ce to
6382304
Compare
Replaces the 640-line in-process librefang-channels::nextcloud adapter with the Python sidecar librefang.sidecar.adapters.nextcloud. Same pattern as ntfy #5224, telegram #5241, gotify #5263, mastodon #5264, bluesky #5277, reddit #5281, twitch #5297, rocketchat #5298. Behaviour parity: - GET /ocs/v2.php/cloud/user startup credential probe - Per-room chat/<token>?lookIntoFuture=1 polling at 3 s default - Empty allowlist auto-discovers via apps/spreed/api/v4/room - Same Bearer + OCS-APIRequest: true headers - Slash-command (/cmd args) routing - Multi-bot account_id metadata injection - 32000-char message chunking (MAX_MESSAGE_LEN) - Per-room transport-error isolation - 304-as-no-op handling of Talk's long-poll-expired response - suppress_error_responses (Talk rooms are multi-participant) Three improvements over the Rust adapter (file/line evidence in CHANGELOG): 1. Outbound threading via replyTo is now actually wired. The Rust api_send_message (nextcloud.rs:130-160) posted just {"message": ...} — Talk's replyTo form parameter was never sent and chunked/threaded replies always landed at the room root. The sidecar surfaces the inbound id (or parentMessage.id when inside a thread) as thread_id, and on_send posts replyTo so replies thread correctly. Mirrors reddit/bluesky/mastodon/ rocketchat. 2. Self-skip on (actorType, actorId) == ("users", own_user_id) rather than actorId alone. The Rust adapter compared on actorId (nextcloud.rs:338) without inspecting actorType, so a Talk guest/federated_users actor whose id happened to match the bot's user id silently spoofed self-skip. Parallels the rocketchat #5298 fix. 3. Dedupe set on id, bounded SEEN_MESSAGES_MAX=10000 / SEEN_MESSAGES_EVICT=5000. The Rust adapter only relied on the server-side lastKnownMessageId cursor (nextcloud.rs:347-354); under retry/re-poll boundaries Talk can resend the same id. Same policy as reddit/rocketchat. Cascade removal: crates/librefang-channels/src/nextcloud.rs deleted; lib.rs mod decl, channels-allowlist.txt entry, channel-nextcloud feature in librefang-channels + librefang-api (incl. all-channels / all-channels-no-email membership); NextcloudConfig + ChannelsConfig.nextcloud field + Default; validation env-var hook; channel_bridge import + spawn block + find_channel_info! arm + check_channel! arm + default-empty test assertion; routes/channels.rs ChannelMeta + 4 match arms; routes/config.rs ch!(nextcloud); kernel channel_sender macro entry + EXPECTED name; CLI TUI ChannelDef; docs (en + zh): configuration / channels / integrations / architecture / communication / security pages — [channels.nextcloud] blocks replaced with [[sidecar_channels]] redirect; architecture-row moves Nextcloud Talk into the sidecar list. routes/channels.rs SIDECAR_CATALOG gains a nextcloud entry. Golden fixture regenerated. New env-var knobs: NEXTCLOUD_SERVER_URL (replaces server_url), optional NEXTCLOUD_ROOMS (comma-separated room tokens, empty = auto-discover), optional NEXTCLOUD_ACCOUNT_ID for multi-bot routing, optional NEXTCLOUD_POLL_INTERVAL_SECS (default 3, floor 1). Operator action required: an existing [channels.nextcloud] block is no longer recognised — re-declare as a [[sidecar_channels]] running python3 -m librefang.sidecar.adapters.nextcloud with env var NEXTCLOUD_SERVER_URL (in [sidecar_channels.env]) and NEXTCLOUD_TOKEN (in ~/.librefang/secrets.env). Verification: - cargo check --workspace --lib: clean - cargo clippy --workspace --all-targets -- -D warnings: clean - cargo test -p librefang-types -p librefang-channels -p librefang-kernel --lib: 842 passed - cargo test -p librefang-api --lib: 686 passed - cargo test -p librefang-api --test channels_routes_test --test sidecar_describe_test: 38 + 2 passed - cd sdk/python && pytest tests/test_nextcloud_adapter.py: 58 passed
…#5301) Replaces the 640-line in-process librefang-channels::nextcloud adapter with the Python sidecar librefang.sidecar.adapters.nextcloud. Same pattern as ntfy #5224, telegram #5241, gotify #5263, mastodon #5264, bluesky #5277, reddit #5281, twitch #5297, rocketchat #5298. Behaviour parity: - GET /ocs/v2.php/cloud/user startup credential probe - Per-room chat/<token>?lookIntoFuture=1 polling at 3 s default - Empty allowlist auto-discovers via apps/spreed/api/v4/room - Same Bearer + OCS-APIRequest: true headers - Slash-command (/cmd args) routing - Multi-bot account_id metadata injection - 32000-char message chunking (MAX_MESSAGE_LEN) - Per-room transport-error isolation - 304-as-no-op handling of Talk's long-poll-expired response - suppress_error_responses (Talk rooms are multi-participant) Three improvements over the Rust adapter (file/line evidence in CHANGELOG): 1. Outbound threading via replyTo is now actually wired. The Rust api_send_message (nextcloud.rs:130-160) posted just {"message": ...} — Talk's replyTo form parameter was never sent and chunked/threaded replies always landed at the room root. The sidecar surfaces the inbound id (or parentMessage.id when inside a thread) as thread_id, and on_send posts replyTo so replies thread correctly. Mirrors reddit/bluesky/mastodon/ rocketchat. 2. Self-skip on (actorType, actorId) == ("users", own_user_id) rather than actorId alone. The Rust adapter compared on actorId (nextcloud.rs:338) without inspecting actorType, so a Talk guest/federated_users actor whose id happened to match the bot's user id silently spoofed self-skip. Parallels the rocketchat #5298 fix. 3. Dedupe set on id, bounded SEEN_MESSAGES_MAX=10000 / SEEN_MESSAGES_EVICT=5000. The Rust adapter only relied on the server-side lastKnownMessageId cursor (nextcloud.rs:347-354); under retry/re-poll boundaries Talk can resend the same id. Same policy as reddit/rocketchat. Cascade removal: crates/librefang-channels/src/nextcloud.rs deleted; lib.rs mod decl, channels-allowlist.txt entry, channel-nextcloud feature in librefang-channels + librefang-api (incl. all-channels / all-channels-no-email membership); NextcloudConfig + ChannelsConfig.nextcloud field + Default; validation env-var hook; channel_bridge import + spawn block + find_channel_info! arm + check_channel! arm + default-empty test assertion; routes/channels.rs ChannelMeta + 4 match arms; routes/config.rs ch!(nextcloud); kernel channel_sender macro entry + EXPECTED name; CLI TUI ChannelDef; docs (en + zh): configuration / channels / integrations / architecture / communication / security pages — [channels.nextcloud] blocks replaced with [[sidecar_channels]] redirect; architecture-row moves Nextcloud Talk into the sidecar list. routes/channels.rs SIDECAR_CATALOG gains a nextcloud entry. Golden fixture regenerated. New env-var knobs: NEXTCLOUD_SERVER_URL (replaces server_url), optional NEXTCLOUD_ROOMS (comma-separated room tokens, empty = auto-discover), optional NEXTCLOUD_ACCOUNT_ID for multi-bot routing, optional NEXTCLOUD_POLL_INTERVAL_SECS (default 3, floor 1). Operator action required: an existing [channels.nextcloud] block is no longer recognised — re-declare as a [[sidecar_channels]] running python3 -m librefang.sidecar.adapters.nextcloud with env var NEXTCLOUD_SERVER_URL (in [sidecar_channels.env]) and NEXTCLOUD_TOKEN (in ~/.librefang/secrets.env). Verification: - cargo check --workspace --lib: clean - cargo clippy --workspace --all-targets -- -D warnings: clean - cargo test -p librefang-types -p librefang-channels -p librefang-kernel --lib: 842 passed - cargo test -p librefang-api --lib: 686 passed - cargo test -p librefang-api --test channels_routes_test --test sidecar_describe_test: 38 + 2 passed - cd sdk/python && pytest tests/test_nextcloud_adapter.py: 58 passed
… adapters (#5305) Rocket.Chat (channels.history), Mastodon (/api/v1/notifications), Reddit (?sort=new), and Bluesky (listNotifications) all poll upstreams that return newest-first, but each iterated the raw response and emitted in that order. So a single poll catching more than one new message delivered them to the agent in reverse chronological order. Reverse each batch to oldest-first before emitting, matching the chronological order nextcloud's Talk feed already yields. Dedupe sets and per-room / since high-water marks are order-independent, so this only changes delivery order. Faithfully reproduced from the original in-process Rust adapters; surfaced while reviewing the rocketchat sidecar migration (#5298). Tests: test_poll_once_emits_in_chronological_order added to each adapter's suite; existing fixtures that hand-ordered input ascending updated to the realistic newest-first shape. 478 Python tests pass.
The channel sidecar migration series (#5298–#5302) landed on main with two CI gates left red: - cargo fmt: reformat three files the migrations left unformatted — crates/librefang-api/tests/channels_routes_test.rs, crates/librefang-migrate/src/openclaw.rs, crates/librefang-types/src/config/mod.rs. - OpenAPI Drift: regenerate xtask/baselines/config.sha256. The migrations removed channel config structs/fields, changing the KernelConfig schema, but the baseline was never regenerated, so the drift gate failed on every push. openapi.json, the four SDKs, and the openapi/agent baselines were re-run and verified already in sync — only config.sha256 was stale. CHANGELOG attribution (the third red gate on earlier commits) was already fixed in-tree on HEAD.
) * feat(channels)!: migrate webex from in-process adapter to sidecar Replaces the 645-line in-process librefang-channels::webex adapter (Cisco Mercury WebSocket gateway for activity events + GET /messages/<id> REST follow-up + POST /messages publish) with the Python sidecar librefang.sidecar.adapters.webex. Same pattern as ntfy #5224, telegram #5241, gotify #5263, mastodon #5264, bluesky #5277, reddit #5281, twitch #5297, rocketchat #5298, discord #5299, nextcloud #5301, slack #5302. Behaviour parity: - GET /people/me startup credential probe (bot id + display name) - Hard-coded Mercury WSS URL with Bearer auth on upgrade request (no device-registration handshake — the Rust adapter never did one either) - data.activity envelope parsing; verb=="post" filter; actor-id self-skip - GET /messages/<id> REST follow-up for the full message body - Room filter (empty allowlist = all rooms the bot is in) - Slash-command (/cmd args) → Command routing - roomType == "group" → is_group mapping - Multi-bot account_id metadata injection - 7439-char message chunking (WEBEX_MSG_LIMIT parity with the Rust MAX_MESSAGE_LEN) - Exponential reconnect backoff (1s → 60s) - ChannelType::Custom("webex") preserved as channel_type = "webex" on the sidecar entry — existing routing / channel_role_mapping keys that reference webex continue to resolve. Four improvements over the Rust adapter (file/line evidence in CHANGELOG): 1. parentId outbound threading wired. The Rust api_send_message (webex.rs:171-201) posted just {"roomId", "text"} — Webex's parentId field was never sent and chunked replies always landed at the room root regardless of inbound context; the inbound side dropped the message id entirely (thread_id: None at webex.rs:438). The sidecar surfaces the inbound id (or inbound parentId when inside a thread) as thread_id, and on_send posts parentId populated so replies thread correctly. Mirrors reddit / rocketchat / nextcloud / mastodon / bluesky. 2. 429 Retry-After honoured on both GET /messages/<id> fetch and POST /messages send. The Rust adapter had no 429 handling at either call site, so a server-side rate-limit either lost the inbound fetch or returned Err and dropped the outbound. The sidecar parses Retry-After (default 30 s fallback, floor 1 s, cap MAX_BACKOFF_SECS), sleeps, and retries once before logging-and-continuing on the second 429. Same shape as #5303. 3. Mercury activity-id dedupe. Mercury can re-deliver an activity.object.id on reconnect (Rust adapter had no dedupe, see unconditional emit at webex.rs:459). Bounded local set on activity.object.id with SEEN_MESSAGES_MAX=10000 / SEEN_MESSAGES_EVICT=5000. 4. Explicit HTTP timeouts on every urlopen call (urllib has no default timeout; a hung Webex API would hang the producer thread forever). Cascade removal: - src/webex.rs deleted; lib.rs mod decl, channels-allowlist.txt entry, cargo features in librefang-channels + librefang-api (incl. membership in all-channels / all-channels-no-email) removed - WebexConfig struct + Default impl + ChannelsConfig.webex field + matching Default entry removed; validation hook removed - channel_bridge.rs: import, spawn block, find_channel_info! arm, check_channel! arm, default-empty test assertion removed - routes/channels.rs: ChannelMeta entry + 4 match arms removed; SIDECAR_CATALOG webex entry added - routes/config.rs ch!(webex) call removed - kernel channel_sender for_each_channel_field! macro entry + EXPECTED name-list entry removed - CLI: TUI ChannelDef row removed, migrated-to-sidecar comment grown - README / AGENTS for librefang-channels updated to list webex among the migrated-to-sidecar adapters - Docs (EN + ZH): configuration/page.mdx, configuration/channels/page.mdx, integrations/channels/enterprise/page.mdx, configuration/security/page.mdx, architecture/page.mdx — [channels.webex] blocks replaced with [[sidecar_channels]] redirect blocks (fence language `bash` per Shiki); architecture wave table moves webex out of Wave 3 in-process and into the sidecar list New env-var knobs (read from [sidecar_channels.env]): - WEBEX_ALLOWED_ROOMS (comma-separated room IDs, empty = allow all) - WEBEX_ACCOUNT_ID (optional, multi-bot routing key) Secret in ~/.librefang/secrets.env: WEBEX_BOT_TOKEN. Operator action required: an existing [channels.webex] block is no longer recognised — re-declare as a [[sidecar_channels]] running python3 -m librefang.sidecar.adapters.webex with WEBEX_BOT_TOKEN (in ~/.librefang/secrets.env) and any of the optional knobs above (in [sidecar_channels.env]). Verification: - cargo check --workspace --lib: clean - cargo clippy --workspace --all-targets -- -D warnings: clean - cargo test -p librefang-types -p librefang-channels -p librefang-kernel --lib: 420 + 838 + 1087 passed - cargo test -p librefang-api --lib: 685 passed - cargo test -p librefang-api --test channels_routes_test --test sidecar_describe_test: 38 + 2 passed - cd sdk/python && pytest tests/test_webex_adapter.py: 78 passed * fix(channels/webex): apply review nits — display name, suppress flag, dead code, redundant filter Self-review pass on top of the migration commit; addresses 4 nits from the pre-merge review without changing the migration's core behaviour: 1. **personDisplayName drives user_name** (`webex.py:281-291, 326-330`). The migration commit preserved the Rust adapter's `webex.rs:431` behaviour of using `personEmail` unconditionally, which leaks emails into bot logs and dashboard UI. Now `parse_webex_message` prefers `personDisplayName` from the `/messages/<id>` body and falls back to `personEmail` when the field is absent (older Webex orgs / some service-account principals) or empty; `personEmail` stays in metadata for routing / audit. 3 new tests (`test_parse_prefers_person_display_name_for_user_name`, `…_falls_back_to_email_when_display_name_missing`, `…_falls_back_to_email_when_display_name_empty`). 2. **Explicit `suppress_error_responses = False`** on `WebexAdapter` with a precedent comment naming the discord / slack / twitch chat-room default vs. the mastodon / bluesky / reddit / nextcloud public-broadcast default. The migration left this implicit; an explicit class-level attr matches how `twitch.py:312` documents the choice so the next reader doesn't have to reconstruct the reasoning. 3. **`register_webhook` dead-code drop documented in CHANGELOG**. The Rust adapter carried a never-wired `register_webhook` helper at `webex.rs:137-168` (marked `#[allow(dead_code)]`) for an HTTP-webhook delivery alternative the channel-bridge never enabled; the sidecar drops it. The CHANGELOG now names this explicitly and points operators who want webhook delivery at the canonical generic `[[sidecar_channels]]` running `librefang.sidecar.adapters.webhook`. 4. **Skip redundant room filter inside `parse_webex_message`** when called from `_handle_envelope` (`webex.py:867-878`). `_handle_envelope` already filtered against `self.allowed_rooms` at line 855 before paying for the REST fetch; passing `allowed_rooms=[]` into the parser drops one redundant list-membership check per inbound message. The parser keeps the parameter for direct callers (tests; future code paths). Verification (run inside the worktree): * `cd sdk/python && python3 -m pytest tests/test_webex_adapter.py -q` → **81 passed** (was 78; +3 personDisplayName tests). No existing test modified. * `cargo check` / `clippy` not re-run — this commit is Python + CHANGELOG only, no Rust touched. Diff: +73 / -5 across `CHANGELOG.md`, `sdk/python/librefang/sidecar/adapters/webex.py`, `sdk/python/tests/test_webex_adapter.py`.
* feat(channels)!: migrate line from in-process adapter to sidecar Replaces the 881-line in-process librefang-channels::line adapter (axum webhook route at /channels/line/webhook on the shared API server, X-Line-Signature HMAC-SHA256 verification, POST /v2/bot/message/push for outbound) with the Python sidecar librefang.sidecar.adapters.line. Same pattern as ntfy #5224, telegram #5241, gotify #5263, mastodon #5264, bluesky #5277, reddit #5281, twitch #5297, rocketchat #5298, discord #5299, nextcloud #5301, slack #5302, webex #5309. Behaviour parity: - GET /v2/bot/info startup credential probe (bot display name) - HTTP webhook server (BaseHTTPRequestHandler over ThreadingTCPServer) on LINE_WEBHOOK_PORT (default 9090) + LINE_WEBHOOK_PATH (default /webhook) - X-Line-Signature HMAC-SHA256 verification over the raw wire bytes (constant-time compare, line.rs:229-250 parity, incl. the wire-bytes-vs-JSON-roundtrip regression) - Only message events with type==text (other event types and non-text message types dropped, line.rs:256-273) - Source-type -> reply_to mapping (user -> userId, group -> groupId, room -> roomId; group/room -> is_group=true, line.rs:280-290) - Slash-command routing (/cmd args -> Command) - Metadata preservation: user_id / reply_to / reply_token / source_type - Multi-bot account_id injection via LINE_ACCOUNT_ID - 5000-char chunking (LINE_MSG_LIMIT parity with MAX_MESSAGE_LEN at line.rs:39) - Image-branch wire shape (originalContentUrl + previewImageUrl both set to the caller URL, caption sent as follow-up text push, line.rs:464-490) - ChannelType::Custom("line") preserved as channel_type = "line" on the sidecar entry so existing routing / channel_role_mapping keys continue to resolve Three improvements over the Rust adapter: 1. 429 Retry-After honoured on outbound. The Rust api_push_message (line.rs:148-184) had no 429 handling; throttled push returned Err and the chunked reply dropped on the floor. Sidecar parses Retry-After (default 30 s fallback, floor 1 s, cap MAX_BACKOFF_SECS), sleeps, retries once, then logs-and-continues on the second 429. Same shape as #5303. 2. Inbound dedupe on message.id. LINE redelivers webhook events when the operator's endpoint fails; the Rust handler at line.rs:413-427 emitted every event unconditionally, causing duplicate agent invocations on transient failures. Bounded local set on message.id with SEEN_MESSAGES_MAX=10000 / SEEN_MESSAGES_EVICT=5000 (matches reddit / rocketchat / nextcloud / webex policy). 3. Explicit HTTP timeouts on every urlopen call (urllib has no default timeout; a hung LINE API would otherwise hang the worker thread forever). Cascade removal: - src/line.rs deleted; lib.rs mod decl, channels-allowlist.txt entry, cargo features in librefang-channels + librefang-api (incl. membership in all-channels / all-channels-no-email) removed - LineConfig struct + Default impl + ChannelsConfig.line field + matching Default entry removed; validation hook removed - channel_bridge.rs: import, spawn block, find_channel_info! arm, check_channel! arm, default-empty test assertion removed - routes/channels.rs: ChannelMeta entry + 5 match arms removed (is_some / serialize / len / ser / is_channel_configured); webhook_route_suffix allowlist entry removed (sidecar runs its own webhook listener, not on the shared API port); SIDECAR_CATALOG line entry added - routes/config.rs ch!(line) call removed - kernel channel_sender for_each_channel_field! entry + EXPECTED name-list entry removed - CLI: TUI ChannelDef row removed, migrated-to-sidecar comment grown - README / AGENTS for librefang-channels updated; the mandatory- HMAC paragraph now lists Teams + DingTalk only (LINE moves to the sidecar but keeps the same X-Line-Signature contract inside the new process) - Docs (EN + ZH): configuration/page.mdx, configuration/channels/page.mdx, configuration/security/page.mdx, integrations/channels/integrations/page.mdx - [channels.line] blocks replaced with [[sidecar_channels]] redirect blocks; security env-var table flags both LINE_CHANNEL_SECRET and LINE_CHANNEL_ACCESS_TOKEN as sidecar-only; validation table drops the LINE row New env-var knobs (read from [sidecar_channels.env]): - LINE_WEBHOOK_PORT (default 9090) - LINE_WEBHOOK_PATH (default /webhook) - LINE_BIND_HOST (default 0.0.0.0) - LINE_ACCOUNT_ID (optional, multi-bot routing key) Secrets in ~/.librefang/secrets.env: LINE_CHANNEL_SECRET + LINE_CHANNEL_ACCESS_TOKEN. Operator action required (substantive): the sidecar runs its OWN HTTP webhook server - it is no longer mounted on the LibreFang API port - so the webhook URL registered at the LINE Developers Console must be updated to point at the sidecar host (typical pattern: an HTTPS reverse proxy in front of the sidecar's LINE_WEBHOOK_PORT). An existing [channels.line] block is no longer recognised - re-declare as a [[sidecar_channels]] running python3 -m librefang.sidecar.adapters.line with LINE_CHANNEL_SECRET + LINE_CHANNEL_ACCESS_TOKEN (in ~/.librefang/secrets.env) and any of the optional knobs above (in [sidecar_channels.env]). Verification: - cargo check --workspace --lib: clean - cargo clippy --workspace --all-targets -- -D warnings: clean - cargo fmt --check: clean - cargo test --lib -p librefang-types -p librefang-channels -p librefang-kernel: 420 + 838 + 1087 passed - cargo test --lib -p librefang-api: 685 passed - cargo test -p librefang-api --test channels_routes_test --test sidecar_describe_test: 38 + 2 passed - cargo xtask channel-policy: passed - cd sdk/python && pytest tests/test_line_adapter.py: 68 passed * docs(channels): correct line sidecar HTTP server class references The module docstring claimed `ThreadingHTTPServer` but the implementation uses `socketserver.ThreadingTCPServer` (intentional: skips `HTTPServer.server_bind()`'s `socket.getfqdn()` DNS lookup on startup). Align the three doc references with the actual class and spell out why we don't use `ThreadingHTTPServer` so the next reader doesn't "fix" it back.
Replaces the 713-line in-process `librefang-channels::zulip` (Basic auth + `POST /api/v1/register` + long-poll `GET /api/v1/events` + `POST /api/v1/messages`) with an out-of-process Python sidecar at `sdk/python/librefang/sidecar/adapters/zulip.py` (stdlib-only: urllib + base64). Behaviour parity: - HTTP Basic auth `<bot_email>:<api_key>` on every REST call - `GET /api/v1/users/me` startup probe → bot's stable `user_id` + `full_name` - event-queue register with optional `narrow=[["stream", …], …]` when `ZULIP_STREAMS` is set - long-poll `/events?dont_block=false` with 70 s HTTP timeout (Rust used POLL_TIMEOUT + 10 s at zulip.rs:244) - `BAD_EVENT_QUEUE_ID` re-register (zulip.rs:262-308 parity) - client-side stream filter as defence-in-depth against best-effort narrow - slash-command routing `/cmd args` → Command - DM vs group via `message.type == "stream"` - 10 000-char chunking (`ZULIP_MSG_LIMIT` parity) - outbound DM heuristic: `@` in platform_id ⇒ type=direct - `account_id` metadata injection - exponential backoff 1 s → 60 s Four improvements (each with file/line evidence on the migrating tree): 1. **outbound topic round-trip via `thread_id`** — the Rust `send` (zulip.rs:463) hard-coded `topic = "LibreFang"` for every stream reply, losing the inbound topic context so the bot's response always landed in a "LibreFang" topic regardless of what triggered it. The sidecar surfaces inbound `message.subject` as `thread_id` and `on_send` routes every stream send through that topic, so the reply lands in the originating topic. 2. **429 `Retry-After` honoured on every REST path** — the Rust adapter had no 429 handling, only generic 1 s → 60 s exponential backoff (zulip.rs:228-313). `_validate` / `_register_queue` / `_poll_once` / `_post_message` all parse `Retry-After` (1 s floor, `MAX_BACKOFF_SECS` cap, 30 s fallback) and retry once. 3. **bounded `message.id` dedupe** — `last_event_id` narrows the event range server-side, but `BAD_EVENT_QUEUE_ID` re-register starts fresh and can re-emit. Rust emit at zulip.rs:434 was unconditional. Sidecar dedupes on `message.id` with bounded `SEEN_MESSAGES_MAX = 10 000` / `EVICT = 5 000` cap (same policy as reddit / rocketchat / nextcloud / webex). 4. **self-skip by stable integer `sender_id`** — Rust compared `sender_email == bot_email` (zulip.rs:357); email rotation breaks self-skip. Sidecar prefers `sender_id == own_user_id` (the integer `/users/me` returns) and falls back to email only when sender_id is absent (parallels rocketchat #5298 / nextcloud #5301). Cascade: - `src/zulip.rs` deleted; `lib.rs` mod, `channels-allowlist.txt` entry, both Cargo.toml features (incl. `all-channels` / `all-channels-no-email`) removed - `ZulipConfig` struct + Default + `ChannelsConfig.zulip` field + default + validation hook + `test_zulip_config_defaults` / `…_serde` unit tests all gone - `channel_bridge.rs` import / spawn block / `find_channel_info!` / `check_channel!` / default-empty assertion removed - `routes/channels.rs` `ChannelMeta` + 4 match arms removed; `SIDECAR_CATALOG` `zulip` entry added at the end - `routes/config.rs` `ch!(zulip)` removed - `channel_sender.rs` `for_each_channel_field!` + `EXPECTED` entries removed - CLI TUI `ChannelDef` removed - README + AGENTS "migrated to sidecars" sentence updated - docs EN + ZH: `[channels.zulip]` blocks replaced with `[[sidecar_channels]]` redirect; `ZULIP_API_KEY` env table marked (sidecar); validation table entry removed New env-var knobs (read from `[sidecar_channels.env]`): `ZULIP_SERVER_URL`, `ZULIP_BOT_EMAIL`, optional `ZULIP_STREAMS`, optional `ZULIP_ACCOUNT_ID`. Bot API key in `~/.librefang/secrets.env` as `ZULIP_API_KEY`. `ChannelType::Custom("zulip")` (the channel-type token the Rust adapter advertised at zulip.rs:197) is preserved across this migration via `channel_type = "zulip"` on the sidecar entry, so existing routing and `channel_role_mapping` keys that reference `zulip` continue to resolve. Operator action required: an existing `[channels.zulip]` block is no longer recognised — re-declare as `[[sidecar_channels]]` running `python3 -m librefang.sidecar.adapters.zulip`. Verification (run inside the worktree): - `cargo check --workspace --lib` clean - `cargo clippy --workspace --all-targets -- -D warnings` clean - `cargo test -p librefang-types -p librefang-channels -p librefang-kernel --lib`: 837 passed - `cargo test -p librefang-api --lib`: 685 passed - `cargo test -p librefang-api --test channels_routes_test --test sidecar_describe_test`: 38 + 2 passed - `cd sdk/python && python3 -m pytest tests/test_zulip_adapter.py -q`: **74 passed** - `grep -rn -iE "channels\\.zulip|channel-zulip|ZulipAdapter|ZulipConfig" docs/ crates/`: only the intentional sidecar-redirect notes remain in docs (and the sidecar-redirect comment in `types.rs`) `kernel_config_schema.golden.json` is `#[ignore]`'d on main per its own header (#4084 follow-up); leave the fixture as-is, matches twitch / webex precedent.
) Replaces the 713-line in-process `librefang-channels::zulip` (Basic auth + `POST /api/v1/register` + long-poll `GET /api/v1/events` + `POST /api/v1/messages`) with an out-of-process Python sidecar at `sdk/python/librefang/sidecar/adapters/zulip.py` (stdlib-only: urllib + base64). Behaviour parity: - HTTP Basic auth `<bot_email>:<api_key>` on every REST call - `GET /api/v1/users/me` startup probe → bot's stable `user_id` + `full_name` - event-queue register with optional `narrow=[["stream", …], …]` when `ZULIP_STREAMS` is set - long-poll `/events?dont_block=false` with 70 s HTTP timeout (Rust used POLL_TIMEOUT + 10 s at zulip.rs:244) - `BAD_EVENT_QUEUE_ID` re-register (zulip.rs:262-308 parity) - client-side stream filter as defence-in-depth against best-effort narrow - slash-command routing `/cmd args` → Command - DM vs group via `message.type == "stream"` - 10 000-char chunking (`ZULIP_MSG_LIMIT` parity) - outbound DM heuristic: `@` in platform_id ⇒ type=direct - `account_id` metadata injection - exponential backoff 1 s → 60 s Four improvements (each with file/line evidence on the migrating tree): 1. **outbound topic round-trip via `thread_id`** — the Rust `send` (zulip.rs:463) hard-coded `topic = "LibreFang"` for every stream reply, losing the inbound topic context so the bot's response always landed in a "LibreFang" topic regardless of what triggered it. The sidecar surfaces inbound `message.subject` as `thread_id` and `on_send` routes every stream send through that topic, so the reply lands in the originating topic. 2. **429 `Retry-After` honoured on every REST path** — the Rust adapter had no 429 handling, only generic 1 s → 60 s exponential backoff (zulip.rs:228-313). `_validate` / `_register_queue` / `_poll_once` / `_post_message` all parse `Retry-After` (1 s floor, `MAX_BACKOFF_SECS` cap, 30 s fallback) and retry once. 3. **bounded `message.id` dedupe** — `last_event_id` narrows the event range server-side, but `BAD_EVENT_QUEUE_ID` re-register starts fresh and can re-emit. Rust emit at zulip.rs:434 was unconditional. Sidecar dedupes on `message.id` with bounded `SEEN_MESSAGES_MAX = 10 000` / `EVICT = 5 000` cap (same policy as reddit / rocketchat / nextcloud / webex). 4. **self-skip by stable integer `sender_id`** — Rust compared `sender_email == bot_email` (zulip.rs:357); email rotation breaks self-skip. Sidecar prefers `sender_id == own_user_id` (the integer `/users/me` returns) and falls back to email only when sender_id is absent (parallels rocketchat #5298 / nextcloud #5301). Cascade: - `src/zulip.rs` deleted; `lib.rs` mod, `channels-allowlist.txt` entry, both Cargo.toml features (incl. `all-channels` / `all-channels-no-email`) removed - `ZulipConfig` struct + Default + `ChannelsConfig.zulip` field + default + validation hook + `test_zulip_config_defaults` / `…_serde` unit tests all gone - `channel_bridge.rs` import / spawn block / `find_channel_info!` / `check_channel!` / default-empty assertion removed - `routes/channels.rs` `ChannelMeta` + 4 match arms removed; `SIDECAR_CATALOG` `zulip` entry added at the end - `routes/config.rs` `ch!(zulip)` removed - `channel_sender.rs` `for_each_channel_field!` + `EXPECTED` entries removed - CLI TUI `ChannelDef` removed - README + AGENTS "migrated to sidecars" sentence updated - docs EN + ZH: `[channels.zulip]` blocks replaced with `[[sidecar_channels]]` redirect; `ZULIP_API_KEY` env table marked (sidecar); validation table entry removed New env-var knobs (read from `[sidecar_channels.env]`): `ZULIP_SERVER_URL`, `ZULIP_BOT_EMAIL`, optional `ZULIP_STREAMS`, optional `ZULIP_ACCOUNT_ID`. Bot API key in `~/.librefang/secrets.env` as `ZULIP_API_KEY`. `ChannelType::Custom("zulip")` (the channel-type token the Rust adapter advertised at zulip.rs:197) is preserved across this migration via `channel_type = "zulip"` on the sidecar entry, so existing routing and `channel_role_mapping` keys that reference `zulip` continue to resolve. Operator action required: an existing `[channels.zulip]` block is no longer recognised — re-declare as `[[sidecar_channels]]` running `python3 -m librefang.sidecar.adapters.zulip`. Verification (run inside the worktree): - `cargo check --workspace --lib` clean - `cargo clippy --workspace --all-targets -- -D warnings` clean - `cargo test -p librefang-types -p librefang-channels -p librefang-kernel --lib`: 837 passed - `cargo test -p librefang-api --lib`: 685 passed - `cargo test -p librefang-api --test channels_routes_test --test sidecar_describe_test`: 38 + 2 passed - `cd sdk/python && python3 -m pytest tests/test_zulip_adapter.py -q`: **74 passed** - `grep -rn -iE "channels\\.zulip|channel-zulip|ZulipAdapter|ZulipConfig" docs/ crates/`: only the intentional sidecar-redirect notes remain in docs (and the sidecar-redirect comment in `types.rs`) `kernel_config_schema.golden.json` is `#[ignore]`'d on main per its own header (#4084 follow-up); leave the fixture as-is, matches twitch / webex precedent.
Replaces the 758-line in-process `librefang-channels::qq` adapter (token mint + WebSocket gateway + HELLO/IDENTIFY/READY handshake + heartbeat + DISPATCH for 4 event types + REST send with markdown stripping) with an out-of-process Python sidecar at `sdk/python/librefang/sidecar/adapters/qq.py`. Same pattern as ntfy #5224, telegram #5241, gotify #5263, mastodon #5264, bluesky #5277, reddit #5281, twitch #5297, rocketchat #5298, discord #5299, nextcloud #5301, slack #5302, webex #5309, line #5312, zulip #5310, mattermost #5315, signal #5317. Behaviour parity (every claim cited against the pre-migration tree): - Token fetch: POST bots.qq.com/app/getAppAccessToken (qq.rs:542-557). - Gateway: GET /gateway with Bearer auth (qq.rs:559-573). - WS handshake: HELLO(op=10) → IDENTIFY(op=2) with token = "QQBot <access_token>", intents bitmask, shard=[0,1] → READY dispatch (qq.rs:413-433). - Heartbeat: op=1 frame at the server-supplied interval, carrying last seen `s` from dispatch frames (qq.rs:359-368). - 4 dispatch event types: MESSAGE_CREATE / AT_MESSAGE_CREATE → /channels/{channel_id}/messages, DIRECT_MESSAGE_CREATE → /dms/{guild_id}/messages, GROUP_AT_MESSAGE_CREATE → /v2/groups/{group_openid}/messages, C2C_MESSAGE_CREATE → /v2/users/{user_openid}/messages (qq.rs:194-224). - Bot-mention `/` strip + slash-command routing (qq.rs:227). - User allowlist + multi-bot account_id metadata (#5003, qq.rs:400-405). - Outbound markdown stripping pipeline preserves order: think tags → code blocks → inline code → bold → italic → headings → table separators → links → blockquotes → HRs → triple-newline collapse (qq.rs:137-180). - 2000-char chunking (qq.rs:26), 1–60 s exponential reconnect backoff (qq.rs:282). Improvements over the Rust adapter ================================== 1. **Reply context actually round-trips.** The Rust `parse_dispatch_event` (qq.rs:182-246) computed `reply_endpoint` and `msg_id` but the dispatch loop bound them to `_endpoint` / `_msg_id` (qq.rs:399) and dropped them on the floor; `send` (qq.rs:497-498) then expected `user.platform_id` to be encoded as `"<endpoint>|<msg_id>"` and silently no-op'd when the delimiter wasn't there. The Rust adapter therefore failed every real outbound — only the synthetic wiremock tests at qq.rs:686-712 exercised the working shape. The sidecar surfaces the reply endpoint as `channel_id` and the QQ `msg_id` as `thread_id` on the inbound event so the bridge round-trips them through to `on_send`, which posts to `{api_base}{channel_id}` with the correct passive-reply `msg_id`. 2. **Inbound dedupe on `msg.id`.** Rust emitted every parsed event unconditionally (qq.rs:399-410); WS reconnect could replay. Bounded local set, SEEN_MAX=10000 / EVICT=5000. 3. **429 Retry-After honoured on every REST path.** Token, gateway, and outbound send all sleep + retry once; second 429 logs-and-continues (matches webex / line / mattermost / signal #5303). 4. **Explicit 15s urlopen timeouts on every REST call.** Rust pre-configured reqwest's 30s default at qq.rs:71; sidecar passes `timeout=SEND_TIMEOUT_SECS` (15 s) so a misbehaving endpoint trips an explicit error. Cascade removal ================ - `src/qq.rs` deleted; `lib.rs` mod, `channels-allowlist.txt` entry, both `Cargo.toml` features removed. - `QqConfig` struct + Default + `ChannelsConfig.qq` field gone. - `channel_bridge.rs` import / spawn block / `check_channel!` / find_channel_info!(qq) (none existed) removed. - `routes/channels.rs` `ChannelMeta` entry + 4 match arms (`is_some` / serialize / `len` / `ser`) removed; SIDECAR_CATALOG `qq` entry added. - `routes/config.rs` `ch!(qq)` removed. - `channel_sender.rs` `for_each_channel_field!` macro entry + `EXPECTED` name-list entry removed. - CLI / TUI had no QQ wizard arm to remove. - librefang-migrate had no QQ block to handle (QQ wasn't in the OpenClaw schema). - Docs EN + ZH (6 files): `[channels.qq]` blocks replaced with `[[sidecar_channels]]` redirects; integrations tutorial rewritten for the sidecar. New env-var knobs (`[sidecar_channels.env]`) ============================================= - `QQ_APP_ID` (required) - `QQ_ALLOWED_USERS` (optional, CSV) - `QQ_ACCOUNT_ID` (optional) - `QQ_INTENTS` (optional, decimal/hex bitmask override) Secret (`~/.librefang/secrets.env`) ==================================== - `QQ_APP_SECRET` (required) Operator action required ======================== An existing `[channels.qq]` block is no longer recognised — re-declare as `[[sidecar_channels]]` running `python3 -m librefang.sidecar.adapters.qq`. See `sdk/python/librefang/sidecar/adapters/qq.py` header for the exact config. `ChannelType::Custom("qq")` is preserved via `channel_type = "qq"` on the sidecar entry so existing routing and `channel_role_mapping` keys continue to resolve. Verification ============ - `cargo check -p librefang-types -p librefang-channels -p librefang-api -p librefang-kernel -p librefang-cli --lib` — clean - `cargo xtask channel-policy` — passed (`qq` removed from allowlist) - `pytest sdk/python/tests/test_qq_adapter.py -q` — **77 passed** (covers env parsing, markdown stripping, parse_qq_event for all 4 event types + edge cases, _mark_seen LRU eviction, _fetch_token + _fetch_gateway happy path / 429 / non-200 / missing field, _post_message chunking + 429 retry-once + persistent-429 fail-open + 5xx fail-open, on_send wiring with markdown stripping at the outbound boundary, WS gateway flow via mock _WebSocketClient — HELLO → IDENTIFY token+intents+ shard, DISPATCH emission, msg.id dedupe, RECONNECT/INVALID_SESSION, heartbeat after interval, --describe schema, capabilities contract). Co-authored-by: Evan <[email protected]>
Summary
Migrates Rocket.Chat from an in-process Rust
ChannelAdapterto an out-of-process Python sidecar — the seventh in the sidecar series after ntfy (#5224), telegram (#5241), gotify (#5263), mastodon (#5264), bluesky (#5277), and reddit (#5281).Behaviour parity
The new
librefang.sidecar.adapters.rocketchat(stdlib-only, 480 lines) preserves the Rust adapter's externally-observable behaviour:GET /api/v1/mestartup credential probechannels.historypolling at the same 2 s default intervalchannels.list.joinedX-Auth-Token/X-User-IdPAT auth headers/cmd args) routing →Command, text otherwiseaccount_idmetadata injectionsuppress_error_responses— Rocket.Chat rooms are public, same rationale as mastodon / bluesky / redditImprovements over the Rust adapter
Outbound threading is now actually wired.
The Rust adapter captured the inbound
tmidfield (crates/librefang-channels/src/rocketchat.rs:297on main) butsend()always calledPOST /api/v1/chat.sendMessagewithout forwarding it, so threaded replies always landed at the room root regardless of the inbound context. The sidecar surfaces the inbound_id(or inboundtmidwhen the user was already in a thread, so the bot replies in the same thread rather than starting a child) asthread_id.on_sendcallsPOST /api/v1/chat.postMessagewithtmidpopulated. Mirrors reddit / bluesky / mastodon's threading round-trip. Coverage:test_parse_thread_reply_uses_tmid_as_thread_id,test_post_message_with_thread_includes_tmid,test_post_message_chunks_preserve_tmid,test_on_send_threads_via_thread_id.Dedupe set on
msg._id.The Rust adapter advanced
last_timestampsby RFC3339 string compare (crates/librefang-channels/src/rocketchat.rs:280-302on main) and re-fetchedoldest=<watermark>. Withcount=50and same-tsrepeats this either re-emitted duplicates on the next poll (server-side timestamps are inclusive on the boundary) or silently dropped messages sharing ats. The sidecar keeps the RFC3339 watermark for the API query but additionally dedupes locally on_idwith boundedSEEN_MESSAGES_MAX=10000/SEEN_MESSAGES_EVICT=5000(same policy as reddit). Coverage:test_poll_once_dedupes_same_ts_repeats,test_seen_messages_capacity_eviction.Self-skip by stable user id.
The Rust adapter compared
u.username == own_username(crates/librefang-channels/src/rocketchat.rs:285on main), which silently breaks when the bot's display name rotates without invalidating the token. The sidecar comparesu._id == ROCKETCHAT_USER_ID(the stable internal id the operator already configured) and falls back to username only when the inbound shape omitsu._id. Coverage:test_parse_skips_self_by_user_id,test_parse_falls_back_to_username_when_uid_missing,test_parse_username_fallback_disabled_when_own_username_empty.Cascade removal
crates/librefang-channels/src/rocketchat.rsdeleted;lib.rsmod decl,channels-allowlist.txtentry,channel-rocketchatcargo feature in bothlibrefang-channelsandlibrefang-api(incl. its membership inall-channels/all-channels-no-email) removedRocketChatConfigstruct +Defaultimpl removed;ChannelsConfig.rocketchatfield +Defaultentry removed;validation.rsenv-var hook removedchannel_bridge.rs: import, spawn block,find_channel_info!arm,check_channel!arm, default-empty test assertion removedroutes/channels.rs:ChannelMetaentry + 4 match arms (is_some/ serialize /len/ser) removed;SIDECAR_CATALOGrocketchat entry addedroutes/config.rsch!(rocketchat)call removedchannel_senderfor_each_channel_field!macro entry +EXPECTEDname-list entry removedChannelDefentry removed; migrated-to-sidecar comment addedtypes/config/mod.rsdefault + serde-roundtrip tests updated;RocketChatConfigimport would not resolve otherwisekernel_config_schema.golden.jsonregenerated (cascade of removingRocketChatConfig)[channels.rocketchat]blocks replaced with[[sidecar_channels]]redirect blocks; channels-bridge architecture rows updated to move rocketchat into the sidecar listNew env-var knobs
ROCKETCHAT_SERVER_URL(replacesserver_url, required)ROCKETCHAT_USER_ID(replacesuser_id, required)ROCKETCHAT_TOKEN(the PAT — belongs in~/.librefang/secrets.env, required)ROCKETCHAT_CHANNELS(comma-separated room id list, optional; empty = auto-discover joined channels)ROCKETCHAT_ACCOUNT_ID(optional, multi-bot routing key)ROCKETCHAT_POLL_INTERVAL_SECS(optional, default 2, floor 1)Operator action required
An existing
[channels.rocketchat]block is no longer recognised. Re-declare as a[[sidecar_channels]]runningpython3 -m librefang.sidecar.adapters.rocketchatwithROCKETCHAT_SERVER_URLandROCKETCHAT_USER_IDin[sidecar_channels.env], andROCKETCHAT_TOKENin~/.librefang/secrets.env. The dashboard's Channels page writes the secret to the right place automatically.Verification
All commands run from inside
/Volumes/Lexar/Workspace/oss/librefang/wt-rocketchat-sidecar:cargo check --workspace --lib— clean (1m 14s)cargo clippy --workspace --all-targets -- -D warnings— clean (1m 37s)cargo test -p librefang-types -p librefang-channels -p librefang-kernel --lib— 843 passedcargo test -p librefang-api --lib— 686 passedcargo test -p librefang-api --test channels_routes_test --test sidecar_describe_test— 40 passed (38 + 2)cd sdk/python && PYTHONPATH=. pytest tests/test_rocketchat_adapter.py -v— 54 passedTest plan
ROCKETCHAT_TOKENlands in~/.librefang/secrets.envand the rest inconfig.toml's[[sidecar_channels]]tmid)tmidmatches the inbound thread parent), not as a childu._idcheck survives the rename)