From 606f2860a026efdff3f5cd291458444b99a8a249 Mon Sep 17 00:00:00 2001 From: Chummy Date: Thu, 19 Feb 2026 10:17:10 +0800 Subject: [PATCH] fix(matrix): send markdown replies and improve e2ee diagnostics Enable matrix-sdk markdown support and send Matrix messages with text_markdown so clients can render formatted_body. Add listener startup diagnostics for device verification and backup state to reduce confusion around matrix_sdk_crypto backup warnings. Expand Matrix docs with backup-warning interpretation, unverified-device guidance, markdown formatting expectations, and updated log keyword appendix. --- Cargo.lock | 19 ++++++++++++++ Cargo.toml | 2 +- docs/channels-reference.md | 7 +++-- docs/matrix-e2ee-guide.md | 10 +++++++- src/channels/matrix.rs | 52 +++++++++++++++++++++++++++++++++++++- 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1965ac4..d058410 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4225,6 +4225,24 @@ dependencies = [ "cc", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.11.0", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quinn" version = "0.11.9" @@ -4657,6 +4675,7 @@ dependencies = [ "js_int", "js_option", "percent-encoding", + "pulldown-cmark", "regex", "ruma-common", "ruma-identifiers-validation", diff --git a/Cargo.toml b/Cargo.toml index 7b133ae..498f2b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ tokio-util = { version = "0.7", default-features = false } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "blocking", "multipart", "stream", "socks"] } # Matrix client + E2EE decryption -matrix-sdk = { version = "0.16", default-features = false, features = ["e2e-encryption", "rustls-tls"] } +matrix-sdk = { version = "0.16", default-features = false, features = ["e2e-encryption", "rustls-tls", "markdown"] } # Serialization serde = { version = "1.0", default-features = false, features = ["derive"] } diff --git a/docs/channels-reference.md b/docs/channels-reference.md index e0cdb54..2238e23 100644 --- a/docs/channels-reference.md +++ b/docs/channels-reference.md @@ -150,6 +150,10 @@ allowed_users = ["*"] See [Matrix E2EE Guide](./matrix-e2ee-guide.md) for encrypted-room troubleshooting. +Notes: +- Outbound Matrix replies are emitted as markdown-capable `m.room.message` text content so common clients can render lists, emphasis, and code blocks. +- If you still see `matrix_sdk_crypto::backups` warnings, follow the backup/recovery section in the Matrix E2EE guide. + ### 4.6 Signal ```toml @@ -316,7 +320,7 @@ rg -n "Matrix|Telegram|Discord|Slack|Mattermost|Signal|WhatsApp|Email|IRC|Lark|D | Discord | `Discord: connected and identified` | `Discord: ignoring message from unauthorized user:` | `Discord: received Reconnect (op 7)` / `Discord: received Invalid Session (op 9)` | | Slack | `Slack channel listening on #` | `Slack: ignoring message from unauthorized user:` | `Slack poll error:` / `Slack parse error:` | | Mattermost | `Mattermost channel listening on` | `Mattermost: ignoring message from unauthorized user:` | `Mattermost poll error:` / `Mattermost parse error:` | -| Matrix | `Matrix channel listening on room` / `Matrix room ... is encrypted; E2EE decryption is enabled via matrix-sdk.` | `Matrix whoami failed; falling back to configured session hints for E2EE session restore:` / `Matrix whoami failed while resolving listener user_id; using configured user_id hint:` | `Matrix sync error: ... retrying...` | +| Matrix | `Matrix channel listening on room` / `Matrix room ... is encrypted; E2EE decryption is enabled via matrix-sdk.` / `Matrix room-key backup is enabled for this device.` / `Matrix device '...' is verified for E2EE.` | `Matrix whoami failed; falling back to configured session hints for E2EE session restore:` / `Matrix whoami failed while resolving listener user_id; using configured user_id hint:` / `Matrix room-key backup is not enabled for this device...` / `Matrix device '...' is not verified...` | `Matrix sync error: ... retrying...` | | Signal | `Signal channel listening via SSE on` | (allowlist checks are enforced by `allowed_from`) | `Signal SSE returned ...` / `Signal SSE connect error:` | | WhatsApp (channel) | `WhatsApp channel active (webhook mode).` | `WhatsApp: ignoring message from unauthorized number:` | `WhatsApp send failed:` | | Webhook / WhatsApp (gateway) | `WhatsApp webhook verified successfully` | `Webhook: rejected — not paired / invalid bearer token` / `Webhook: rejected request — invalid or missing X-Webhook-Secret` / `WhatsApp webhook verification failed — token mismatch` | `Webhook JSON parse error:` | @@ -336,4 +340,3 @@ If a specific channel task crashes or exits, the channel supervisor in `channels - `Channel message worker crashed:` These messages indicate automatic restart behavior is active, and you should inspect preceding logs for root cause. - diff --git a/docs/matrix-e2ee-guide.md b/docs/matrix-e2ee-guide.md index e927410..6f31089 100644 --- a/docs/matrix-e2ee-guide.md +++ b/docs/matrix-e2ee-guide.md @@ -109,8 +109,16 @@ curl -sS -H "Authorization: Bearer $MATRIX_TOKEN" \ - The bot device must receive room keys from trusted devices. - If keys are not shared to this device, encrypted events cannot be decrypted. - Verify device trust and key sharing in your Matrix client/admin workflow. +- If logs show `matrix_sdk_crypto::backups: Trying to backup room keys but no backup key was found`, key backup recovery is not enabled on this device yet. This warning is usually non-fatal for live message flow, but you should still complete key backup/recovery setup. +- If recipients see bot messages as "unverified", verify/sign the bot device from a trusted Matrix session and keep `channels_config.matrix.device_id` stable across restarts. -### E. Fresh start test +### E. Message formatting (Markdown) + +- ZeroClaw sends Matrix text replies as markdown-capable `m.room.message` text content. +- Matrix clients that support `formatted_body` should render emphasis, lists, and code blocks. +- If formatting appears as plain text, check client capability first, then confirm ZeroClaw is running a build that includes markdown-enabled Matrix output. + +### F. Fresh start test After updating config, restart daemon and send a new message (not just old timeline history). diff --git a/src/channels/matrix.rs b/src/channels/matrix.rs index fda0e9c..0b063c5 100644 --- a/src/channels/matrix.rs +++ b/src/channels/matrix.rs @@ -438,6 +438,40 @@ impl MatrixChannel { }) .to_string() } + + async fn log_e2ee_diagnostics(&self, client: &MatrixSdkClient) { + match client.encryption().get_own_device().await { + Ok(Some(device)) => { + if device.is_verified() { + tracing::info!( + "Matrix device '{}' is verified for E2EE.", + device.device_id() + ); + } else { + tracing::warn!( + "Matrix device '{}' is not verified. Some clients may label bot messages as unverified until you sign/verify this device from a trusted session.", + device.device_id() + ); + } + } + Ok(None) => { + tracing::warn!( + "Matrix own-device metadata is unavailable; verify/signing status cannot be determined." + ); + } + Err(error) => { + tracing::warn!("Matrix own-device verification check failed: {error}"); + } + } + + if client.encryption().backups().are_enabled().await { + tracing::info!("Matrix room-key backup is enabled for this device."); + } else { + tracing::warn!( + "Matrix room-key backup is not enabled for this device; `matrix_sdk_crypto::backups` warnings about missing backup keys may appear until recovery is configured." + ); + } + } } #[async_trait] @@ -465,7 +499,7 @@ impl Channel for MatrixChannel { anyhow::bail!("Matrix room '{}' is not in joined state", target_room_id); } - room.send(RoomMessageEventContent::text_plain(&message.content)) + room.send(RoomMessageEventContent::text_markdown(&message.content)) .await?; Ok(()) @@ -491,6 +525,8 @@ impl Channel for MatrixChannel { }; let client = self.matrix_client().await?; + self.log_e2ee_diagnostics(&client).await; + let _ = client.sync_once(SyncSettings::new()).await; tracing::info!( @@ -725,6 +761,20 @@ mod tests { assert!(!MatrixChannel::has_non_empty_body(" \n\t ")); } + #[test] + fn send_content_uses_markdown_formatting() { + let content = RoomMessageEventContent::text_markdown("**hello**"); + let value = serde_json::to_value(content).unwrap(); + + assert_eq!(value["msgtype"], "m.text"); + assert_eq!(value["body"], "**hello**"); + assert_eq!(value["format"], "org.matrix.custom.html"); + assert!(value["formatted_body"] + .as_str() + .unwrap_or_default() + .contains("hello")); + } + #[test] fn sync_filter_for_room_targets_requested_room() { let filter = MatrixChannel::sync_filter_for_room("!room:matrix.org", 0);