Skip to content

Commit ace0026

Browse files
committed
feat(sdk): add a room method to retrieve all related events
1 parent 9a5e9e8 commit ace0026

File tree

3 files changed

+317
-7
lines changed

3 files changed

+317
-7
lines changed

crates/matrix-sdk/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file.
99
### Features
1010

1111
- `Room::list_threads()` is a new method to list all the threads in a room.
12+
([#4972](https://github.com/matrix-org/matrix-rust-sdk/pull/4972))
13+
- `Room::relations()` is a new method to list all the events related to another event
14+
("relations"), with additional filters for relation type or relation type + event type.
15+
([#4972](https://github.com/matrix-org/matrix-rust-sdk/pull/4972))
1216

1317
### Bug fixes
1418

crates/matrix-sdk/src/room/messages.rs

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,27 @@
1414

1515
use std::fmt;
1616

17+
use futures_util::future::join_all;
1718
use matrix_sdk_common::{debug::DebugStructExt as _, deserialized_responses::TimelineEvent};
1819
use ruma::{
1920
api::{
2021
client::{
2122
filter::RoomEventFilter,
2223
message::get_message_events,
24+
relations,
2325
threads::get_threads::{self, v1::IncludeThreads},
2426
},
2527
Direction,
2628
},
2729
assign,
28-
events::AnyStateEvent,
30+
events::{relation::RelationType, AnyStateEvent, TimelineEventType},
2931
serde::Raw,
30-
uint, RoomId, UInt,
32+
uint, OwnedEventId, RoomId, UInt,
3133
};
3234

35+
use super::Room;
36+
use crate::Result;
37+
3338
/// Options for [`messages`][super::Room::messages].
3439
///
3540
/// See that method and
@@ -224,3 +229,149 @@ pub struct ThreadRoots {
224229
/// [`super::Room::list_threads`].
225230
pub prev_batch_token: Option<String>,
226231
}
232+
233+
/// What kind of relations should be included in a [`super::Room::relations`]
234+
/// query.
235+
#[derive(Clone, Debug, Default)]
236+
pub enum IncludeRelations {
237+
/// Include all relations independently of their relation type.
238+
#[default]
239+
AllRelations,
240+
/// Include all relations of a given relation type.
241+
RelationsOfType(RelationType),
242+
/// Include all relations of a given relation type and event type.
243+
RelationsOfTypeAndEventType(RelationType, TimelineEventType),
244+
}
245+
246+
/// Options for [`messages`][super::Room::relations].
247+
#[derive(Clone, Debug, Default)]
248+
pub struct RelationsOptions {
249+
/// The token to start returning events from.
250+
///
251+
/// This token can be obtained from a [`Relations::prev_batch_token`]
252+
/// returned by a previous call to [`super::Room::relations()`].
253+
///
254+
/// If `from` isn't provided the homeserver shall return a list of thread
255+
/// roots from end of the timeline history.
256+
pub from: Option<String>,
257+
258+
/// The direction to return events in.
259+
///
260+
/// Defaults to backwards.
261+
pub dir: Direction,
262+
263+
/// The maximum number of events to return.
264+
///
265+
/// Default: 10.
266+
pub limit: Option<UInt>,
267+
268+
/// Optional restrictions on the relations to include based on their type or
269+
/// event type.
270+
///
271+
/// Defaults to all relations.
272+
pub include_relations: IncludeRelations,
273+
274+
/// Whether to include events which relate indirectly to the given event.
275+
///
276+
/// These are events related to the given event via two or more direct
277+
/// relationships.
278+
pub recurse: bool,
279+
}
280+
281+
impl RelationsOptions {
282+
/// Converts this options object into a request, according to the filled
283+
/// parameters, and returns a canonicalized response.
284+
pub(super) async fn send(self, room: &Room, event: OwnedEventId) -> Result<Relations> {
285+
macro_rules! fill_params {
286+
($request:expr) => {
287+
assign! { $request, {
288+
from: self.from,
289+
dir: self.dir,
290+
limit: self.limit,
291+
recurse: self.recurse,
292+
}}
293+
};
294+
}
295+
296+
// This match to common out the different `Response` types into a single one. It
297+
// would've been nice that Ruma used the same response type for all the
298+
// responses, but it is likely doing so to guard against possible future
299+
// changes.
300+
let (chunk, prev_batch, next_batch, recursion_depth) = match self.include_relations {
301+
IncludeRelations::AllRelations => {
302+
let request = fill_params!(relations::get_relating_events::v1::Request::new(
303+
room.room_id().to_owned(),
304+
event,
305+
));
306+
let response = room.client.send(request).await?;
307+
(response.chunk, response.prev_batch, response.next_batch, response.recursion_depth)
308+
}
309+
310+
IncludeRelations::RelationsOfType(relation_type) => {
311+
let request =
312+
fill_params!(relations::get_relating_events_with_rel_type::v1::Request::new(
313+
room.room_id().to_owned(),
314+
event,
315+
relation_type,
316+
));
317+
let response = room.client.send(request).await?;
318+
(response.chunk, response.prev_batch, response.next_batch, response.recursion_depth)
319+
}
320+
321+
IncludeRelations::RelationsOfTypeAndEventType(relation_type, timeline_event_type) => {
322+
let request = fill_params!(
323+
relations::get_relating_events_with_rel_type_and_event_type::v1::Request::new(
324+
room.room_id().to_owned(),
325+
event,
326+
relation_type,
327+
timeline_event_type,
328+
)
329+
);
330+
let response = room.client.send(request).await?;
331+
(response.chunk, response.prev_batch, response.next_batch, response.recursion_depth)
332+
}
333+
};
334+
335+
let push_ctx = room.push_context().await?;
336+
let chunk = join_all(chunk.into_iter().map(|ev| {
337+
// Cast safety: an `AnyMessageLikeEvent` is a subset of an `AnyTimelineEvent`.
338+
room.try_decrypt_event(ev.cast(), push_ctx.as_ref())
339+
}))
340+
.await;
341+
342+
Ok(Relations {
343+
chunk,
344+
prev_batch_token: prev_batch,
345+
next_batch_token: next_batch,
346+
recursion_depth,
347+
})
348+
}
349+
}
350+
351+
/// The result of a [`super::Room::relations`] query.
352+
///
353+
/// This is a wrapper around the Ruma equivalents, with events decrypted if
354+
/// needs be.
355+
#[derive(Debug)]
356+
pub struct Relations {
357+
/// The events related to the specified event from the request.
358+
///
359+
/// Note: the events will be sorted according to the `dir` parameter:
360+
/// - if the direction was backwards, then the events will be ordered in
361+
/// reverse topological order.
362+
/// - if the direction was forwards, then the events will be ordered in
363+
/// topological order.
364+
pub chunk: Vec<TimelineEvent>,
365+
366+
/// An opaque string representing a pagination token to retrieve the
367+
/// previous batch of events.
368+
pub prev_batch_token: Option<String>,
369+
370+
/// An opaque string representing a pagination token to retrieve the next
371+
/// batch of events.
372+
pub next_batch_token: Option<String>,
373+
374+
/// If [`RelationsOptions::recurse`] was set, the depth to which the server
375+
/// recursed.
376+
pub recursion_depth: Option<UInt>,
377+
}

crates/matrix-sdk/src/room/mod.rs

Lines changed: 160 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ use matrix_sdk_common::{
5454
executor::{spawn, JoinHandle},
5555
timeout::timeout,
5656
};
57-
use messages::{ListThreadsOptions, ThreadRoots};
5857
use mime::Mime;
5958
use reply::Reply;
6059
#[cfg(feature = "e2e-encryption")]
@@ -132,7 +131,10 @@ use tracing::{debug, info, instrument, warn};
132131
use self::futures::{SendAttachment, SendMessageLikeEvent, SendRawMessageLikeEvent};
133132
pub use self::{
134133
member::{RoomMember, RoomMemberRole},
135-
messages::{EventWithContextResponse, Messages, MessagesOptions},
134+
messages::{
135+
EventWithContextResponse, IncludeRelations, ListThreadsOptions, Messages, MessagesOptions,
136+
Relations, RelationsOptions, ThreadRoots,
137+
},
136138
};
137139
#[cfg(doc)]
138140
use crate::event_cache::EventCache;
@@ -3568,6 +3570,27 @@ impl Room {
35683570

35693571
Ok(ThreadRoots { chunk, prev_batch_token: response.next_batch })
35703572
}
3573+
3574+
/// Retrieve a list of relations for the given event, according to the given
3575+
/// options.
3576+
///
3577+
/// Since this client-server API is paginated, the return type may include a
3578+
/// token used to resuming back-pagination into the list of results, in
3579+
/// [`Relations::prev_batch_token`]. This token can be fed back into
3580+
/// [`RelationsOptions::from`] to continue the pagination from the previous
3581+
/// position.
3582+
///
3583+
/// **Note**: if [`RelationsOptions::from`] is set for a subsequent request,
3584+
/// then it must be used with the same
3585+
/// [`RelationsOptions::include_relations`] value as the request that
3586+
/// returns the `from` token, otherwise the server behavior is undefined.
3587+
pub async fn relations(
3588+
&self,
3589+
event_id: OwnedEventId,
3590+
opts: RelationsOptions,
3591+
) -> Result<Relations> {
3592+
opts.send(self, event_id).await
3593+
}
35713594
}
35723595

35733596
#[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))]
@@ -3887,7 +3910,11 @@ mod tests {
38873910
async_test, event_factory::EventFactory, test_json, JoinedRoomBuilder, StateTestEvent,
38883911
SyncResponseBuilder,
38893912
};
3890-
use ruma::{event_id, events::room::member::MembershipState, int, room_id, user_id};
3913+
use ruma::{
3914+
event_id,
3915+
events::{relation::RelationType, room::member::MembershipState},
3916+
int, owned_event_id, room_id, user_id,
3917+
};
38913918
use wiremock::{
38923919
matchers::{header, method, path_regex},
38933920
Mock, MockServer, ResponseTemplate,
@@ -3896,8 +3923,12 @@ mod tests {
38963923
use super::ReportedContentScore;
38973924
use crate::{
38983925
config::RequestConfig,
3899-
room::messages::ListThreadsOptions,
3900-
test_utils::{client::mock_matrix_session, logged_in_client, mocks::MatrixMockServer},
3926+
room::messages::{IncludeRelations, ListThreadsOptions, RelationsOptions},
3927+
test_utils::{
3928+
client::mock_matrix_session,
3929+
logged_in_client,
3930+
mocks::{MatrixMockServer, RoomRelationsResponseTemplate},
3931+
},
39013932
Client,
39023933
};
39033934

@@ -4284,4 +4315,128 @@ mod tests {
42844315
assert_eq!(result.chunk[0].event_id().unwrap(), eid2);
42854316
assert!(result.prev_batch_token.is_none());
42864317
}
4318+
4319+
#[async_test]
4320+
async fn test_relations() {
4321+
let server = MatrixMockServer::new().await;
4322+
let client = server.client_builder().build().await;
4323+
4324+
let room_id = room_id!("!a:b.c");
4325+
let sender_id = user_id!("@alice:b.c");
4326+
let f = EventFactory::new().room(room_id).sender(sender_id);
4327+
4328+
let target_event_id = owned_event_id!("$target");
4329+
let eid1 = event_id!("$1");
4330+
let eid2 = event_id!("$2");
4331+
let batch1 = vec![f.text_msg("Related event 1").event_id(eid1).into_raw_sync().cast()];
4332+
let batch2 = vec![f.text_msg("Related event 2").event_id(eid2).into_raw_sync().cast()];
4333+
4334+
server
4335+
.mock_room_relations()
4336+
.match_target_event(target_event_id.clone())
4337+
.ok(RoomRelationsResponseTemplate::default().events(batch1).next_batch("next_batch"))
4338+
.mock_once()
4339+
.mount()
4340+
.await;
4341+
4342+
server
4343+
.mock_room_relations()
4344+
.match_target_event(target_event_id.clone())
4345+
.match_from("next_batch")
4346+
.ok(RoomRelationsResponseTemplate::default().events(batch2))
4347+
.mock_once()
4348+
.mount()
4349+
.await;
4350+
4351+
let room = server.sync_joined_room(&client, room_id).await;
4352+
4353+
// Main endpoint: no relation type filtered out.
4354+
let mut opts = RelationsOptions {
4355+
include_relations: IncludeRelations::AllRelations,
4356+
..Default::default()
4357+
};
4358+
let result = room
4359+
.relations(target_event_id.clone(), opts.clone())
4360+
.await
4361+
.expect("Failed to list relations the first time");
4362+
assert_eq!(result.chunk.len(), 1);
4363+
assert_eq!(result.chunk[0].event_id().unwrap(), eid1);
4364+
assert!(result.prev_batch_token.is_none());
4365+
assert!(result.next_batch_token.is_some());
4366+
assert!(result.recursion_depth.is_none());
4367+
4368+
opts.from = result.next_batch_token;
4369+
let result = room
4370+
.relations(target_event_id, opts)
4371+
.await
4372+
.expect("Failed to list relations the second time");
4373+
assert_eq!(result.chunk.len(), 1);
4374+
assert_eq!(result.chunk[0].event_id().unwrap(), eid2);
4375+
assert!(result.prev_batch_token.is_none());
4376+
assert!(result.next_batch_token.is_none());
4377+
assert!(result.recursion_depth.is_none());
4378+
}
4379+
4380+
#[async_test]
4381+
async fn test_relations_with_reltype() {
4382+
let server = MatrixMockServer::new().await;
4383+
let client = server.client_builder().build().await;
4384+
4385+
let room_id = room_id!("!a:b.c");
4386+
let sender_id = user_id!("@alice:b.c");
4387+
let f = EventFactory::new().room(room_id).sender(sender_id);
4388+
4389+
let target_event_id = owned_event_id!("$target");
4390+
let eid1 = event_id!("$1");
4391+
let eid2 = event_id!("$2");
4392+
let batch1 = vec![f.text_msg("In-thread event 1").event_id(eid1).into_raw_sync().cast()];
4393+
let batch2 = vec![f.text_msg("In-thread event 2").event_id(eid2).into_raw_sync().cast()];
4394+
4395+
server
4396+
.mock_room_relations()
4397+
.match_target_event(target_event_id.clone())
4398+
.match_subrequest(IncludeRelations::RelationsOfType(RelationType::Thread))
4399+
.ok(RoomRelationsResponseTemplate::default().events(batch1).next_batch("next_batch"))
4400+
.mock_once()
4401+
.mount()
4402+
.await;
4403+
4404+
server
4405+
.mock_room_relations()
4406+
.match_target_event(target_event_id.clone())
4407+
.match_from("next_batch")
4408+
.match_subrequest(IncludeRelations::RelationsOfType(RelationType::Thread))
4409+
.ok(RoomRelationsResponseTemplate::default().events(batch2))
4410+
.mock_once()
4411+
.mount()
4412+
.await;
4413+
4414+
let room = server.sync_joined_room(&client, room_id).await;
4415+
4416+
// Reltype-filtered endpoint, for threads \o/
4417+
let mut opts = RelationsOptions {
4418+
include_relations: IncludeRelations::RelationsOfType(RelationType::Thread),
4419+
..Default::default()
4420+
};
4421+
let result = room
4422+
.relations(target_event_id.clone(), opts.clone())
4423+
.await
4424+
.expect("Failed to list relations the first time");
4425+
assert_eq!(result.chunk.len(), 1);
4426+
assert_eq!(result.chunk[0].event_id().unwrap(), eid1);
4427+
assert!(result.prev_batch_token.is_none());
4428+
assert!(result.next_batch_token.is_some());
4429+
assert!(result.recursion_depth.is_none());
4430+
4431+
opts.from = result.next_batch_token;
4432+
let result = room
4433+
.relations(target_event_id, opts)
4434+
.await
4435+
.expect("Failed to list relations the second time");
4436+
assert_eq!(result.chunk.len(), 1);
4437+
assert_eq!(result.chunk[0].event_id().unwrap(), eid2);
4438+
assert!(result.prev_batch_token.is_none());
4439+
assert!(result.next_batch_token.is_none());
4440+
assert!(result.recursion_depth.is_none());
4441+
}
42874442
}

0 commit comments

Comments
 (0)