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",
]
[[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",

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"] }
# 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"] }

View file

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

View file

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

View file

@ -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("<strong>hello</strong>"));
}
#[test]
fn sync_filter_for_room_targets_requested_room() {
let filter = MatrixChannel::sync_filter_for_room("!room:matrix.org", 0);