mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
matrix: fixed the text-message checkpoint regression (#595)
* matrix: fixed the text-message checkpoint regression * matrix: improve formatting * matrix: fix tests
This commit is contained in:
@@ -55,6 +55,7 @@ impl BridgeState {
|
||||
if s.last_rx_time.is_none() {
|
||||
s.last_rx_time = s.last_checked_at;
|
||||
}
|
||||
s.last_checked_at = None;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
@@ -147,6 +148,7 @@ async fn poll_once(
|
||||
continue;
|
||||
}
|
||||
|
||||
state.update_with(msg);
|
||||
// persist after each processed message
|
||||
if let Err(e) = state.save(state_path) {
|
||||
error!("Error saving state: {:?}", e);
|
||||
@@ -214,16 +216,18 @@ async fn handle_message(
|
||||
.unwrap_or_else(|| node.long_name.clone());
|
||||
|
||||
let preset_short = modem_preset_short(&msg.modem_preset);
|
||||
let body = format!(
|
||||
"[{freq}][{preset_short}][{channel}][{short}] {text}",
|
||||
let prefix = format!(
|
||||
"[{freq}][{preset_short}][{channel}][{short}]",
|
||||
freq = msg.lora_freq,
|
||||
preset_short = preset_short,
|
||||
channel = msg.channel_name,
|
||||
short = short,
|
||||
text = msg.text,
|
||||
);
|
||||
let (body, formatted_body) = format_message_bodies(&prefix, &msg.text);
|
||||
|
||||
matrix.send_text_message_as(&user_id, &body).await?;
|
||||
matrix
|
||||
.send_formatted_message_as(&user_id, &body, &formatted_body)
|
||||
.await?;
|
||||
|
||||
state.update_with(msg);
|
||||
Ok(())
|
||||
@@ -242,6 +246,29 @@ fn modem_preset_short(preset: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build plain text + HTML message bodies with inline-code metadata.
|
||||
fn format_message_bodies(prefix: &str, text: &str) -> (String, String) {
|
||||
let body = format!("`{}` {}", prefix, text);
|
||||
let formatted_body = format!("<code>{}</code> {}", escape_html(prefix), escape_html(text));
|
||||
(body, formatted_body)
|
||||
}
|
||||
|
||||
/// Minimal HTML escaping for Matrix formatted_body payloads.
|
||||
fn escape_html(input: &str) -> String {
|
||||
let mut escaped = String::with_capacity(input.len());
|
||||
for ch in input.chars() {
|
||||
match ch {
|
||||
'&' => escaped.push_str("&"),
|
||||
'<' => escaped.push_str("<"),
|
||||
'>' => escaped.push_str(">"),
|
||||
'"' => escaped.push_str("""),
|
||||
'\'' => escaped.push_str("'"),
|
||||
_ => escaped.push(ch),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -276,6 +303,18 @@ mod tests {
|
||||
assert_eq!(modem_preset_short("MediumFast"), "MF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_message_bodies_escape_html() {
|
||||
let (body, formatted) = format_message_bodies("[868][LF]", "Hello <&>");
|
||||
assert_eq!(body, "`[868][LF]` Hello <&>");
|
||||
assert_eq!(formatted, "<code>[868][LF]</code> Hello <&>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_html_escapes_quotes() {
|
||||
assert_eq!(escape_html("a\"b'c"), "a"b'c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_state_initially_forwards_all() {
|
||||
let state = BridgeState::default();
|
||||
@@ -619,6 +658,10 @@ mod tests {
|
||||
.as_str(),
|
||||
)
|
||||
.match_query(format!("user_id={}&access_token=AS_TOKEN", encoded_user).as_str())
|
||||
.match_body(mockito::Matcher::PartialJson(serde_json::json!({
|
||||
"msgtype": "m.text",
|
||||
"format": "org.matrix.custom.html",
|
||||
})))
|
||||
.with_status(200)
|
||||
.create();
|
||||
|
||||
|
||||
@@ -165,12 +165,19 @@ impl MatrixAppserviceClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a plain text message into the configured room as puppet user_id.
|
||||
pub async fn send_text_message_as(&self, user_id: &str, body_text: &str) -> anyhow::Result<()> {
|
||||
/// Send a text message with HTML formatting into the configured room as puppet user_id.
|
||||
pub async fn send_formatted_message_as(
|
||||
&self,
|
||||
user_id: &str,
|
||||
body_text: &str,
|
||||
formatted_body: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
#[derive(Serialize)]
|
||||
struct MsgContent<'a> {
|
||||
msgtype: &'a str,
|
||||
body: &'a str,
|
||||
format: &'a str,
|
||||
formatted_body: &'a str,
|
||||
}
|
||||
|
||||
let txn_id = self.txn_counter.fetch_add(1, Ordering::SeqCst);
|
||||
@@ -189,24 +196,23 @@ impl MatrixAppserviceClient {
|
||||
let content = MsgContent {
|
||||
msgtype: "m.text",
|
||||
body: body_text,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body,
|
||||
};
|
||||
|
||||
let resp = self.http.put(&url).json(&content).send().await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
// optional: pull a short body snippet for debugging
|
||||
let body_snip = resp.text().await.unwrap_or_default();
|
||||
|
||||
// Log for observability
|
||||
tracing::warn!(
|
||||
"Failed to send message as {}: status {}, body: {}",
|
||||
"Failed to send formatted message as {}: status {}, body: {}",
|
||||
user_id,
|
||||
status,
|
||||
body_snip
|
||||
);
|
||||
|
||||
// Propagate an error so callers know this message was NOT delivered
|
||||
return Err(anyhow::anyhow!(
|
||||
"Matrix send failed for {} with status {}",
|
||||
user_id,
|
||||
@@ -441,7 +447,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_text_message_as_success() {
|
||||
async fn test_send_formatted_message_as_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let user_id = "@test:example.org";
|
||||
let room_id = "!roomid:example.org";
|
||||
@@ -464,45 +470,20 @@ mod tests {
|
||||
let mock = server
|
||||
.mock("PUT", path.as_str())
|
||||
.match_query(query.as_str())
|
||||
.match_body(mockito::Matcher::PartialJson(serde_json::json!({
|
||||
"msgtype": "m.text",
|
||||
"body": "`[meta]` hello",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<code>[meta]</code> hello",
|
||||
})))
|
||||
.with_status(200)
|
||||
.create();
|
||||
|
||||
let result = client.send_text_message_as(user_id, "hello").await;
|
||||
let result = client
|
||||
.send_formatted_message_as(user_id, "`[meta]` hello", "<code>[meta]</code> hello")
|
||||
.await;
|
||||
|
||||
mock.assert();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_text_message_as_fail() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let user_id = "@test:example.org";
|
||||
let room_id = "!roomid:example.org";
|
||||
let encoded_user = urlencoding::encode(user_id);
|
||||
let encoded_room = urlencoding::encode(room_id);
|
||||
|
||||
let client = {
|
||||
let mut cfg = dummy_cfg();
|
||||
cfg.homeserver = server.url();
|
||||
cfg.room_id = room_id.to_string();
|
||||
MatrixAppserviceClient::new(reqwest::Client::new(), cfg)
|
||||
};
|
||||
let txn_id = client.txn_counter.load(Ordering::SeqCst);
|
||||
let query = format!("user_id={}&access_token=AS_TOKEN", encoded_user);
|
||||
let path = format!(
|
||||
"/_matrix/client/v3/rooms/{}/send/m.room.message/{}",
|
||||
encoded_room, txn_id
|
||||
);
|
||||
|
||||
let mock = server
|
||||
.mock("PUT", path.as_str())
|
||||
.match_query(query.as_str())
|
||||
.with_status(500)
|
||||
.create();
|
||||
|
||||
let result = client.send_text_message_as(user_id, "hello").await;
|
||||
|
||||
mock.assert();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user