Skip to content

Commit 298f007

Browse files
committed
events: Add support for intentional mentions
According to MSC3952
1 parent 07bc060 commit 298f007

File tree

6 files changed

+276
-21
lines changed

6 files changed

+276
-21
lines changed

crates/ruma-common/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ Breaking changes:
2020
- Remove `SessionDescriptionType`, use a `String` instead. A clarification in MSC2746 / Matrix 1.7
2121
explains that the `type` field should not be validated but passed as-is to the WebRTC API. It
2222
also avoids an unnecessary conversion between the WebRTC API and the Ruma type.
23-
- The `reason` field in `CallHangupEventContent` is now required an defaults to `Reason::UserHangup`
23+
- The `reason` field in `CallHangupEventContent` is now required and defaults to `Reason::UserHangup`
2424
(MSC2746 / Matrix 1.7)
25+
- The `Replacement` relation for `RoomMessageEventContent` now takes a
26+
`RoomMessageEventContentWithoutRelation` instead of a `MessageType`
2527

2628
Improvements:
2729

@@ -50,6 +52,7 @@ Improvements:
5052
- Stabilize support for VoIP signalling improvements (MSC2746 / Matrix 1.7)
5153
- Make the generated and stripped plain text reply fallback behavior more compatible with most
5254
of the Matrix ecosystem.
55+
- Add support for intentional mentions according to MSC3952 / Matrix 1.7
5356

5457
# 0.11.3
5558

crates/ruma-common/src/events.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@
102102
//! ));
103103
//! ```
104104
105-
use serde::{de::IgnoredAny, Deserialize, Serializer};
105+
use serde::{de::IgnoredAny, Deserialize, Serialize, Serializer};
106106

107-
use crate::{EventEncryptionAlgorithm, RoomVersionId};
107+
use crate::{EventEncryptionAlgorithm, OwnedUserId, RoomVersionId};
108108

109109
// Needs to be public for trybuild tests
110110
#[doc(hidden)]
@@ -224,3 +224,37 @@ pub fn serialize_custom_event_error<T, S: Serializer>(_: &T, _: S) -> Result<S::
224224
`serde_json::value::to_raw_value` and `Raw::from_json`.",
225225
))
226226
}
227+
228+
/// Describes whether the event mentions other users or the room.
229+
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
230+
#[non_exhaustive]
231+
pub struct Mentions {
232+
/// The list of mentioned users.
233+
///
234+
/// Defaults to an empty `Vec`.
235+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
236+
pub user_ids: Vec<OwnedUserId>,
237+
238+
/// Whether the whole room is mentioned.
239+
///
240+
/// Defaults to `false`.
241+
#[serde(default, skip_serializing_if = "crate::serde::is_default")]
242+
pub room: bool,
243+
}
244+
245+
impl Mentions {
246+
/// Create a `Mentions` with the default values.
247+
pub fn new() -> Self {
248+
Self::default()
249+
}
250+
251+
/// Create a `Mentions` for the given user IDs.
252+
pub fn with_user_ids(user_ids: Vec<OwnedUserId>) -> Self {
253+
Self { user_ids, ..Default::default() }
254+
}
255+
256+
/// Create a `Mentions` for a room mention.
257+
pub fn with_room_mention() -> Self {
258+
Self { room: true, ..Default::default() }
259+
}
260+
}

crates/ruma-common/src/events/room/message.rs

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
99
use serde_json::Value as JsonValue;
1010

1111
use crate::{
12-
events::relation::{CustomRelation, InReplyTo, RelationType, Replacement, Thread},
12+
events::{
13+
relation::{CustomRelation, InReplyTo, RelationType, Replacement, Thread},
14+
Mentions,
15+
},
1316
serde::{JsonObject, StringEnum},
14-
EventId, OwnedEventId, PrivOwnedStr,
17+
EventId, PrivOwnedStr,
1518
};
1619

1720
mod audio;
@@ -64,13 +67,23 @@ pub struct RoomMessageEventContent {
6467
///
6568
/// [related messages]: https://spec.matrix.org/latest/client-server-api/#forming-relationships-between-events
6669
#[serde(flatten, skip_serializing_if = "Option::is_none")]
67-
pub relates_to: Option<Relation<MessageType>>,
70+
pub relates_to: Option<Relation<RoomMessageEventContentWithoutRelation>>,
71+
72+
/// The [mentions] of this event.
73+
///
74+
/// This should always be set to avoid triggering the legacy mention push rules. It is
75+
/// recommended to use [`Self::set_mentions()`] to set this field, that will take care of
76+
/// populating the fields correctly if this is a replacement.
77+
///
78+
/// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
79+
#[serde(rename = "m.mentions", skip_serializing_if = "Option::is_none")]
80+
pub mentions: Option<Mentions>,
6881
}
6982

7083
impl RoomMessageEventContent {
7184
/// Create a `RoomMessageEventContent` with the given `MessageType`.
7285
pub fn new(msgtype: MessageType) -> Self {
73-
Self { msgtype, relates_to: None }
86+
Self { msgtype, relates_to: None, mentions: None }
7487
}
7588

7689
/// A constructor to create a plain text message.
@@ -247,6 +260,10 @@ impl RoomMessageEventContent {
247260
/// `original_message`.
248261
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
249262
///
263+
/// If the message that is replaced contains [`Mentions`], they are copied into
264+
/// `m.new_content` to keep the same mentions, but not into `content` to avoid repeated
265+
/// notifications.
266+
///
250267
/// # Panics
251268
///
252269
/// Panics if `self` has a `formatted_body` with a format other than HTML.
@@ -255,13 +272,16 @@ impl RoomMessageEventContent {
255272
#[track_caller]
256273
pub fn make_replacement(
257274
mut self,
258-
original_message_id: OwnedEventId,
275+
original_message: &OriginalSyncRoomMessageEvent,
259276
replied_to_message: Option<&OriginalRoomMessageEvent>,
260277
) -> Self {
261278
// Prepare relates_to with the untouched msgtype.
262279
let relates_to = Relation::Replacement(Replacement {
263-
event_id: original_message_id,
264-
new_content: self.msgtype.clone(),
280+
event_id: original_message.event_id.clone(),
281+
new_content: RoomMessageEventContentWithoutRelation {
282+
msgtype: self.msgtype.clone(),
283+
mentions: original_message.content.mentions.clone(),
284+
},
265285
});
266286

267287
let empty_formatted_body = || FormattedBody::html(String::new());
@@ -311,6 +331,43 @@ impl RoomMessageEventContent {
311331
self
312332
}
313333

334+
/// Set the [mentions] of this event.
335+
///
336+
/// If this event is a replacement, it will update the mentions both in the `content` and the
337+
/// `m.new_content` so only new mentions will trigger a notification. As such, this needs to be
338+
/// called after [`Self::make_replacement()`].
339+
///
340+
/// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
341+
pub fn set_mentions(mut self, mentions: Mentions) -> Self {
342+
if let Some(Relation::Replacement(replacement)) = &mut self.relates_to {
343+
let old_mentions = &replacement.new_content.mentions;
344+
345+
let new_mentions = if let Some(old_mentions) = old_mentions {
346+
let mut new_mentions = Mentions::new();
347+
348+
new_mentions.user_ids = mentions
349+
.user_ids
350+
.iter()
351+
.filter(|u| !old_mentions.user_ids.contains(*u))
352+
.cloned()
353+
.collect();
354+
355+
new_mentions.room = if old_mentions.room { false } else { mentions.room };
356+
357+
new_mentions
358+
} else {
359+
mentions.clone()
360+
};
361+
362+
replacement.new_content.mentions = Some(mentions);
363+
self.mentions = Some(new_mentions);
364+
} else {
365+
self.mentions = Some(mentions);
366+
}
367+
368+
self
369+
}
370+
314371
/// Returns a reference to the `msgtype` string.
315372
///
316373
/// If you want to access the message type-specific data rather than the message type itself,
@@ -352,6 +409,44 @@ impl RoomMessageEventContent {
352409
}
353410
}
354411

412+
/// Form of [`RoomMessageEventContent`] without relation.
413+
///
414+
/// To construct this type, construct a [`RoomMessageEventContent`] and then use one of its ::from()
415+
/// / .into() methods.
416+
#[derive(Clone, Debug, Serialize)]
417+
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
418+
pub struct RoomMessageEventContentWithoutRelation {
419+
/// A key which identifies the type of message being sent.
420+
///
421+
/// This also holds the specific content of each message.
422+
#[serde(flatten)]
423+
pub msgtype: MessageType,
424+
425+
/// The [mentions] of this event.
426+
///
427+
/// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
428+
#[serde(rename = "m.mentions", skip_serializing_if = "Option::is_none")]
429+
pub mentions: Option<Mentions>,
430+
}
431+
432+
impl RoomMessageEventContentWithoutRelation {
433+
/// Transform `self` into a `RoomMessageEventContent` with the given relation.
434+
pub fn with_relation(
435+
self,
436+
relates_to: Option<Relation<RoomMessageEventContentWithoutRelation>>,
437+
) -> RoomMessageEventContent {
438+
let Self { msgtype, mentions } = self;
439+
RoomMessageEventContent { msgtype, relates_to, mentions }
440+
}
441+
}
442+
443+
impl From<RoomMessageEventContent> for RoomMessageEventContentWithoutRelation {
444+
fn from(value: RoomMessageEventContent) -> Self {
445+
let RoomMessageEventContent { msgtype, mentions, .. } = value;
446+
Self { msgtype, mentions }
447+
}
448+
}
449+
355450
/// Whether or not to forward a [`Relation::Thread`] when sending a reply.
356451
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
357452
#[allow(clippy::exhaustive_enums)]

crates/ruma-common/src/events/room/message/content_serde.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,47 @@
33
use serde::{de, Deserialize};
44
use serde_json::value::RawValue as RawJsonValue;
55

6-
use super::{relation_serde::deserialize_relation, MessageType, RoomMessageEventContent};
7-
use crate::serde::from_raw_json_value;
6+
use super::{
7+
relation_serde::deserialize_relation, MessageType, RoomMessageEventContent,
8+
RoomMessageEventContentWithoutRelation,
9+
};
10+
use crate::{events::Mentions, serde::from_raw_json_value};
811

912
impl<'de> Deserialize<'de> for RoomMessageEventContent {
1013
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1114
where
1215
D: de::Deserializer<'de>,
1316
{
1417
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
18+
1519
let mut deserializer = serde_json::Deserializer::from_str(json.get());
1620
let relates_to = deserialize_relation(&mut deserializer).map_err(de::Error::custom)?;
1721

18-
Ok(Self { msgtype: from_raw_json_value(&json)?, relates_to })
22+
let MentionsDeHelper { mentions } = from_raw_json_value(&json)?;
23+
24+
Ok(Self { msgtype: from_raw_json_value(&json)?, relates_to, mentions })
25+
}
26+
}
27+
28+
impl<'de> Deserialize<'de> for RoomMessageEventContentWithoutRelation {
29+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
30+
where
31+
D: de::Deserializer<'de>,
32+
{
33+
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
34+
35+
let MentionsDeHelper { mentions } = from_raw_json_value(&json)?;
36+
37+
Ok(Self { msgtype: from_raw_json_value(&json)?, mentions })
1938
}
2039
}
2140

41+
#[derive(Deserialize)]
42+
struct MentionsDeHelper {
43+
#[serde(rename = "m.mentions")]
44+
mentions: Option<Mentions>,
45+
}
46+
2247
/// Helper struct to determine the msgtype from a `serde_json::value::RawValue`
2348
#[derive(Debug, Deserialize)]
2449
struct MessageTypeDeHelper {

crates/ruma-common/tests/events/relations.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ fn replacement_deserialize() {
107107
})
108108
);
109109
assert_eq!(replacement.event_id, "$1598361704261elfgc");
110-
assert_matches!(replacement.new_content, MessageType::Text(text));
110+
assert_matches!(replacement.new_content.msgtype, MessageType::Text(text));
111111
assert_eq!(text.body, "Hello! My name is bar");
112112
}
113113

0 commit comments

Comments
 (0)