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.
This commit is contained in:
Chummy 2026-02-19 10:17:10 +08:00
parent a9fcf6b58c
commit 606f2860a0
5 changed files with 85 additions and 5 deletions

19
Cargo.lock generated
View file

@ -4225,6 +4225,24 @@ dependencies = [
"cc", "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]] [[package]]
name = "quinn" name = "quinn"
version = "0.11.9" version = "0.11.9"
@ -4657,6 +4675,7 @@ dependencies = [
"js_int", "js_int",
"js_option", "js_option",
"percent-encoding", "percent-encoding",
"pulldown-cmark",
"regex", "regex",
"ruma-common", "ruma-common",
"ruma-identifiers-validation", "ruma-identifiers-validation",

View file

@ -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"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "blocking", "multipart", "stream", "socks"] }
# Matrix client + E2EE decryption # 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 # Serialization
serde = { version = "1.0", default-features = false, features = ["derive"] } serde = { version = "1.0", default-features = false, features = ["derive"] }

View file

@ -150,6 +150,10 @@ allowed_users = ["*"]
See [Matrix E2EE Guide](./matrix-e2ee-guide.md) for encrypted-room troubleshooting. 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 ### 4.6 Signal
```toml ```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)` | | 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:` | | 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:` | | 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:` | | 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:` | | 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:` | | 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:` - `Channel message worker crashed:`
These messages indicate automatic restart behavior is active, and you should inspect preceding logs for root cause. These messages indicate automatic restart behavior is active, and you should inspect preceding logs for root cause.

View file

@ -109,8 +109,16 @@ curl -sS -H "Authorization: Bearer $MATRIX_TOKEN" \
- The bot device must receive room keys from trusted devices. - The bot device must receive room keys from trusted devices.
- If keys are not shared to this device, encrypted events cannot be decrypted. - 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. - 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). After updating config, restart daemon and send a new message (not just old timeline history).

View file

@ -438,6 +438,40 @@ impl MatrixChannel {
}) })
.to_string() .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] #[async_trait]
@ -465,7 +499,7 @@ impl Channel for MatrixChannel {
anyhow::bail!("Matrix room '{}' is not in joined state", target_room_id); 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?; .await?;
Ok(()) Ok(())
@ -491,6 +525,8 @@ impl Channel for MatrixChannel {
}; };
let client = self.matrix_client().await?; let client = self.matrix_client().await?;
self.log_e2ee_diagnostics(&client).await;
let _ = client.sync_once(SyncSettings::new()).await; let _ = client.sync_once(SyncSettings::new()).await;
tracing::info!( tracing::info!(
@ -725,6 +761,20 @@ mod tests {
assert!(!MatrixChannel::has_non_empty_body(" \n\t ")); 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("<strong>hello</strong>"));
}
#[test] #[test]
fn sync_filter_for_room_targets_requested_room() { fn sync_filter_for_room_targets_requested_room() {
let filter = MatrixChannel::sync_filter_for_room("!room:matrix.org", 0); let filter = MatrixChannel::sync_filter_for_room("!room:matrix.org", 0);