#![allow(dead_code)]
use eyeball_im::Vector;
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use ruma::{
events::{
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
receipt::{ReceiptEventContent, ReceiptThread, ReceiptType},
room::message::Relation,
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
SyncMessageLikeEvent,
},
serde::Raw,
EventId, OwnedEventId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
use tracing::{instrument, trace};
use crate::error::Result;
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub(crate) struct RoomReadReceipts {
pub num_unread: u64,
pub num_notifications: u64,
pub num_mentions: u64,
latest_read_receipt_event_id: Option<OwnedEventId>,
}
impl RoomReadReceipts {
#[inline(always)]
fn update_for_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) -> bool {
let mut has_unread = false;
if marks_as_unread(&event.event, user_id) {
self.num_unread += 1;
has_unread = true
}
let mut has_notify = false;
let mut has_mention = false;
for action in &event.push_actions {
if !has_notify && action.should_notify() {
self.num_notifications += 1;
has_notify = true;
}
if !has_mention && action.is_highlight() {
self.num_mentions += 1;
has_mention = true;
}
}
has_unread || has_notify || has_mention
}
#[inline(always)]
fn reset(&mut self) {
self.num_unread = 0;
self.num_notifications = 0;
self.num_mentions = 0;
}
fn find_and_count_events<'a>(
&mut self,
receipt_event_id: &EventId,
user_id: &UserId,
events: impl IntoIterator<Item = &'a SyncTimelineEvent>,
) -> bool {
let mut counting_receipts = false;
for event in events {
if counting_receipts {
self.update_for_event(event, user_id);
} else if let Ok(Some(event_id)) = event.event.get_field::<OwnedEventId>("event_id") {
if event_id == receipt_event_id {
trace!("Found the event the receipt was referring to! Starting to count.");
self.reset();
counting_receipts = true;
}
}
}
counting_receipts
}
}
pub trait PreviousEventsProvider: Send + Sync {
fn for_room(&self, room_id: &RoomId) -> Vector<SyncTimelineEvent>;
}
impl PreviousEventsProvider for () {
fn for_room(&self, _: &RoomId) -> Vector<SyncTimelineEvent> {
Vector::new()
}
}
#[instrument(skip_all, fields(room_id = %room_id, ?read_receipts))]
pub(crate) fn compute_notifications<PEP: PreviousEventsProvider>(
user_id: &UserId,
room_id: &RoomId,
receipt_event: Option<&ReceiptEventContent>,
previous_events_provider: &PEP,
new_events: &[SyncTimelineEvent],
read_receipts: &mut RoomReadReceipts,
) -> Result<bool> {
let prev_latest_receipt_event_id = read_receipts.latest_read_receipt_event_id.clone();
if let Some(receipt_event) = receipt_event {
trace!("Got a new receipt event!");
let mut receipt_event_id = None;
if let Some((event_id, receipt)) = receipt_event
.user_receipt(user_id, ReceiptType::Read)
.or_else(|| receipt_event.user_receipt(user_id, ReceiptType::ReadPrivate))
{
if receipt.thread == ReceiptThread::Unthreaded || receipt.thread == ReceiptThread::Main
{
receipt_event_id = Some(event_id.to_owned());
}
}
if let Some(receipt_event_id) = receipt_event_id {
read_receipts.latest_read_receipt_event_id = Some(receipt_event_id.clone());
trace!("We got a new event with a read receipt: {receipt_event_id}. Search in new events...");
if read_receipts.find_and_count_events(&receipt_event_id, user_id, new_events) {
return Ok(true);
}
let previous_events = previous_events_provider.for_room(room_id);
trace!("Couldn't find the event attached to the receipt in the new events; looking in past events too now...");
if read_receipts.find_and_count_events(
&receipt_event_id,
user_id,
previous_events.iter().chain(new_events.iter()),
) {
return Ok(true);
}
}
}
if let Some(receipt_event_id) = prev_latest_receipt_event_id {
trace!("No new receipts, or couldn't find attached event; looking if the past latest known receipt refers to a new event...");
if read_receipts.find_and_count_events(&receipt_event_id, user_id, new_events) {
return Ok(true);
}
}
trace!("Default path: including all new events for the receipts count.");
let mut new_receipt = false;
for event in new_events {
if read_receipts.update_for_event(event, user_id) {
new_receipt = true;
}
}
Ok(new_receipt)
}
fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool {
let event = match event.deserialize() {
Ok(event) => event,
Err(err) => {
tracing::debug!(
"couldn't deserialize event {:?}: {err}",
event.get_field::<String>("event_id").ok().flatten()
);
return false;
}
};
if event.sender() == user_id {
return false;
}
match event {
ruma::events::AnySyncTimelineEvent::MessageLike(event) => {
let Some(content) = event.original_content() else {
tracing::trace!("not interesting because redacted");
return false;
};
if matches!(
content.relation(),
Some(ruma::events::room::encrypted::Relation::Replacement(..))
) {
tracing::trace!("not interesting because edited");
return false;
}
match event {
AnySyncMessageLikeEvent::CallAnswer(_)
| AnySyncMessageLikeEvent::CallInvite(_)
| AnySyncMessageLikeEvent::CallHangup(_)
| AnySyncMessageLikeEvent::CallCandidates(_)
| AnySyncMessageLikeEvent::CallNegotiate(_)
| AnySyncMessageLikeEvent::CallReject(_)
| AnySyncMessageLikeEvent::CallSelectAnswer(_)
| AnySyncMessageLikeEvent::PollResponse(_)
| AnySyncMessageLikeEvent::UnstablePollResponse(_)
| AnySyncMessageLikeEvent::Reaction(_)
| AnySyncMessageLikeEvent::RoomRedaction(_)
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
| AnySyncMessageLikeEvent::KeyVerificationReady(_)
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
| AnySyncMessageLikeEvent::KeyVerificationDone(_)
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
| AnySyncMessageLikeEvent::KeyVerificationKey(_) => false,
AnySyncMessageLikeEvent::PollStart(SyncMessageLikeEvent::Original(
OriginalSyncMessageLikeEvent {
content:
PollStartEventContent { relates_to: Some(Relation::Replacement(_)), .. },
..
},
))
| AnySyncMessageLikeEvent::UnstablePollStart(SyncMessageLikeEvent::Original(
OriginalSyncMessageLikeEvent {
content: UnstablePollStartEventContent::Replacement(_),
..
},
)) => false,
AnySyncMessageLikeEvent::Message(_)
| AnySyncMessageLikeEvent::PollStart(_)
| AnySyncMessageLikeEvent::UnstablePollStart(_)
| AnySyncMessageLikeEvent::PollEnd(_)
| AnySyncMessageLikeEvent::UnstablePollEnd(_)
| AnySyncMessageLikeEvent::RoomEncrypted(_)
| AnySyncMessageLikeEvent::RoomMessage(_)
| AnySyncMessageLikeEvent::Sticker(_) => true,
_ => {
tracing::debug!("unhandled timeline event type: {}", event.event_type());
false
}
}
}
ruma::events::AnySyncTimelineEvent::State(_) => false,
}
}
#[cfg(test)]
mod tests {
use std::ops::Not as _;
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use matrix_sdk_test::sync_timeline_event;
use ruma::{event_id, push::Action, user_id, EventId, UserId};
use crate::read_receipts::{marks_as_unread, RoomReadReceipts};
#[test]
fn test_room_message_marks_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let ev = sync_timeline_event!({
"sender": other_user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
});
assert!(marks_as_unread(&ev, user_id));
let ev = sync_timeline_event!({
"sender": user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
});
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_room_edit_doesnt_mark_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let ev = sync_timeline_event!({
"sender": other_user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": {
"body": " * edited message",
"m.new_content": {
"body": "edited message",
"msgtype": "m.text"
},
"m.relates_to": {
"event_id": "$someeventid:localhost",
"rel_type": "m.replace"
},
"msgtype": "m.text"
},
});
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_redaction_doesnt_mark_room_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let ev = sync_timeline_event!({
"content": {
"reason": "🛑"
},
"event_id": "$151957878228ssqrJ:localhost",
"origin_server_ts": 151957878000000_u64,
"sender": other_user_id,
"type": "m.room.redaction",
"redacts": "$151957878228ssqrj:localhost",
"unsigned": {
"age": 85
}
});
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_reaction_doesnt_mark_room_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let ev = sync_timeline_event!({
"content": {
"m.relates_to": {
"event_id": "$15275047031IXQRi:localhost",
"key": "👍",
"rel_type": "m.annotation"
}
},
"event_id": "$15275047031IXQRi:localhost",
"origin_server_ts": 159027581000000_u64,
"sender": other_user_id,
"type": "m.reaction",
"unsigned": {
"age": 85
}
});
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_state_event_doesnt_mark_as_unread() {
let user_id = user_id!("@alice:example.org");
let event_id = event_id!("$1");
let ev = sync_timeline_event!({
"content": {
"displayname": "Alice",
"membership": "join",
},
"event_id": event_id,
"origin_server_ts": 1432135524678u64,
"sender": user_id,
"state_key": user_id,
"type": "m.room.member",
});
assert!(marks_as_unread(&ev, user_id).not());
let other_user_id = user_id!("@bob:example.org");
assert!(marks_as_unread(&ev, other_user_id).not());
}
#[test]
fn test_count_unread_and_mentions() {
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> SyncTimelineEvent {
SyncTimelineEvent {
event: sync_timeline_event!({
"sender": user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
}),
encryption_info: None,
push_actions,
}
}
let user_id = user_id!("@alice:example.org");
let event = make_event(user_id, Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
let event = make_event(user_id!("@bob:example.org"), Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
let event = make_event(
user_id!("@bob:example.org"),
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true))],
);
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 0);
let event = make_event(
user_id!("@bob:example.org"),
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true)), Action::Notify],
);
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 1);
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
}
#[test]
fn test_find_and_count_events() {
let ev0 = event_id!("$0");
let user_id = user_id!("@alice:example.org");
let mut receipts = RoomReadReceipts::default();
assert!(receipts.find_and_count_events(ev0, user_id, &[]).not());
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
fn make_event(event_id: &EventId) -> SyncTimelineEvent {
SyncTimelineEvent {
event: sync_timeline_event!({
"sender": "@bob:example.org",
"type": "m.room.message",
"event_id": event_id,
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
}),
encryption_info: None,
push_actions: Vec::new(),
}
}
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
latest_read_receipt_event_id: None,
};
assert!(receipts
.find_and_count_events(ev0, user_id, &[make_event(event_id!("$1"))],)
.not());
assert_eq!(receipts.num_unread, 42);
assert_eq!(receipts.num_notifications, 13);
assert_eq!(receipts.num_mentions, 37);
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
latest_read_receipt_event_id: None,
};
assert!(receipts.find_and_count_events(ev0, user_id, &[make_event(ev0)]));
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
latest_read_receipt_event_id: None,
};
assert!(receipts
.find_and_count_events(
ev0,
user_id,
&[
make_event(event_id!("$1")),
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
)
.not());
assert_eq!(receipts.num_unread, 42);
assert_eq!(receipts.num_notifications, 13);
assert_eq!(receipts.num_mentions, 37);
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
latest_read_receipt_event_id: None,
};
assert!(receipts.find_and_count_events(
ev0,
user_id,
&[
make_event(event_id!("$1")),
make_event(ev0),
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
));
assert_eq!(receipts.num_unread, 2);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
}
}