use std::{fmt, sync::Arc};
use imbl::{vector, Vector};
use matrix_sdk::{deserialized_responses::TimelineEvent, Room};
use ruma::{
assign,
events::{
relation::{InReplyTo, Thread},
room::message::{
MessageType, Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
SyncRoomMessageEvent,
},
AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnyTimelineEvent,
BundledMessageLikeRelations, Mentions,
},
html::RemoveReplyFallback,
OwnedEventId, OwnedUserId, RoomVersionId, UserId,
};
use tracing::error;
use super::TimelineItemContent;
use crate::{
timeline::{
event_item::{EventTimelineItem, Profile, TimelineDetails},
traits::RoomDataProvider,
Error as TimelineError, TimelineItem,
},
DEFAULT_SANITIZER_MODE,
};
#[derive(Clone)]
pub struct Message {
pub(in crate::timeline) msgtype: MessageType,
pub(in crate::timeline) in_reply_to: Option<InReplyToDetails>,
pub(in crate::timeline) thread_root: Option<OwnedEventId>,
pub(in crate::timeline) edited: bool,
pub(in crate::timeline) mentions: Option<Mentions>,
}
impl Message {
pub(in crate::timeline) fn from_event(
c: RoomMessageEventContent,
relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
timeline_items: &Vector<Arc<TimelineItem>>,
) -> Self {
let edited = relations.has_replacement();
let edit = relations.replace.and_then(|r| match *r {
AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Original(ev)) => match ev
.content
.relates_to
{
Some(Relation::Replacement(re)) => Some(re),
_ => {
error!("got m.room.message event with an edit without a valid m.replace relation");
None
}
},
AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Redacted(_)) => None,
_ => {
error!("got m.room.message event with an edit of a different event type");
None
}
});
let mut thread_root = None;
let in_reply_to = c.relates_to.and_then(|relation| match relation {
Relation::Reply { in_reply_to } => {
Some(InReplyToDetails::new(in_reply_to.event_id, timeline_items))
}
Relation::Thread(thread) => {
thread_root = Some(thread.event_id);
thread
.in_reply_to
.map(|in_reply_to| InReplyToDetails::new(in_reply_to.event_id, timeline_items))
}
_ => None,
});
let (msgtype, mentions) = match edit {
Some(mut e) => {
e.new_content.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No);
(e.new_content.msgtype, e.new_content.mentions)
}
None => {
let remove_reply_fallback = if in_reply_to.is_some() {
RemoveReplyFallback::Yes
} else {
RemoveReplyFallback::No
};
let mut msgtype = c.msgtype;
msgtype.sanitize(DEFAULT_SANITIZER_MODE, remove_reply_fallback);
(msgtype, c.mentions)
}
};
Self { msgtype, in_reply_to, thread_root, edited, mentions }
}
pub fn msgtype(&self) -> &MessageType {
&self.msgtype
}
pub fn body(&self) -> &str {
self.msgtype.body()
}
pub fn in_reply_to(&self) -> Option<&InReplyToDetails> {
self.in_reply_to.as_ref()
}
pub fn is_threaded(&self) -> bool {
self.thread_root.is_some()
}
pub fn is_edited(&self) -> bool {
self.edited
}
pub fn mentions(&self) -> Option<&Mentions> {
self.mentions.as_ref()
}
pub(in crate::timeline) fn to_content(&self) -> RoomMessageEventContent {
let relates_to = make_relates_to(
self.thread_root.clone(),
self.in_reply_to.as_ref().map(|details| details.event_id.clone()),
);
assign!(RoomMessageEventContent::new(self.msgtype.clone()), { relates_to })
}
pub(in crate::timeline) fn with_in_reply_to(&self, in_reply_to: InReplyToDetails) -> Self {
Self { in_reply_to: Some(in_reply_to), ..self.clone() }
}
}
impl From<Message> for RoomMessageEventContent {
fn from(msg: Message) -> Self {
let relates_to =
make_relates_to(msg.thread_root, msg.in_reply_to.map(|details| details.event_id));
assign!(Self::new(msg.msgtype), { relates_to })
}
}
fn make_relates_to(
thread_root: Option<OwnedEventId>,
in_reply_to: Option<OwnedEventId>,
) -> Option<Relation<RoomMessageEventContentWithoutRelation>> {
match (thread_root, in_reply_to) {
(Some(thread_root), Some(in_reply_to)) => {
Some(Relation::Thread(Thread::plain(thread_root, in_reply_to)))
}
(Some(thread_root), None) => Some(Relation::Thread(Thread::without_fallback(thread_root))),
(None, Some(in_reply_to)) => {
Some(Relation::Reply { in_reply_to: InReplyTo::new(in_reply_to) })
}
(None, None) => None,
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { msgtype: _, in_reply_to, thread_root, edited, mentions: _ } = self;
f.debug_struct("Message")
.field("in_reply_to", in_reply_to)
.field("thread_root", thread_root)
.field("edited", edited)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct InReplyToDetails {
pub event_id: OwnedEventId,
pub event: TimelineDetails<Box<RepliedToEvent>>,
}
impl InReplyToDetails {
pub fn new(
event_id: OwnedEventId,
timeline_items: &Vector<Arc<TimelineItem>>,
) -> InReplyToDetails {
let event = timeline_items
.iter()
.filter_map(|it| it.as_event())
.find(|it| it.event_id() == Some(&*event_id))
.map(|item| Box::new(RepliedToEvent::from_timeline_item(item)));
InReplyToDetails { event_id, event: TimelineDetails::from_initial_value(event) }
}
}
#[derive(Clone, Debug)]
pub struct RepliedToEvent {
pub(in crate::timeline) content: TimelineItemContent,
pub(in crate::timeline) sender: OwnedUserId,
pub(in crate::timeline) sender_profile: TimelineDetails<Profile>,
}
impl RepliedToEvent {
pub fn content(&self) -> &TimelineItemContent {
&self.content
}
pub fn sender(&self) -> &UserId {
&self.sender
}
pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
&self.sender_profile
}
pub fn from_timeline_item(timeline_item: &EventTimelineItem) -> Self {
Self {
content: timeline_item.content.clone(),
sender: timeline_item.sender.clone(),
sender_profile: timeline_item.sender_profile.clone(),
}
}
pub(in crate::timeline) fn redact(&self, room_version: &RoomVersionId) -> Self {
Self {
content: self.content.redact(room_version),
sender: self.sender.clone(),
sender_profile: self.sender_profile.clone(),
}
}
pub async fn try_from_timeline_event_for_room(
timeline_event: TimelineEvent,
room_data_provider: &Room,
) -> Result<Self, TimelineError> {
Self::try_from_timeline_event(timeline_event, room_data_provider).await
}
pub(in crate::timeline) async fn try_from_timeline_event<P: RoomDataProvider>(
timeline_event: TimelineEvent,
room_data_provider: &P,
) -> Result<Self, TimelineError> {
let event = match timeline_event.event.deserialize() {
Ok(AnyTimelineEvent::MessageLike(event)) => event,
_ => {
return Err(TimelineError::UnsupportedEvent);
}
};
let Some(AnyMessageLikeEventContent::RoomMessage(c)) = event.original_content() else {
return Err(TimelineError::UnsupportedEvent);
};
let content =
TimelineItemContent::Message(Message::from_event(c, event.relations(), &vector![]));
let sender = event.sender().to_owned();
let sender_profile = TimelineDetails::from_initial_value(
room_data_provider.profile_from_user_id(&sender).await,
);
Ok(Self { content, sender, sender_profile })
}
}