| 1 | use ruma::{ |
| 2 | OwnedEventId, UserId, |
| 3 | events::{ |
| 4 | poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent}, |
| 5 | room::message::{ |
| 6 | MessageType, Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, |
| 7 | }, |
| 8 | sticker::StickerEventContent, |
| 9 | }, |
| 10 | }; |
| 11 | use serde_json::Value; |
| 12 | |
| 13 | use super::{ |
| 14 | link_preview::sender_previews, |
| 15 | media::{ |
| 16 | MediaRegistration, media_descriptor_for_sticker, media_registration, |
| 17 | media_sticker_registration, |
| 18 | }, |
| 19 | timeline::formatted_body, |
| 20 | }; |
| 21 | use crate::{ |
| 22 | Message, MessageMentions, ReplyDetails, ReplyReference, SendState, Sender, TimelineContent, |
| 23 | TimelineEvent, TimelineItem, TimelineItemId, UtdCause, |
| 24 | }; |
| 25 | |
| 26 | pub(crate) fn normalise_observed( |
| 27 | value: &Value, |
| 28 | current_user_id: Option<&UserId>, |
| 29 | identity: &str, |
| 30 | ) -> Option<(TimelineItem, Vec<MediaRegistration>)> { |
| 31 | let event_id: OwnedEventId = serde_json::from_value(value.get("event_id")?.clone()).ok()?; |
| 32 | let sender_id: ruma::OwnedUserId = serde_json::from_value(value.get("sender")?.clone()).ok()?; |
| 33 | let timestamp_ms = value.get("origin_server_ts")?.as_u64()?; |
| 34 | let event_type = value.get("type")?.as_str()?.to_owned(); |
| 35 | let own = current_user_id.is_some_and(|user_id| user_id.as_str() == sender_id.as_str()); |
| 36 | let redacted = value |
| 37 | .get("unsigned") |
| 38 | .and_then(|unsigned| unsigned.get("redacted_because")) |
| 39 | .is_some(); |
| 40 | let mut media = Vec::new(); |
| 41 | let (content, reply_to, thread_root) = if redacted { |
| 42 | (TimelineContent::Redacted, None, None) |
| 43 | } else if event_type == "m.room.message" { |
| 44 | match value |
| 45 | .get("content") |
| 46 | .cloned() |
| 47 | .and_then(|content| serde_json::from_value::<RoomMessageEventContent>(content).ok()) |
| 48 | { |
| 49 | Some(mut message) => { |
| 50 | let (reply_to, thread_root) = relation_refs(message.relates_to.as_ref()); |
| 51 | let relation_replacement = match &message.relates_to { |
| 52 | Some(Relation::Replacement(replacement)) => { |
| 53 | Some(replacement.new_content.clone()) |
| 54 | } |
| 55 | _ => None, |
| 56 | }; |
| 57 | let bundled_replacement = value |
| 58 | .get("unsigned") |
| 59 | .and_then(|unsigned| unsigned.get("m.relations")) |
| 60 | .and_then(|relations| relations.get("m.replace")) |
| 61 | .and_then(|edit| { |
| 62 | let edit_sender = edit.get("sender")?.as_str()?; |
| 63 | if edit_sender != sender_id.as_str() { |
| 64 | return None; |
| 65 | } |
| 66 | edit.get("content")?.get("m.new_content").cloned() |
| 67 | }) |
| 68 | .and_then(|content| { |
| 69 | serde_json::from_value::<RoomMessageEventContentWithoutRelation>(content) |
| 70 | .ok() |
| 71 | }); |
| 72 | let edited = relation_replacement.is_some() |
| 73 | || bundled_replacement.is_some() |
| 74 | || value |
| 75 | .get("unsigned") |
| 76 | .and_then(|unsigned| unsigned.get("macltok_edited")) |
| 77 | .and_then(Value::as_bool) |
| 78 | .unwrap_or(false); |
| 79 | if let Some(replacement) = relation_replacement.or(bundled_replacement) { |
| 80 | message.apply_replacement(replacement); |
| 81 | } |
| 82 | let message_type = &message.msgtype; |
| 83 | media.extend(media_registration(identity, message_type)); |
| 84 | let previews = sender_previews(Some(identity), message_type); |
| 85 | media.extend( |
| 86 | previews |
| 87 | .iter() |
| 88 | .filter_map(|(_, registration)| registration.clone()), |
| 89 | ); |
| 90 | ( |
| 91 | TimelineContent::Message(Message { |
| 92 | kind: super::message_kind(message_type), |
| 93 | body: message.body().to_owned(), |
| 94 | geo_uri: match message_type { |
| 95 | MessageType::Location(location) => Some(location.geo_uri.clone()), |
| 96 | _ => None, |
| 97 | }, |
| 98 | formatted_body: formatted_body(message_type), |
| 99 | mentions: message.mentions.as_ref().map(|mentions| MessageMentions { |
| 100 | user_ids: mentions.user_ids.iter().cloned().collect(), |
| 101 | room: mentions.room, |
| 102 | }), |
| 103 | edited, |
| 104 | media: super::media::media_from_message(identity, message_type), |
| 105 | link_previews: previews.into_iter().map(|(preview, _)| preview).collect(), |
| 106 | }), |
| 107 | reply_to, |
| 108 | thread_root, |
| 109 | ) |
| 110 | } |
| 111 | None => (TimelineContent::Other { event_type }, None, None), |
| 112 | } |
| 113 | } else if event_type == "m.room.encrypted" { |
| 114 | ( |
| 115 | TimelineContent::UnableToDecrypt(UtdCause::Unknown), |
| 116 | None, |
| 117 | None, |
| 118 | ) |
| 119 | } else if event_type == "m.sticker" { |
| 120 | match value |
| 121 | .get("content") |
| 122 | .cloned() |
| 123 | .and_then(|content| serde_json::from_value::<StickerEventContent>(content).ok()) |
| 124 | { |
| 125 | Some(sticker) => { |
| 126 | let (reply_to, thread_root) = relation_refs(sticker.relates_to.as_ref()); |
| 127 | media.extend(media_sticker_registration(identity, &sticker)); |
| 128 | ( |
| 129 | TimelineContent::Sticker { |
| 130 | body: sticker.body.clone(), |
| 131 | media: media_descriptor_for_sticker(identity, &sticker), |
| 132 | }, |
| 133 | reply_to, |
| 134 | thread_root, |
| 135 | ) |
| 136 | } |
| 137 | None => (TimelineContent::Other { event_type }, None, None), |
| 138 | } |
| 139 | } else if event_type == "m.poll.start" || event_type == "org.matrix.msc3381.poll.start" { |
| 140 | let poll_content = value.get("content"); |
| 141 | let question = poll_content.and_then(|content| poll_question(content, &event_type)); |
| 142 | match question { |
| 143 | Some(question) => { |
| 144 | let (reply_to, thread_root) = |
| 145 | poll_content.map(raw_relation_refs).unwrap_or_default(); |
| 146 | (TimelineContent::Poll { question }, reply_to, thread_root) |
| 147 | } |
| 148 | None => (TimelineContent::Other { event_type }, None, None), |
| 149 | } |
| 150 | } else if let Some(state_key) = value.get("state_key").and_then(Value::as_str) { |
| 151 | ( |
| 152 | TimelineContent::OtherState { |
| 153 | event_type, |
| 154 | state_key: state_key.to_owned(), |
| 155 | }, |
| 156 | None, |
| 157 | None, |
| 158 | ) |
| 159 | } else { |
| 160 | (TimelineContent::Other { event_type }, None, None) |
| 161 | }; |
| 162 | let event = TimelineEvent { |
| 163 | item_id: TimelineItemId::new(identity.to_owned()), |
| 164 | event_id: Some(event_id), |
| 165 | transaction_id: None, |
| 166 | sender: Sender { |
| 167 | user_id: sender_id, |
| 168 | display_name: None, |
| 169 | avatar: None, |
| 170 | name_ambiguous: false, |
| 171 | }, |
| 172 | timestamp_ms, |
| 173 | own, |
| 174 | send_state: SendState::Sent, |
| 175 | content, |
| 176 | reply_to: reply_to.map(|event_id| ReplyReference { |
| 177 | event_id, |
| 178 | details: ReplyDetails::Pending, |
| 179 | }), |
| 180 | thread_root, |
| 181 | thread_summary: None, |
| 182 | reactions: Vec::new(), |
| 183 | read_by: Vec::new(), |
| 184 | editable: false, |
| 185 | highlight: false, |
| 186 | }; |
| 187 | Some((TimelineItem::Event(Box::new(event)), media)) |
| 188 | } |
| 189 | |
| 190 | fn relation_refs<C>( |
| 191 | relation: Option<&Relation<C>>, |
| 192 | ) -> (Option<OwnedEventId>, Option<OwnedEventId>) { |
| 193 | match relation { |
| 194 | Some(Relation::Reply(reply)) => (Some(reply.in_reply_to.event_id.clone()), None), |
| 195 | Some(Relation::Thread(thread)) => ( |
| 196 | thread |
| 197 | .in_reply_to |
| 198 | .as_ref() |
| 199 | .map(|reply| reply.event_id.clone()), |
| 200 | Some(thread.event_id.clone()), |
| 201 | ), |
| 202 | _ => (None, None), |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | fn raw_relation_refs(content: &Value) -> (Option<OwnedEventId>, Option<OwnedEventId>) { |
| 207 | let Some(relation) = content.get("m.relates_to") else { |
| 208 | return (None, None); |
| 209 | }; |
| 210 | let reply_to = relation |
| 211 | .get("m.in_reply_to") |
| 212 | .and_then(|reply| reply.get("event_id")) |
| 213 | .and_then(Value::as_str) |
| 214 | .and_then(|event_id| event_id.parse().ok()); |
| 215 | let thread_root = (relation.get("rel_type").and_then(Value::as_str) == Some("m.thread")) |
| 216 | .then(|| relation.get("event_id").and_then(Value::as_str)) |
| 217 | .flatten() |
| 218 | .and_then(|event_id| event_id.parse().ok()); |
| 219 | (reply_to, thread_root) |
| 220 | } |
| 221 | |
| 222 | pub(crate) fn poll_question(content: &Value, event_type: &str) -> Option<String> { |
| 223 | let typed_question = match event_type { |
| 224 | "m.poll.start" => serde_json::from_value::<PollStartEventContent>(content.clone()) |
| 225 | .ok() |
| 226 | .and_then(|poll| poll.poll.question.text.find_plain().map(str::to_owned)), |
| 227 | "org.matrix.msc3381.poll.start" => { |
| 228 | serde_json::from_value::<UnstablePollStartEventContent>(content.clone()) |
| 229 | .ok() |
| 230 | .map(|poll| poll.poll_start().question.text.clone()) |
| 231 | } |
| 232 | _ => None, |
| 233 | }; |
| 234 | typed_question |
| 235 | .or_else(|| { |
| 236 | content |
| 237 | .get("m.poll") |
| 238 | .and_then(|poll| poll.get("question")) |
| 239 | .and_then(|question| question.get("m.text")) |
| 240 | .and_then(generic_text) |
| 241 | }) |
| 242 | .or_else(|| { |
| 243 | content |
| 244 | .get("org.matrix.msc3381.poll.start") |
| 245 | .and_then(|poll| poll.get("question")) |
| 246 | .and_then(|question| question.get("org.matrix.msc1767.text")) |
| 247 | .and_then(generic_text) |
| 248 | }) |
| 249 | .or_else(|| { |
| 250 | ["org.matrix.msc1767.text", "m.text", "question", "body"] |
| 251 | .iter() |
| 252 | .find_map(|key| content.get(*key).and_then(generic_text)) |
| 253 | }) |
| 254 | } |
| 255 | |
| 256 | fn generic_text(value: &Value) -> Option<String> { |
| 257 | match value { |
| 258 | Value::String(text) => Some(text.clone()), |
| 259 | Value::Array(representations) => representations |
| 260 | .iter() |
| 261 | .find(|representation| { |
| 262 | representation.get("mimetype").and_then(Value::as_str) == Some("text/plain") |
| 263 | }) |
| 264 | .or_else(|| representations.first()) |
| 265 | .and_then(|representation| representation.get("body")) |
| 266 | .and_then(Value::as_str) |
| 267 | .map(str::to_owned), |
| 268 | Value::Object(_) => value.get("body").and_then(Value::as_str).map(str::to_owned), |
| 269 | _ => None, |
| 270 | } |
| 271 | } |