use anyhow::Result;
use matrix_sdk::Client;
use matrix_sdk::ruma::{OwnedEventId, OwnedRoomId, OwnedUserId};
use crate::state::room_state::*;
pub async fn load_room_messages(
client: &Client,
room_id: &OwnedRoomId,
from_token: Option<&str>,
) -> Result<(Vec<TimelineItemKind>, Option<String>)> {
let room = client
.get_room(room_id)
.ok_or_else(|| anyhow::anyhow!("Room not found: {room_id}"))?;
let own_user_id = client.user_id().map(|u| u.to_owned());
let mut options = matrix_sdk::room::MessagesOptions::backward();
if let Some(token) = from_token {
options.from = Some(token.to_string());
}
let response = room.messages(options).await?;
let mut items = Vec::new();
for timeline_event in &response.chunk {
if let Some(json) = event_to_json(timeline_event) {
if let Some(item) = convert_raw_event(&json, own_user_id.as_ref()) {
items.push(item);
}
}
}
items.reverse();
Ok((items, response.end))
}
fn event_to_json(
event: &matrix_sdk::deserialized_responses::TimelineEvent,
) -> Option<serde_json::Value> {
let raw = event.raw();
serde_json::from_str(raw.json().get()).ok()
}
fn convert_raw_event(
json: &serde_json::Value,
own_user_id: Option<&OwnedUserId>,
) -> Option<TimelineItemKind> {
let event_type = json.get("type")?.as_str()?;
let sender_str = json.get("sender")?.as_str()?;
let sender: OwnedUserId = sender_str.try_into().ok()?;
let event_id: Option<OwnedEventId> = json
.get("event_id")
.and_then(|v| v.as_str())
.and_then(|s| s.try_into().ok());
let timestamp = json
.get("origin_server_ts")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let is_own = own_user_id.map_or(false, |u| u.as_str() == sender_str);
match event_type {
"m.room.message" => {
let content = json.get("content")?;
let timeline_content = convert_message_content(content)?;
Some(TimelineItemKind::Event(EventTimelineData {
event_id,
sender,
sender_display_name: sender_str.to_string(),
sender_avatar_url: None,
timestamp,
content: timeline_content,
reactions: Vec::new(),
is_edited: content
.get("m.relates_to")
.and_then(|r| r.get("rel_type"))
.and_then(|t| t.as_str())
.map_or(false, |t| t == "m.replace"),
reply_to: extract_reply_preview(content),
read_receipts: Vec::new(),
send_state: SendState::None,
is_own_message: is_own,
}))
}
"m.room.encrypted" => Some(TimelineItemKind::Event(EventTimelineData {
event_id,
sender,
sender_display_name: sender_str.to_string(),
sender_avatar_url: None,
timestamp,
content: TimelineContent::EncryptionError {
message: "Unable to decrypt message".to_string(),
},
reactions: Vec::new(),
is_edited: false,
reply_to: None,
read_receipts: Vec::new(),
send_state: SendState::None,
is_own_message: is_own,
})),
"m.room.member" => {
let content = json.get("content")?;
let membership = content.get("membership")?.as_str()?;
let description = format_membership_event(sender_str, membership, content);
Some(make_state_event(
event_id,
sender,
sender_str,
timestamp,
description,
is_own,
))
}
"m.room.name" | "m.room.topic" | "m.room.avatar" | "m.room.create"
| "m.room.power_levels" | "m.room.join_rules" | "m.room.history_visibility"
| "m.room.canonical_alias" | "m.room.guest_access" => {
let content = json.get("content")?;
let description = format_state_event(event_type, sender_str, content);
Some(make_state_event(
event_id,
sender,
sender_str,
timestamp,
description,
is_own,
))
}
"m.room.redaction" => {
let reason = json
.get("content")
.and_then(|c| c.get("reason"))
.and_then(|r| r.as_str())
.map(|s| s.to_string());
Some(TimelineItemKind::Event(EventTimelineData {
event_id,
sender,
sender_display_name: sender_str.to_string(),
sender_avatar_url: None,
timestamp,
content: TimelineContent::Redacted { reason },
reactions: Vec::new(),
is_edited: false,
reply_to: None,
read_receipts: Vec::new(),
send_state: SendState::None,
is_own_message: is_own,
}))
}
"m.sticker" => {
let content = json.get("content")?;
let body = content.get("body")?.as_str()?.to_string();
let url = str_field(content, "url");
Some(TimelineItemKind::Event(EventTimelineData {
event_id,
sender,
sender_display_name: sender_str.to_string(),
sender_avatar_url: None,
timestamp,
content: TimelineContent::Sticker { body, url },
reactions: Vec::new(),
is_edited: false,
reply_to: None,
read_receipts: Vec::new(),
send_state: SendState::None,
is_own_message: is_own,
}))
}
_ => None, }
}
fn make_state_event(
event_id: Option<OwnedEventId>,
sender: OwnedUserId,
sender_str: &str,
timestamp: u64,
description: String,
is_own: bool,
) -> TimelineItemKind {
TimelineItemKind::Event(EventTimelineData {
event_id,
sender,
sender_display_name: sender_str.to_string(),
sender_avatar_url: None,
timestamp,
content: TimelineContent::StateEvent { description },
reactions: Vec::new(),
is_edited: false,
reply_to: None,
read_receipts: Vec::new(),
send_state: SendState::None,
is_own_message: is_own,
})
}
fn convert_message_content(content: &serde_json::Value) -> Option<TimelineContent> {
let msgtype = content.get("msgtype")?.as_str()?;
let body = content.get("body")?.as_str()?.to_string();
match msgtype {
"m.text" => Some(TimelineContent::Text {
body,
formatted_body: str_field(content, "formatted_body"),
}),
"m.image" => {
let info = content.get("info");
Some(TimelineContent::Image {
body,
url: str_field(content, "url"),
thumbnail_url: info
.and_then(|i| i.get("thumbnail_url"))
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
blurhash: info
.and_then(|i| i.get("xyz.amorgan.blurhash"))
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
width: info.and_then(|i| u32_field(i, "w")),
height: info.and_then(|i| u32_field(i, "h")),
})
}
"m.file" => {
let info = content.get("info");
Some(TimelineContent::File {
body,
url: str_field(content, "url"),
size: info.and_then(|i| i.get("size")).and_then(|v| v.as_u64()),
mimetype: info.and_then(|i| str_field(i, "mimetype")),
})
}
"m.audio" => {
let info = content.get("info");
let duration_ms = content
.get("org.matrix.msc1767.audio")
.and_then(|audio| audio.get("duration"))
.and_then(|v| v.as_u64())
.or_else(|| info.and_then(|i| i.get("duration")).and_then(|v| v.as_u64()));
if content.get("org.matrix.msc3245.voice").is_some() {
let waveform = content
.get("org.matrix.msc1767.audio")
.and_then(|audio| audio.get("waveform"))
.and_then(|value| value.as_array())
.map(|samples| {
samples
.iter()
.filter_map(|sample| sample.as_u64())
.map(|sample| sample.min(u16::MAX as u64) as u16)
.collect::<Vec<_>>()
})
.unwrap_or_default();
Some(TimelineContent::VoiceMessage {
body,
url: str_field(content, "url"),
duration_ms,
waveform,
})
} else {
Some(TimelineContent::Audio {
body,
url: str_field(content, "url"),
duration_ms,
})
}
}
"m.video" => {
let info = content.get("info");
Some(TimelineContent::Video {
body,
url: str_field(content, "url"),
thumbnail_url: info
.and_then(|i| i.get("thumbnail_url"))
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
width: info.and_then(|i| u32_field(i, "w")),
height: info.and_then(|i| u32_field(i, "h")),
duration_ms: info
.and_then(|i| i.get("duration"))
.and_then(|v| v.as_u64()),
})
}
"m.emote" => Some(TimelineContent::Emote {
body,
formatted_body: str_field(content, "formatted_body"),
}),
"m.notice" => Some(TimelineContent::Notice {
body,
formatted_body: str_field(content, "formatted_body"),
}),
_ => Some(TimelineContent::Text {
body: format!("[{msgtype}] {body}"),
formatted_body: None,
}),
}
}
fn str_field(obj: &serde_json::Value, key: &str) -> Option<String> {
obj.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn u32_field(obj: &serde_json::Value, key: &str) -> Option<u32> {
obj.get(key).and_then(|v| v.as_u64()).map(|v| v as u32)
}
fn extract_reply_preview(content: &serde_json::Value) -> Option<Box<ReplyPreview>> {
let relates_to = content.get("m.relates_to")?;
let in_reply_to = relates_to.get("m.in_reply_to")?;
let event_id_str = in_reply_to.get("event_id")?.as_str()?;
let event_id: OwnedEventId = event_id_str.try_into().ok()?;
Some(Box::new(ReplyPreview {
event_id,
sender_name: String::new(),
body: String::new(),
}))
}
fn format_membership_event(
sender: &str,
membership: &str,
content: &serde_json::Value,
) -> String {
let display_name = content
.get("displayname")
.and_then(|v| v.as_str())
.unwrap_or(sender);
match membership {
"join" => format!("{display_name} joined the room"),
"leave" => format!("{display_name} left the room"),
"invite" => format!("{sender} invited {display_name}"),
"ban" => format!("{sender} banned {display_name}"),
"knock" => format!("{display_name} requested to join"),
_ => format!("{display_name} changed membership to {membership}"),
}
}
fn format_state_event(
event_type: &str,
sender: &str,
content: &serde_json::Value,
) -> String {
match event_type {
"m.room.name" => {
let name = content
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("(empty)");
format!("{sender} changed the room name to \"{name}\"")
}
"m.room.topic" => {
let topic = content
.get("topic")
.and_then(|v| v.as_str())
.unwrap_or("(empty)");
format!("{sender} changed the topic to \"{topic}\"")
}
"m.room.avatar" => format!("{sender} changed the room avatar"),
"m.room.create" => format!("Room created by {sender}"),
"m.room.power_levels" => format!("{sender} changed the power levels"),
"m.room.join_rules" => {
let rule = content
.get("join_rule")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
format!("{sender} set join rule to {rule}")
}
"m.room.history_visibility" => {
let vis = content
.get("history_visibility")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
format!("{sender} set history visibility to {vis}")
}
_ => format!("{sender} sent a {event_type} event"),
}
}