use matrix_sdk::media::{MediaFormat, MediaRequestParameters}; use matrix_sdk::room::Room; use matrix_sdk::RoomMemberships; use ruma::events::room::message::{ MessageType, OriginalSyncRoomMessageEvent, Relation, RoomMessageEventContent, }; use ruma::events::relation::{Annotation, InReplyTo}; use ruma::events::reaction::ReactionEventContent; use ruma::OwnedEventId; /// Extract the plain-text body from a message event. pub fn extract_body(event: &OriginalSyncRoomMessageEvent) -> Option { match &event.content.msgtype { MessageType::Text(text) => Some(text.body.clone()), MessageType::Notice(notice) => Some(notice.body.clone()), MessageType::Emote(emote) => Some(emote.body.clone()), _ => None, } } /// Check if this event is an edit (m.replace relation) and return the new body. pub fn extract_edit(event: &OriginalSyncRoomMessageEvent) -> Option<(OwnedEventId, String)> { if let Some(Relation::Replacement(replacement)) = &event.content.relates_to { let new_body = match &replacement.new_content.msgtype { MessageType::Text(text) => text.body.clone(), MessageType::Notice(notice) => notice.body.clone(), _ => return None, }; return Some((replacement.event_id.clone(), new_body)); } None } /// Extract the event ID being replied to, if any. pub fn extract_reply_to(event: &OriginalSyncRoomMessageEvent) -> Option { if let Some(Relation::Reply { in_reply_to }) = &event.content.relates_to { return Some(in_reply_to.event_id.clone()); } None } /// Extract thread root event ID, if any. pub fn extract_thread_id(event: &OriginalSyncRoomMessageEvent) -> Option { if let Some(Relation::Thread(thread)) = &event.content.relates_to { return Some(thread.event_id.clone()); } None } /// Build a reply message content with m.in_reply_to relation and markdown rendering. pub fn make_reply_content(body: &str, reply_to_event_id: OwnedEventId) -> RoomMessageEventContent { let mut content = RoomMessageEventContent::text_markdown(body); content.relates_to = Some(Relation::Reply { in_reply_to: InReplyTo::new(reply_to_event_id), }); content } /// Build a threaded reply — shows up in Matrix threads UI. /// The thread root is the event being replied to (creates the thread on first use). pub fn make_thread_reply( body: &str, thread_root_id: OwnedEventId, ) -> RoomMessageEventContent { use ruma::events::relation::Thread; let mut content = RoomMessageEventContent::text_markdown(body); let thread = Thread::plain(thread_root_id.clone(), thread_root_id); content.relates_to = Some(Relation::Thread(thread)); content } /// Send an emoji reaction to a message. pub async fn send_reaction( room: &Room, event_id: OwnedEventId, emoji: &str, ) -> anyhow::Result<()> { let annotation = Annotation::new(event_id, emoji.to_string()); let content = ReactionEventContent::new(annotation); room.send(content).await?; Ok(()) } /// Extract image info from an m.image message event. /// Returns (mxc_url, mimetype, body/caption) if present. pub fn extract_image(event: &OriginalSyncRoomMessageEvent) -> Option<(String, String, String)> { if let MessageType::Image(image) = &event.content.msgtype { let url = match &image.source { ruma::events::room::MediaSource::Plain(mxc) => mxc.to_string(), ruma::events::room::MediaSource::Encrypted(_) => return None, }; let mime = image .info .as_ref() .and_then(|i| i.mimetype.clone()) .unwrap_or_else(|| "image/png".to_string()); let caption = image.body.clone(); Some((url, mime, caption)) } else { None } } /// Download image bytes from a Matrix mxc:// URL via the media API. /// Returns the raw bytes as a base64 data URI suitable for Mistral vision. pub async fn download_image_as_data_uri( client: &matrix_sdk::Client, event: &OriginalSyncRoomMessageEvent, ) -> Option { if let MessageType::Image(image) = &event.content.msgtype { let media_source = &image.source; let mime = image .info .as_ref() .and_then(|i| i.mimetype.clone()) .unwrap_or_else(|| "image/png".to_string()); let request = MediaRequestParameters { source: media_source.clone(), format: MediaFormat::File, }; match client.media().get_media_content(&request, true).await { Ok(bytes) => { use base64::Engine; let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes); Some(format!("data:{};base64,{}", mime, b64)) } Err(e) => { tracing::warn!("Failed to download image: {e}"); None } } } else { None } } /// Get the display name for a room. pub fn room_display_name(room: &Room) -> String { room.cached_display_name() .map(|n| n.to_string()) .unwrap_or_else(|| room.room_id().to_string()) } /// Get member display names for a room. pub async fn room_member_names(room: &Room) -> Vec { match room.members(RoomMemberships::JOIN).await { Ok(members) => members .iter() .map(|m| { m.display_name() .unwrap_or_else(|| m.user_id().as_str()) .to_string() }) .collect(), Err(_) => Vec::new(), } }