use std::{
ops::{Deref, DerefMut},
sync::Arc,
};
use as_variant::as_variant;
use indexmap::IndexMap;
use matrix_sdk::{
deserialized_responses::{EncryptionInfo, ShieldState},
send_queue::{SendHandle, SendReactionHandle},
Client, Error,
};
use matrix_sdk_base::{
deserialized_responses::{ShieldStateCode, SENT_IN_CLEAR},
latest_event::LatestEvent,
};
use once_cell::sync::Lazy;
use ruma::{
events::{receipt::Receipt, room::message::MessageType, AnySyncTimelineEvent},
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
};
use tracing::warn;
mod content;
mod local;
mod remote;
pub(super) use self::{
content::{
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
ResponseData,
},
local::LocalEventTimelineItem,
remote::{RemoteEventOrigin, RemoteEventTimelineItem},
};
pub use self::{
content::{
AnyOtherFullStateEventContent, EncryptedMessage, InReplyToDetails, MemberProfileChange,
MembershipChange, Message, OtherState, PollResult, PollState, RepliedToEvent,
RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineItemContent,
},
local::EventSendState,
};
use super::{RepliedToInfo, ReplyContent, UnsupportedReplyItem};
#[derive(Clone, Debug)]
pub struct EventTimelineItem {
pub(super) sender: OwnedUserId,
pub(super) sender_profile: TimelineDetails<Profile>,
pub(super) reactions: ReactionsByKeyBySender,
pub(super) timestamp: MilliSecondsSinceUnixEpoch,
pub(super) content: TimelineItemContent,
pub(super) kind: EventTimelineItemKind,
pub(super) is_room_encrypted: Option<bool>,
}
#[derive(Clone, Debug)]
pub(super) enum EventTimelineItemKind {
Local(LocalEventTimelineItem),
Remote(RemoteEventTimelineItem),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum TimelineEventItemId {
TransactionId(OwnedTransactionId),
EventId(OwnedEventId),
}
pub(crate) enum TimelineItemHandle<'a> {
Remote(&'a EventId),
Local(&'a SendHandle),
}
impl EventTimelineItem {
pub(super) fn new(
sender: OwnedUserId,
sender_profile: TimelineDetails<Profile>,
timestamp: MilliSecondsSinceUnixEpoch,
content: TimelineItemContent,
kind: EventTimelineItemKind,
reactions: ReactionsByKeyBySender,
is_room_encrypted: bool,
) -> Self {
let is_room_encrypted = Some(is_room_encrypted);
Self { sender, sender_profile, timestamp, content, reactions, kind, is_room_encrypted }
}
pub async fn from_latest_event(
client: Client,
room_id: &RoomId,
latest_event: LatestEvent,
) -> Option<EventTimelineItem> {
use super::traits::RoomDataProvider;
let raw_sync_event = latest_event.event().raw().clone();
let encryption_info = latest_event.event().encryption_info().cloned();
let Ok(event) = raw_sync_event.deserialize_as::<AnySyncTimelineEvent>() else {
warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!");
return None;
};
let timestamp = event.origin_server_ts();
let sender = event.sender().to_owned();
let event_id = event.event_id().to_owned();
let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false);
let power_levels = if let Some(room) = client.get_room(room_id) {
room.power_levels().await.ok()
} else {
None
};
let room_power_levels_info = client.user_id().zip(power_levels.as_ref());
let content =
TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?;
let reactions = ReactionsByKeyBySender::default();
let read_receipts = IndexMap::new();
let is_highlighted = false;
let latest_edit_json = None;
let origin = RemoteEventOrigin::Sync;
let kind = RemoteEventTimelineItem {
event_id,
transaction_id: None,
read_receipts,
is_own,
is_highlighted,
encryption_info,
original_json: Some(raw_sync_event),
latest_edit_json,
origin,
}
.into();
let room = client.get_room(room_id);
let sender_profile = if let Some(room) = room {
let mut profile = room.profile_from_latest_event(&latest_event);
if profile.is_none() {
profile = room.profile_from_user_id(&sender).await;
}
profile.map(TimelineDetails::Ready).unwrap_or(TimelineDetails::Unavailable)
} else {
TimelineDetails::Unavailable
};
Some(Self {
sender,
sender_profile,
timestamp,
content,
kind,
reactions,
is_room_encrypted: None,
})
}
pub fn is_local_echo(&self) -> bool {
matches!(self.kind, EventTimelineItemKind::Local(_))
}
pub fn is_remote_event(&self) -> bool {
matches!(self.kind, EventTimelineItemKind::Remote(_))
}
pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
}
pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
}
pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
}
pub fn send_state(&self) -> Option<&EventSendState> {
as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
}
pub fn identifier(&self) -> TimelineEventItemId {
match &self.kind {
EventTimelineItemKind::Local(local) => local.identifier(),
EventTimelineItemKind::Remote(remote) => {
TimelineEventItemId::EventId(remote.event_id.clone())
}
}
}
pub fn transaction_id(&self) -> Option<&TransactionId> {
as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
}
pub fn event_id(&self) -> Option<&EventId> {
match &self.kind {
EventTimelineItemKind::Local(local_event) => local_event.event_id(),
EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
}
}
pub fn sender(&self) -> &UserId {
&self.sender
}
pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
&self.sender_profile
}
pub fn content(&self) -> &TimelineItemContent {
&self.content
}
pub fn reactions(&self) -> &ReactionsByKeyBySender {
&self.reactions
}
pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
static EMPTY_RECEIPTS: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::new(Default::default);
match &self.kind {
EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
}
}
pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
self.timestamp
}
pub fn is_own(&self) -> bool {
match &self.kind {
EventTimelineItemKind::Local(_) => true,
EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
}
}
pub fn is_editable(&self) -> bool {
if !self.is_own() {
return false;
}
match self.content() {
TimelineItemContent::Message(message) => {
matches!(message.msgtype(), MessageType::Text(_) | MessageType::Emote(_))
}
TimelineItemContent::Poll(poll) => {
poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
}
_ => {
false
}
}
}
pub fn is_highlighted(&self) -> bool {
match &self.kind {
EventTimelineItemKind::Local(_) => false,
EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
}
}
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
match &self.kind {
EventTimelineItemKind::Local(_) => None,
EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_ref(),
}
}
pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
if self.is_room_encrypted != Some(true) || self.is_local_echo() {
return None;
}
if let TimelineItemContent::UnableToDecrypt(_) = self.content() {
return None;
}
match self.encryption_info() {
Some(info) => {
if strict {
Some(info.verification_state.to_shield_state_strict())
} else {
Some(info.verification_state.to_shield_state_lax())
}
}
None => Some(ShieldState::Red {
code: ShieldStateCode::SentInClear,
message: SENT_IN_CLEAR,
}),
}
}
pub fn can_be_replied_to(&self) -> bool {
if self.event_id().is_none() {
false
} else if let TimelineItemContent::Message(_) = self.content() {
true
} else {
self.latest_json().is_some()
}
}
pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
match &self.kind {
EventTimelineItemKind::Local(_) => None,
EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
}
}
pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
match &self.kind {
EventTimelineItemKind::Local(_) => None,
EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
}
}
pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
self.latest_edit_json().or_else(|| self.original_json())
}
pub fn origin(&self) -> Option<EventItemOrigin> {
match &self.kind {
EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
_ => None,
},
}
}
pub(super) fn set_content(&mut self, content: TimelineItemContent) {
self.content = content;
}
pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
Self { kind: kind.into(), ..self.clone() }
}
pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
Self { reactions, ..self.clone() }
}
pub(super) fn with_content(
&self,
new_content: TimelineItemContent,
edit_json: Option<Raw<AnySyncTimelineEvent>>,
) -> Self {
let mut new = self.clone();
new.content = new_content;
if let EventTimelineItemKind::Remote(r) = &mut new.kind {
r.latest_edit_json = edit_json;
}
new
}
pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
Self { sender_profile, ..self.clone() }
}
pub(super) fn redact(&self, room_version: &RoomVersionId) -> Self {
let content = self.content.redact(room_version);
let kind = match &self.kind {
EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
};
Self {
sender: self.sender.clone(),
sender_profile: self.sender_profile.clone(),
timestamp: self.timestamp,
content,
kind,
is_room_encrypted: self.is_room_encrypted,
reactions: ReactionsByKeyBySender::default(),
}
}
pub fn replied_to_info(&self) -> Result<RepliedToInfo, UnsupportedReplyItem> {
let reply_content = match self.content() {
TimelineItemContent::Message(msg) => ReplyContent::Message(msg.to_owned()),
_ => {
let Some(raw_event) = self.latest_json() else {
return Err(UnsupportedReplyItem::MissingJson);
};
ReplyContent::Raw(raw_event.clone())
}
};
let Some(event_id) = self.event_id() else {
return Err(UnsupportedReplyItem::MissingEventId);
};
Ok(RepliedToInfo {
event_id: event_id.to_owned(),
sender: self.sender().to_owned(),
timestamp: self.timestamp(),
content: reply_content,
})
}
pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
match &self.kind {
EventTimelineItemKind::Local(local) => {
if let Some(event_id) = local.event_id() {
TimelineItemHandle::Remote(event_id)
} else {
TimelineItemHandle::Local(
local.send_handle.as_ref().expect("Unexpected missing send_handle"),
)
}
}
EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
}
}
}
impl From<LocalEventTimelineItem> for EventTimelineItemKind {
fn from(value: LocalEventTimelineItem) -> Self {
EventTimelineItemKind::Local(value)
}
}
impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
fn from(value: RemoteEventTimelineItem) -> Self {
EventTimelineItemKind::Remote(value)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Profile {
pub display_name: Option<String>,
pub display_name_ambiguous: bool,
pub avatar_url: Option<OwnedMxcUri>,
}
#[derive(Clone, Debug)]
pub enum TimelineDetails<T> {
Unavailable,
Pending,
Ready(T),
Error(Arc<Error>),
}
impl<T> TimelineDetails<T> {
pub(crate) fn from_initial_value(value: Option<T>) -> Self {
match value {
Some(v) => Self::Ready(v),
None => Self::Unavailable,
}
}
pub(crate) fn is_unavailable(&self) -> bool {
matches!(self, Self::Unavailable)
}
pub fn is_ready(&self) -> bool {
matches!(self, Self::Ready(_))
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum EventItemOrigin {
Local,
Sync,
Pagination,
}
#[derive(Clone, Debug)]
pub enum ReactionStatus {
LocalToLocal(Option<SendReactionHandle>),
LocalToRemote(Option<SendHandle>),
RemoteToRemote(OwnedEventId),
}
#[derive(Clone, Debug)]
pub struct ReactionInfo {
pub timestamp: MilliSecondsSinceUnixEpoch,
pub status: ReactionStatus,
}
#[derive(Debug, Clone, Default)]
pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
impl Deref for ReactionsByKeyBySender {
type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ReactionsByKeyBySender {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl ReactionsByKeyBySender {
pub(crate) fn remove_reaction(
&mut self,
sender: &UserId,
annotation: &str,
) -> Option<ReactionInfo> {
if let Some(by_user) = self.0.get_mut(annotation) {
if let Some(info) = by_user.swap_remove(sender) {
if by_user.is_empty() {
self.0.swap_remove(annotation);
}
return Some(info);
}
}
None
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use matrix_sdk::test_utils::{events::EventFactory, logged_in_client};
use matrix_sdk_base::{
deserialized_responses::SyncTimelineEvent, latest_event::LatestEvent, sliding_sync::http,
MinimalStateEvent, OriginalMinimalStateEvent,
};
use matrix_sdk_test::{async_test, sync_state_event, sync_timeline_event};
use ruma::{
event_id,
events::{
room::{
member::RoomMemberEventContent,
message::{MessageFormat, MessageType},
},
AnySyncStateEvent, AnySyncTimelineEvent, BundledMessageLikeRelations,
},
room_id,
serde::Raw,
user_id, RoomId, UInt, UserId,
};
use super::{EventTimelineItem, Profile};
use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent};
#[async_test]
async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
let room_id = room_id!("!q:x.uk");
let user_id = user_id!("@t:o.uk");
let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
let client = logged_in_client(None).await;
let timeline_item =
EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
.await
.unwrap();
assert_eq!(timeline_item.sender, user_id);
assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
assert_eq!(txt.body, "**My M**");
let formatted = txt.formatted.as_ref().unwrap();
assert_eq!(formatted.format, MessageFormat::Html);
assert_eq!(formatted.body, "<b>My M</b>");
} else {
panic!("Unexpected message type");
}
}
#[async_test]
async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() {
let room_id = room_id!("!q:x.uk");
let user_id = user_id!("@t:o.uk");
let raw_event = member_event_as_state_event(
room_id,
user_id,
"knock",
"Alice Margatroid",
"mxc://e.org/SEs",
);
let client = logged_in_client(None).await;
let power_level_event = sync_state_event!({
"type": "m.room.power_levels",
"content": {},
"event_id": "$143278582443PhrSn:example.org",
"origin_server_ts": 143273581,
"room_id": room_id,
"sender": user_id,
"state_key": "",
"unsigned": {
"age": 1234
}
});
let mut room = http::response::Room::new();
room.required_state.push(power_level_event);
let response = response_with_room(room_id, room);
client.process_sliding_sync_test_helper(&response).await.unwrap();
let event = SyncTimelineEvent::new(raw_event.cast());
let timeline_item =
EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
.await
.unwrap();
assert_eq!(timeline_item.sender, user_id);
assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap());
if let TimelineItemContent::MembershipChange(change) = timeline_item.content {
assert_eq!(change.user_id, user_id);
assert_matches!(change.change, Some(MembershipChange::Knocked));
} else {
panic!("Unexpected state event type");
}
}
#[async_test]
async fn test_latest_message_includes_bundled_edit() {
let room_id = room_id!("!q:x.uk");
let user_id = user_id!("@t:o.uk");
let f = EventFactory::new();
let original_event_id = event_id!("$original");
let mut relations = BundledMessageLikeRelations::new();
relations.replace = Some(Box::new(
f.text_html(" * Updated!", " * <b>Updated!</b>")
.edit(
original_event_id,
MessageType::text_html("Updated!", "<b>Updated!</b>").into(),
)
.event_id(event_id!("$edit"))
.sender(user_id)
.into_raw_sync(),
));
let event = f
.text_html("**My M**", "<b>My M</b>")
.sender(user_id)
.event_id(original_event_id)
.bundled_relations(relations)
.server_ts(42)
.into_sync();
let client = logged_in_client(None).await;
let timeline_item =
EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
.await
.unwrap();
assert_eq!(timeline_item.sender, user_id);
assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
assert_eq!(timeline_item.timestamp.0, UInt::new(42).unwrap());
if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
assert_eq!(txt.body, "Updated!");
let formatted = txt.formatted.as_ref().unwrap();
assert_eq!(formatted.format, MessageFormat::Html);
assert_eq!(formatted.body, "<b>Updated!</b>");
} else {
panic!("Unexpected message type");
}
}
#[async_test]
async fn test_latest_poll_includes_bundled_edit() {
let room_id = room_id!("!q:x.uk");
let user_id = user_id!("@t:o.uk");
let f = EventFactory::new();
let original_event_id = event_id!("$original");
let mut relations = BundledMessageLikeRelations::new();
relations.replace = Some(Box::new(
f.poll_edit(
original_event_id,
"It's one banana, Michael, how much could it cost?",
vec!["1 dollar", "10 dollars", "100 dollars"],
)
.event_id(event_id!("$edit"))
.sender(user_id)
.into_raw_sync(),
));
let event = f
.poll_start(
"It's one avocado, Michael, how much could it cost? 10 dollars?",
"It's one avocado, Michael, how much could it cost?",
vec!["1 dollar", "10 dollars", "100 dollars"],
)
.event_id(original_event_id)
.bundled_relations(relations)
.sender(user_id)
.into_sync();
let client = logged_in_client(None).await;
let timeline_item =
EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
.await
.unwrap();
assert_eq!(timeline_item.sender, user_id);
let poll = timeline_item.content().as_poll().unwrap();
assert!(poll.has_been_edited);
assert_eq!(
poll.start_event_content.poll_start.question.text,
"It's one banana, Michael, how much could it cost?"
);
}
#[async_test]
async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage(
) {
use ruma::owned_mxc_uri;
let room_id = room_id!("!q:x.uk");
let user_id = user_id!("@t:o.uk");
let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
let client = logged_in_client(None).await;
let mut room = http::response::Room::new();
room.required_state.push(member_event_as_state_event(
room_id,
user_id,
"join",
"Alice Margatroid",
"mxc://e.org/SEs",
));
let response = response_with_room(room_id, room);
client.process_sliding_sync_test_helper(&response).await.unwrap();
let timeline_item =
EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
.await
.unwrap();
assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
assert_eq!(
profile,
Profile {
display_name: Some("Alice Margatroid".to_owned()),
display_name_ambiguous: false,
avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
}
);
}
#[async_test]
async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache(
) {
use ruma::owned_mxc_uri;
let room_id = room_id!("!q:x.uk");
let user_id = user_id!("@t:o.uk");
let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
let client = logged_in_client(None).await;
let member_event = MinimalStateEvent::Original(
member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")
.deserialize_as::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
.unwrap(),
);
let room = http::response::Room::new();
let response = response_with_room(room_id, room);
client.process_sliding_sync_test_helper(&response).await.unwrap();
let timeline_item = EventTimelineItem::from_latest_event(
client,
room_id,
LatestEvent::new_with_sender_details(event, Some(member_event), None),
)
.await
.unwrap();
assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
assert_eq!(
profile,
Profile {
display_name: Some("Alice Margatroid".to_owned()),
display_name_ambiguous: false,
avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
}
);
}
fn member_event(
room_id: &RoomId,
user_id: &UserId,
display_name: &str,
avatar_url: &str,
) -> Raw<AnySyncTimelineEvent> {
sync_timeline_event!({
"type": "m.room.member",
"content": {
"avatar_url": avatar_url,
"displayname": display_name,
"membership": "join",
"reason": ""
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 143273583,
"room_id": room_id,
"sender": "@example:example.org",
"state_key": user_id,
"type": "m.room.member",
"unsigned": {
"age": 1234
}
})
}
fn member_event_as_state_event(
room_id: &RoomId,
user_id: &UserId,
membership: &str,
display_name: &str,
avatar_url: &str,
) -> Raw<AnySyncStateEvent> {
sync_state_event!({
"type": "m.room.member",
"content": {
"avatar_url": avatar_url,
"displayname": display_name,
"membership": membership,
"reason": ""
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 143273583,
"room_id": room_id,
"sender": user_id,
"state_key": user_id,
"unsigned": {
"age": 1234
}
})
}
fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
let mut response = http::Response::new("6".to_owned());
response.rooms.insert(room_id.to_owned(), room);
response
}
fn message_event(
room_id: &RoomId,
user_id: &UserId,
body: &str,
formatted_body: &str,
ts: u64,
) -> SyncTimelineEvent {
SyncTimelineEvent::new(sync_timeline_event!({
"event_id": "$eventid6",
"sender": user_id,
"origin_server_ts": ts,
"type": "m.room.message",
"room_id": room_id.to_string(),
"content": {
"body": body,
"format": "org.matrix.custom.html",
"formatted_body": formatted_body,
"msgtype": "m.text"
},
}))
}
}