Stickers, voice messages and places
The rest of what Element sends and this backend dropped. A sticker is an image that arrives under its own event type rather than as a message with a msgtype, so it was failing the type gate and vanishing. Given the msgtype it lacks, the whole media path already here - fetch, cache, describe - reads it as the image it is. A voice message is an audio file with a note saying it was spoken. It said "Voice message.ogg" and now says what it is; the audio itself was already arriving as an attachment. A location is the one that cannot be drawn honestly: `geo:52.2,0.12` means nothing to a person, so it becomes what the sender called it plus a link to a map that can show the point. Drawing the map here would mean fetching tiles from somebody else's server for every message mentioning a street corner. Both readers are tested, including the geo URI with a precision suffix and the audio file that is not a voice message. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -688,6 +688,7 @@ async fn handle_timeline_event(
|
||||
if outer_type != protocol::EVENT_ROOM_MESSAGE
|
||||
&& outer_type != protocol::EVENT_ROOM_ENCRYPTED
|
||||
&& outer_type != protocol::EVENT_REACTION
|
||||
&& outer_type != protocol::EVENT_STICKER
|
||||
// A poll is three kinds of event and none of them is a message.
|
||||
&& !polls::is_poll_event(outer_type)
|
||||
{
|
||||
@@ -719,7 +720,7 @@ async fn handle_timeline_event(
|
||||
// reaction can be encrypted too, not just messages) - so the
|
||||
// effective type/content used below come from the decrypted envelope
|
||||
// when there is one, the outer event otherwise.
|
||||
let (effective_type, content, undecryptable): (String, Value, bool) = if outer_type == protocol::EVENT_ROOM_ENCRYPTED {
|
||||
let (effective_type, mut content, undecryptable): (String, Value, bool) = if outer_type == protocol::EVENT_ROOM_ENCRYPTED {
|
||||
match decrypt_event(session, event, room_id).await {
|
||||
Ok(decrypted) => {
|
||||
let t = decrypted["type"].as_str().unwrap_or(protocol::EVENT_ROOM_MESSAGE).to_string();
|
||||
@@ -757,7 +758,15 @@ async fn handle_timeline_event(
|
||||
return;
|
||||
}
|
||||
|
||||
if effective_type != protocol::EVENT_ROOM_MESSAGE {
|
||||
// A sticker is an image that arrived under its own event type rather
|
||||
// than as a message with a msgtype. Given the msgtype it is missing, the
|
||||
// whole media path below - fetch, cache, describe - reads it as what it
|
||||
// is instead of dropping it for having the wrong envelope.
|
||||
if effective_type == protocol::EVENT_STICKER {
|
||||
if let Some(object) = content.as_object_mut() {
|
||||
object.entry("msgtype").or_insert_with(|| Value::from("m.image"));
|
||||
}
|
||||
} else if effective_type != protocol::EVENT_ROOM_MESSAGE {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -796,6 +805,12 @@ async fn handle_timeline_event(
|
||||
let path = cached_encrypted_media_path(homeserver_url, access_token, file, ext).await;
|
||||
attachments.push(build_attachment(&content, path, thumbnail_for(&content, homeserver_url, access_token).await));
|
||||
(protocol::message_body(&content).0, false)
|
||||
} else if let Some(place) = protocol::location_of(&content) {
|
||||
// A place, as the one thing a chat client can honestly do with one:
|
||||
// what the sender called it, and a link to a map that can show it.
|
||||
// Drawing the map here would mean shipping tiles from somebody's
|
||||
// server on every message that mentions a street corner.
|
||||
(place, false)
|
||||
} else {
|
||||
protocol::message_body(&content)
|
||||
};
|
||||
@@ -851,6 +866,13 @@ async fn handle_timeline_event(
|
||||
}
|
||||
}
|
||||
|
||||
// A voice message is an audio file with a note on it saying it was
|
||||
// spoken rather than sent. Element shows it as a waveform; this at least
|
||||
// stops it reading as somebody attaching "Voice message.ogg".
|
||||
if protocol::is_voice_message(&content) {
|
||||
body = "Voice message".to_string();
|
||||
}
|
||||
|
||||
let sender_avatar_url = state.runtime.get_matrix_member_avatar(account_id, sender);
|
||||
|
||||
// The time the server recorded, not the time this arrived. They are the
|
||||
|
||||
@@ -16,6 +16,8 @@ pub const EVENT_ROOM_ENCRYPTION: &str = "m.room.encryption";
|
||||
pub const EVENT_ROOM_ENCRYPTED: &str = "m.room.encrypted";
|
||||
pub const EVENT_REACTION: &str = "m.reaction";
|
||||
pub const EVENT_REDACTION: &str = "m.room.redaction";
|
||||
/// A sticker: an image under its own event type rather than a msgtype.
|
||||
pub const EVENT_STICKER: &str = "m.sticker";
|
||||
|
||||
pub fn event_type(event: &Value) -> &str {
|
||||
event["type"].as_str().unwrap_or("")
|
||||
@@ -56,6 +58,38 @@ pub fn message_body(content: &Value) -> (String, bool) {
|
||||
(body, is_action)
|
||||
}
|
||||
|
||||
/// Whether this is a voice message rather than an attached audio file.
|
||||
///
|
||||
/// The marker is still under its MSC prefix everywhere that sends it, and the
|
||||
/// stable name is read too so this keeps working when that lands.
|
||||
pub fn is_voice_message(content: &Value) -> bool {
|
||||
content["msgtype"].as_str() == Some("m.audio")
|
||||
&& (content.get("org.matrix.msc3245.voice").is_some() || content.get("m.voice").is_some())
|
||||
}
|
||||
|
||||
/// A location message, as a line somebody can act on.
|
||||
///
|
||||
/// `geo_uri` is "geo:lat,long" and means nothing to a person; the description
|
||||
/// beside it usually does. Both are given, with a link to a map that can show
|
||||
/// the point - pointing at one is honest, drawing one would mean fetching
|
||||
/// tiles from somebody's server for every message that mentions a place.
|
||||
pub fn location_of(content: &Value) -> Option<String> {
|
||||
if content["msgtype"].as_str() != Some("m.location") {
|
||||
return None;
|
||||
}
|
||||
let geo = content["geo_uri"].as_str()?;
|
||||
let point = geo.strip_prefix("geo:")?.split(';').next()?;
|
||||
let (lat, long) = point.split_once(',')?;
|
||||
let described = content["body"].as_str().filter(|b| !b.is_empty()).unwrap_or("Location");
|
||||
Some(format!(
|
||||
"{described} — https://www.openstreetmap.org/?mlat={}&mlon={}#map=16/{}/{}",
|
||||
lat.trim(),
|
||||
long.trim(),
|
||||
lat.trim(),
|
||||
long.trim()
|
||||
))
|
||||
}
|
||||
|
||||
/// The sender's formatted version of the body, if there is one.
|
||||
///
|
||||
/// Only `org.matrix.custom.html` counts: the spec allows other formats and a
|
||||
@@ -197,6 +231,40 @@ pub fn encrypted_media_file(content: &Value) -> Option<&Value> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A place, in the only form a chat client can honestly show one.
|
||||
#[test]
|
||||
fn reads_a_location_as_something_to_press() {
|
||||
let content = serde_json::json!({
|
||||
"msgtype": "m.location",
|
||||
"body": "The pub",
|
||||
"geo_uri": "geo:52.2053,0.1218"
|
||||
});
|
||||
let line = location_of(&content).expect("a location");
|
||||
assert!(line.starts_with("The pub — https://www.openstreetmap.org/?mlat=52.2053&mlon=0.1218"), "{line}");
|
||||
|
||||
// A geo URI with a precision suffix is still a point.
|
||||
let precise = serde_json::json!({ "msgtype": "m.location", "geo_uri": "geo:52.2,0.12;u=35" });
|
||||
assert!(location_of(&precise).expect("a location").starts_with("Location — "));
|
||||
|
||||
// Anything else is not a location and must not be read as one.
|
||||
assert_eq!(location_of(&serde_json::json!({ "msgtype": "m.text", "body": "geo:1,2" })), None);
|
||||
}
|
||||
|
||||
/// A voice message is an audio file with a note saying it was spoken.
|
||||
#[test]
|
||||
fn tells_a_voice_message_from_an_audio_file() {
|
||||
assert!(is_voice_message(&serde_json::json!({
|
||||
"msgtype": "m.audio",
|
||||
"org.matrix.msc3245.voice": {}
|
||||
})));
|
||||
// The stable name, for when it lands.
|
||||
assert!(is_voice_message(&serde_json::json!({ "msgtype": "m.audio", "m.voice": {} })));
|
||||
// An ordinary attached song is not a voice message.
|
||||
assert!(!is_voice_message(&serde_json::json!({ "msgtype": "m.audio" })));
|
||||
}
|
||||
|
||||
use super::formatted_body;
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user