Skip to content

Commit 12c280b

Browse files
bnjbvrpoljar
authored andcommitted
feat(sdk): add a room method to retrieve all related events
1 parent b2dcf33 commit 12c280b

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;
@@ -3566,6 +3568,27 @@ impl Room {
35663568

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

35713594
#[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))]
@@ -3885,7 +3908,11 @@ mod tests {
38853908
async_test, event_factory::EventFactory, test_json, JoinedRoomBuilder, StateTestEvent,
38863909
SyncResponseBuilder,
38873910
};
3888-
use ruma::{event_id, events::room::member::MembershipState, int, room_id, user_id};
3911+
use ruma::{
3912+
event_id,
3913+
events::{relation::RelationType, room::member::MembershipState},
3914+
int, owned_event_id, room_id, user_id,
3915+
};
38893916
use wiremock::{
38903917
matchers::{header, method, path_regex},
38913918
Mock, MockServer, ResponseTemplate,
@@ -3894,8 +3921,12 @@ mod tests {
38943921
use super::ReportedContentScore;
38953922
use crate::{
38963923
config::RequestConfig,
3897-
room::messages::ListThreadsOptions,
3898-
test_utils::{client::mock_matrix_session, logged_in_client, mocks::MatrixMockServer},
3924+
room::messages::{IncludeRelations, ListThreadsOptions, RelationsOptions},
3925+
test_utils::{
3926+
client::mock_matrix_session,
3927+
logged_in_client,
3928+
mocks::{MatrixMockServer, RoomRelationsResponseTemplate},
3929+
},
38993930
Client,
39003931
};
39013932

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

0 commit comments

Comments
 (0)