You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

709 lines
27 KiB

use gtk::{glib, glib::clone, glib::DateTime, prelude::*, subclass::prelude::*};
use log::warn;
use matrix_sdk::{
deserialized_responses::SyncRoomEvent,
media::MediaEventContent,
ruma::{
events::{
room::message::Relation,
room::{message::MessageType, redaction::RoomRedactionEventContent},
AnyMessageEventContent, AnyRedactedSyncMessageEvent, AnyRedactedSyncStateEvent,
AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent, Unsigned,
},
identifiers::{EventId, UserId},
MilliSecondsSinceUnixEpoch,
},
};
use crate::{
session::{room::Member, Room},
spawn_tokio,
utils::{filename_for_mime, media_type_uid},
};
#[derive(Clone, Debug, glib::GBoxed)]
#[gboxed(type_name = "BoxedSyncRoomEvent")]
pub struct BoxedSyncRoomEvent(SyncRoomEvent);
mod imp {
use super::*;
use glib::object::WeakRef;
use glib::SignalHandlerId;
use once_cell::{sync::Lazy, unsync::OnceCell};
use std::cell::{Cell, RefCell};
#[derive(Debug, Default)]
pub struct Event {
/// The deserialized matrix event
pub event: RefCell<Option<AnySyncRoomEvent>>,
/// The SDK event containing encryption information and the serialized event as `Raw`
pub pure_event: RefCell<Option<SyncRoomEvent>>,
/// Events that replace this one, in the order they arrive.
pub replacing_events: RefCell<Vec<super::Event>>,
pub source_changed_handler: RefCell<Option<SignalHandlerId>>,
pub show_header: Cell<bool>,
pub room: OnceCell<WeakRef<Room>>,
}
#[glib::object_subclass]
impl ObjectSubclass for Event {
const NAME: &'static str = "RoomEvent";
type Type = super::Event;
type ParentType = glib::Object;
}
impl ObjectImpl for Event {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpec::new_boxed(
"event",
"event",
"The matrix event of this Event",
BoxedSyncRoomEvent::static_type(),
glib::ParamFlags::WRITABLE,
),
glib::ParamSpec::new_string(
"source",
"Source",
"The source (JSON) of this Event",
None,
glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_boolean(
"show-header",
"Show Header",
"Whether this event should show a header. This does nothing if this event doesn’t have a header. ",
false,
glib::ParamFlags::READWRITE,
),
glib::ParamSpec::new_boolean(
"can-hide-header",
"Can hide header",
"Whether this event is allowed to hide it's header or not.",
false,
glib::ParamFlags::READABLE,
),
glib::ParamSpec::new_object(
"sender",
"Sender",
"The sender of this matrix event",
Member::static_type(),
glib::ParamFlags::READABLE,
),
glib::ParamSpec::new_object(
"room",
"Room",
"The room containing this event",
Room::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
glib::ParamSpec::new_string(
"time",
"Time",
"The locally formatted time of this matrix event",
None,
glib::ParamFlags::READABLE,
),
glib::ParamSpec::new_boolean(
"can-view-media",
"Can View Media",
"Whether this is a media event that can be viewed",
false,
glib::ParamFlags::READABLE,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"event" => {
let event = value.get::<BoxedSyncRoomEvent>().unwrap();
obj.set_matrix_pure_event(event.0);
}
"show-header" => {
let show_header = value.get().unwrap();
let _ = obj.set_show_header(show_header);
}
"room" => {
self.room
.set(value.get::<Room>().unwrap().downgrade())
.unwrap();
}
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"source" => obj.source().to_value(),
"sender" => obj.sender().to_value(),
"room" => obj.room().to_value(),
"show-header" => obj.show_header().to_value(),
"can-hide-header" => obj.can_hide_header().to_value(),
"time" => obj.time().to_value(),
"can-view-media" => obj.can_view_media().to_value(),
_ => unimplemented!(),
}
}
}
}
glib::wrapper! {
/// GObject representation of a Matrix room event.
pub struct Event(ObjectSubclass<imp::Event>);
}
// TODO:
// - [ ] implement operations for events: forward, reply, delete...
impl Event {
pub fn new(event: SyncRoomEvent, room: &Room) -> Self {
let event = BoxedSyncRoomEvent(event);
glib::Object::new(&[("event", &event), ("room", room)]).expect("Failed to create Event")
}
pub fn sender(&self) -> Member {
self.room().members().member_by_id(&self.matrix_sender())
}
pub fn room(&self) -> Room {
let priv_ = imp::Event::from_instance(self);
priv_.room.get().unwrap().upgrade().unwrap()
}
/// Get the matrix event
///
/// If the `SyncRoomEvent` couldn't be deserialized this is `None`
pub fn matrix_event(&self) -> Option<AnySyncRoomEvent> {
let priv_ = imp::Event::from_instance(self);
priv_.event.borrow().clone()
}
pub fn matrix_pure_event(&self) -> SyncRoomEvent {
let priv_ = imp::Event::from_instance(self);
priv_.pure_event.borrow().clone().unwrap()
}
pub fn set_matrix_pure_event(&self, event: SyncRoomEvent) {
let priv_ = imp::Event::from_instance(self);
if let Ok(deserialized) = event.event.deserialize() {
priv_.event.replace(Some(deserialized));
} else {
warn!("Failed to deserialize event: {:?}", event);
}
priv_.pure_event.replace(Some(event));
self.notify("event");
self.notify("can-view-media");
}
pub fn matrix_sender(&self) -> UserId {
let priv_ = imp::Event::from_instance(self);
if let Some(event) = priv_.event.borrow().as_ref() {
event.sender().to_owned()
} else {
priv_
.pure_event
.borrow()
.as_ref()
.unwrap()
.event
.get_field::<UserId>("sender")
.unwrap()
.unwrap()
}
}
pub fn matrix_event_id(&self) -> EventId {
let priv_ = imp::Event::from_instance(self);
if let Some(event) = priv_.event.borrow().as_ref() {
event.event_id().to_owned()
} else {
priv_
.pure_event
.borrow()
.as_ref()
.unwrap()
.event
.get_field::<EventId>("event_id")
.unwrap()
.unwrap()
}
}
pub fn matrix_transaction_id(&self) -> Option<String> {
let priv_ = imp::Event::from_instance(self);
priv_
.pure_event
.borrow()
.as_ref()
.unwrap()
.event
.get_field::<Unsigned>("unsigned")
.ok()
.and_then(|opt| opt)
.and_then(|unsigned| unsigned.transaction_id)
}
/// The pretty-formatted JSON of this matrix event.
pub fn original_source(&self) -> String {
let priv_ = imp::Event::from_instance(self);
// We have to convert it to a Value, because a RawValue cannot be pretty-printed.
let json: serde_json::Value = serde_json::from_str(
priv_
.pure_event
.borrow()
.as_ref()
.unwrap()
.event
.json()
.get(),
)
.unwrap();
serde_json::to_string_pretty(&json).unwrap()
}
/// The pretty-formatted JSON used for this matrix event.
///
/// If this matrix event has been replaced, returns the replacing `Event`'s source.
pub fn source(&self) -> String {
self.replacement()
.map(|replacement| replacement.source())
.unwrap_or(self.original_source())
}
pub fn timestamp(&self) -> DateTime {
let priv_ = imp::Event::from_instance(self);
let ts = if let Some(event) = priv_.event.borrow().as_ref() {
event.origin_server_ts().as_secs()
} else {
priv_
.pure_event
.borrow()
.as_ref()
.unwrap()
.event
.get_field::<MilliSecondsSinceUnixEpoch>("origin_server_ts")
.unwrap()
.unwrap()
.as_secs()
};
DateTime::from_unix_utc(ts.into())
.and_then(|t| t.to_local())
.unwrap()
}
pub fn time(&self) -> String {
let datetime = self.timestamp();
// FIXME Is there a cleaner way to do that?
let local_time = datetime.format("%X").unwrap().as_str().to_ascii_lowercase();
if local_time.ends_with("am") || local_time.ends_with("pm") {
// Use 12h time format (AM/PM)
datetime.format("%l∶%M %p").unwrap().to_string()
} else {
// Use 24 time format
datetime.format("%R").unwrap().to_string()
}
}
/// Find the related event if any
pub fn related_matrix_event(&self) -> Option<EventId> {
let priv_ = imp::Event::from_instance(self);
match priv_.event.borrow().as_ref()? {
AnySyncRoomEvent::Message(ref message) => match message {
AnySyncMessageEvent::RoomRedaction(event) => Some(event.redacts.clone()),
_ => match message.content() {
AnyMessageEventContent::Reaction(event) => Some(event.relates_to.event_id),
AnyMessageEventContent::RoomMessage(event) => match event.relates_to {
Some(relates_to) => match relates_to {
// TODO: Figure out Relation::Annotation(), Relation::Reference() but they are pre-specs for now
// See: https://github.com/uhoreg/matrix-doc/blob/aggregations-reactions/proposals/2677-reactions.md
Relation::Reply { in_reply_to } => Some(in_reply_to.event_id),
Relation::Replacement(replacement) => Some(replacement.event_id),
_ => None,
},
_ => None,
},
// TODO: RoomEncrypted needs https://github.com/ruma/ruma/issues/502
_ => None,
},
},
_ => None,
}
}
/// Whether this event is hidden from the user or displayed in the room history.
pub fn is_hidden_event(&self) -> bool {
let priv_ = imp::Event::from_instance(self);
if self.related_matrix_event().is_some() {
if let Some(AnySyncRoomEvent::Message(message)) = priv_.event.borrow().as_ref() {
if let AnyMessageEventContent::RoomMessage(content) = message.content() {
if let Some(Relation::Reply { in_reply_to: _ }) = content.relates_to {
return false;
}
}
}
return true;
}
let event = priv_.event.borrow();
// List of all events to be hidden.
match event.as_ref() {
Some(AnySyncRoomEvent::Message(message)) => matches!(
message,
AnySyncMessageEvent::CallAnswer(_)
| AnySyncMessageEvent::CallInvite(_)
| AnySyncMessageEvent::CallHangup(_)
| AnySyncMessageEvent::CallCandidates(_)
| AnySyncMessageEvent::KeyVerificationReady(_)
| AnySyncMessageEvent::KeyVerificationStart(_)
| AnySyncMessageEvent::KeyVerificationCancel(_)
| AnySyncMessageEvent::KeyVerificationAccept(_)
| AnySyncMessageEvent::KeyVerificationKey(_)
| AnySyncMessageEvent::KeyVerificationMac(_)
| AnySyncMessageEvent::KeyVerificationDone(_)
| AnySyncMessageEvent::RoomMessageFeedback(_)
| AnySyncMessageEvent::RoomRedaction(_)
),
Some(AnySyncRoomEvent::State(state)) => matches!(
state,
AnySyncStateEvent::PolicyRuleRoom(_)
| AnySyncStateEvent::PolicyRuleServer(_)
| AnySyncStateEvent::PolicyRuleUser(_)
| AnySyncStateEvent::RoomAliases(_)
| AnySyncStateEvent::RoomAvatar(_)
| AnySyncStateEvent::RoomCanonicalAlias(_)
| AnySyncStateEvent::RoomEncryption(_)
| AnySyncStateEvent::RoomJoinRules(_)
| AnySyncStateEvent::RoomName(_)
| AnySyncStateEvent::RoomPinnedEvents(_)
| AnySyncStateEvent::RoomPowerLevels(_)
| AnySyncStateEvent::RoomServerAcl(_)
| AnySyncStateEvent::RoomTopic(_)
),
Some(AnySyncRoomEvent::RedactedMessage(message)) => matches!(
message,
AnyRedactedSyncMessageEvent::CallAnswer(_)
| AnyRedactedSyncMessageEvent::CallInvite(_)
| AnyRedactedSyncMessageEvent::CallHangup(_)
| AnyRedactedSyncMessageEvent::CallCandidates(_)
| AnyRedactedSyncMessageEvent::KeyVerificationReady(_)
| AnyRedactedSyncMessageEvent::KeyVerificationStart(_)
| AnyRedactedSyncMessageEvent::KeyVerificationCancel(_)
| AnyRedactedSyncMessageEvent::KeyVerificationAccept(_)
| AnyRedactedSyncMessageEvent::KeyVerificationKey(_)
| AnyRedactedSyncMessageEvent::KeyVerificationMac(_)
| AnyRedactedSyncMessageEvent::KeyVerificationDone(_)
| AnyRedactedSyncMessageEvent::RoomMessageFeedback(_)
| AnyRedactedSyncMessageEvent::RoomRedaction(_)
| AnyRedactedSyncMessageEvent::Sticker(_)
),
Some(AnySyncRoomEvent::RedactedState(state)) => matches!(
state,
AnyRedactedSyncStateEvent::PolicyRuleRoom(_)
| AnyRedactedSyncStateEvent::PolicyRuleServer(_)
| AnyRedactedSyncStateEvent::PolicyRuleUser(_)
| AnyRedactedSyncStateEvent::RoomAliases(_)
| AnyRedactedSyncStateEvent::RoomAvatar(_)
| AnyRedactedSyncStateEvent::RoomCanonicalAlias(_)
| AnyRedactedSyncStateEvent::RoomEncryption(_)
| AnyRedactedSyncStateEvent::RoomJoinRules(_)
| AnyRedactedSyncStateEvent::RoomName(_)
| AnyRedactedSyncStateEvent::RoomPinnedEvents(_)
| AnyRedactedSyncStateEvent::RoomPowerLevels(_)
| AnyRedactedSyncStateEvent::RoomServerAcl(_)
| AnyRedactedSyncStateEvent::RoomTopic(_)
),
_ => false,
}
}
pub fn set_show_header(&self, visible: bool) {
let priv_ = imp::Event::from_instance(self);
if priv_.show_header.get() == visible {
return;
}
priv_.show_header.set(visible);
self.notify("show-header");
}
pub fn show_header(&self) -> bool {
let priv_ = imp::Event::from_instance(self);
priv_.show_header.get()
}
/// The content of this message.
///
/// Returns `None` if this is not a message.
pub fn message_content(&self) -> Option<AnyMessageEventContent> {
match self.matrix_event() {
Some(AnySyncRoomEvent::Message(message)) => Some(message.content()),
_ => None,
}
}
pub fn can_hide_header(&self) -> bool {
match self.message_content() {
Some(AnyMessageEventContent::RoomMessage(message)) => {
matches!(
message.msgtype,
MessageType::Audio(_)
| MessageType::File(_)
| MessageType::Image(_)
| MessageType::Location(_)
| MessageType::Notice(_)
| MessageType::Text(_)
| MessageType::Video(_)
)
}
Some(AnyMessageEventContent::Sticker(_)) => true,
_ => false,
}
}
/// Whether this is a replacing `Event`.
///
/// Replacing matrix events are:
///
/// - `RoomRedaction`
/// - `RoomMessage` with `Relation::Replacement`
pub fn is_replacing_event(&self) -> bool {
match self.matrix_event() {
Some(AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(message))) => {
matches!(message.content.relates_to, Some(Relation::Replacement(_)))
}
Some(AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomRedaction(_))) => true,
_ => false,
}
}
pub fn prepend_replacing_events(&self, events: Vec<Event>) {
let priv_ = imp::Event::from_instance(self);
priv_.replacing_events.borrow_mut().splice(..0, events);
}
pub fn append_replacing_events(&self, events: Vec<Event>) {
let priv_ = imp::Event::from_instance(self);
let old_replacement = self.replacement();
priv_.replacing_events.borrow_mut().extend(events);
let new_replacement = self.replacement();
// Update the signal handler to the new replacement
if new_replacement != old_replacement {
if let Some(replacement) = old_replacement {
if let Some(source_changed_handler) = priv_.source_changed_handler.take() {
replacement.disconnect(source_changed_handler);
}
}
// If the replacing event's content changed, this content changed too.
if let Some(replacement) = new_replacement {
priv_
.source_changed_handler
.replace(Some(replacement.connect_notify_local(
Some("source"),
clone!(@weak self as obj => move |_, _| {
obj.notify("source");
}),
)));
}
self.notify("source");
}
}
pub fn replacing_events(&self) -> Vec<Event> {
let priv_ = imp::Event::from_instance(self);
priv_.replacing_events.borrow().clone()
}
/// The `Event` that replaces this one, if any.
///
/// If this matrix event has been redacted or replaced, returns the corresponding `Event`,
/// otherwise returns `None`.
pub fn replacement(&self) -> Option<Event> {
self.replacing_events()
.iter()
.rev()
.find(|event| event.is_replacing_event() && !event.redacted())
.map(|event| event.clone())
}
/// Whether this matrix event has been redacted.
pub fn redacted(&self) -> bool {
self.replacement()
.filter(|event| {
matches!(
event.matrix_event(),
Some(AnySyncRoomEvent::Message(
AnySyncMessageEvent::RoomRedaction(_)
))
)
})
.is_some()
}
/// The content of this matrix event.
pub fn original_content(&self) -> Option<AnyMessageEventContent> {
match self.matrix_event()? {
AnySyncRoomEvent::Message(message) => Some(message.content()),
AnySyncRoomEvent::RedactedMessage(message) => {
if let Some(ref redaction_event) = message.unsigned().redacted_because {
Some(AnyMessageEventContent::RoomRedaction(
redaction_event.content.clone(),
))
} else {
Some(AnyMessageEventContent::RoomRedaction(
RoomRedactionEventContent::new(),
))
}
}
AnySyncRoomEvent::RedactedState(state) => {
if let Some(ref redaction_event) = state.unsigned().redacted_because {
Some(AnyMessageEventContent::RoomRedaction(
redaction_event.content.clone(),
))
} else {
Some(AnyMessageEventContent::RoomRedaction(
RoomRedactionEventContent::new(),
))
}
}
_ => None,
}
}
/// The content to display for this `Event`.
///
/// If this matrix event has been replaced, returns the replacing `Event`'s content.
pub fn content(&self) -> Option<AnyMessageEventContent> {
self.replacement()
.and_then(|replacement| replacement.content())
.or(self.original_content())
}
pub fn connect_show_header_notify<F: Fn(&Self, &glib::ParamSpec) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_notify_local(Some("show-header"), f)
}
/// The content of a media message.
///
/// Compatible events:
///
/// - File message (`MessageType::File`).
/// - Image message (`MessageType::Image`).
/// - Video message (`MessageType::Video`).
///
/// Returns `Ok((uid, filename, binary_content))` on success, `Err` if an error occured while
/// fetching the content. Panics on an incompatible event. `uid` is a unique identifier for this
/// media.
pub async fn get_media_content(&self) -> Result<(String, String, Vec<u8>), matrix_sdk::Error> {
if let AnyMessageEventContent::RoomMessage(content) = self.message_content().unwrap() {
let client = self.room().session().client();
match content.msgtype {
MessageType::File(content) => {
let uid = media_type_uid(content.file());
let filename = content
.filename
.as_ref()
.filter(|name| !name.is_empty())
.or(Some(&content.body))
.filter(|name| !name.is_empty())
.map(|name| name.clone())
.unwrap_or_else(|| {
filename_for_mime(
content
.info
.as_ref()
.and_then(|info| info.mimetype.as_deref()),
None,
)
});
let handle = spawn_tokio!(async move { client.get_file(content, true).await });
let data = handle.await.unwrap()?.unwrap();
return Ok((uid, filename, data));
}
MessageType::Image(content) => {
let uid = media_type_uid(content.file());
let filename = if content.body.is_empty() {
filename_for_mime(
content
.info
.as_ref()
.and_then(|info| info.mimetype.as_deref()),
Some(mime::IMAGE),
)
} else {
content.body.clone()
};
let handle = spawn_tokio!(async move { client.get_file(content, true).await });
let data = handle.await.unwrap()?.unwrap();
return Ok((uid, filename, data));
}
MessageType::Video(content) => {
let uid = media_type_uid(content.file());
let filename = if content.body.is_empty() {
filename_for_mime(
content
.info
.as_ref()
.and_then(|info| info.mimetype.as_deref()),
Some(mime::VIDEO),
)
} else {
content.body.clone()
};
let handle = spawn_tokio!(async move { client.get_file(content, true).await });
let data = handle.await.unwrap()?.unwrap();
return Ok((uid, filename, data));
}
_ => {}
};
};
panic!("Trying to get the media content of an event of incompatible type");
}
/// Whether this is a media event that can be viewed.
pub fn can_view_media(&self) -> bool {
match self.message_content() {
Some(AnyMessageEventContent::RoomMessage(message)) => {
matches!(
message.msgtype,
MessageType::Image(_) | MessageType::Video(_)
)
}
_ => false,
}
}
}