Skip to main content

tuwunel_api/client/membership/
members.rs

1use axum::extract::State;
2use futures::{FutureExt, StreamExt};
3use ruma::{
4	api::client::membership::{
5		get_member_events,
6		joined_members::{self, v3::RoomMember},
7	},
8	events::{
9		StateEventType,
10		room::member::{MembershipState, RoomMemberEventContent},
11	},
12};
13use tuwunel_core::{
14	Err, Result, at, err, is_equal_to, is_not_equal_to,
15	matrix::{Event, PduCount},
16	utils::stream::ReadyExt,
17};
18
19use crate::Ruma;
20
21/// Lists the room's member events at the current state or a token's snapshot.
22///
23/// Accepts sync `prev_batch`/`next_batch` and `/messages` tokens.
24/// Visibility is decided from the caller's current membership; Synapse
25/// decides it from the state at the token.
26pub(crate) async fn get_member_events_route(
27	State(services): State<crate::State>,
28	body: Ruma<get_member_events::v3::Request>,
29) -> Result<get_member_events::v3::Response> {
30	if !services
31		.state_accessor
32		.user_can_see_state_events(body.sender_user(), &body.room_id)
33		.await
34	{
35		return Err!(Request(Forbidden(
36			"You aren't a member of the room and weren't previously a member of the room."
37		)));
38	}
39
40	let at: Option<PduCount> = body
41		.at
42		.as_deref()
43		.map(str::parse)
44		.transpose()
45		.map_err(|_| err!(Request(InvalidParam("Invalid `at` token."))))?;
46
47	let shortstatehash = match at {
48		| None => services
49			.state
50			.get_room_shortstatehash(&body.room_id)
51			.await
52			.map_err(|e| err!(Database("Missing state for {:?}: {e:?}", body.room_id)))?,
53
54		| Some(at) =>
55			services
56				.timeline
57				.shortstatehash_after(&body.room_id, at)
58				.await?,
59	};
60
61	let membership = body.membership.as_ref();
62	let not_membership = body.not_membership.as_ref();
63	let membership_filter = |content: &RoomMemberEventContent| {
64		membership.is_none_or(is_equal_to!(&content.membership))
65			&& not_membership.is_none_or(is_not_equal_to!(&content.membership))
66	};
67
68	Ok(get_member_events::v3::Response {
69		chunk: services
70			.state_accessor
71			.state_full(shortstatehash)
72			.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
73			.map(at!(1))
74			.ready_filter(|pdu| {
75				pdu.get_content::<RoomMemberEventContent>()
76					.as_ref()
77					.is_ok_and(membership_filter)
78			})
79			.map(Event::into_format)
80			.collect()
81			.boxed()
82			.await,
83	})
84}
85
86/// # `GET /_matrix/client/r0/rooms/{roomId}/joined_members`
87///
88/// Lists all members of a room.
89///
90/// - The sender user must be in the room
91/// - TODO: An appservice just needs a puppet joined
92pub(crate) async fn joined_members_route(
93	State(services): State<crate::State>,
94	body: Ruma<joined_members::v3::Request>,
95) -> Result<joined_members::v3::Response> {
96	if !services
97		.state_accessor
98		.user_can_peek(body.sender_user(), &body.room_id)
99		.await
100	{
101		return Err!(Request(Forbidden("You aren't a member of the room.")));
102	}
103
104	Ok(joined_members::v3::Response {
105		joined: services
106			.state_accessor
107			.room_state_full(&body.room_id)
108			.ready_filter_map(Result::ok)
109			.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
110			.map(at!(1))
111			.ready_filter_map(|pdu| {
112				let content = pdu.get_content::<RoomMemberEventContent>().ok()?;
113
114				let matches = content.membership == MembershipState::Join;
115
116				matches.then(|| {
117					let sender = pdu.sender().to_owned();
118					let member = RoomMember {
119						display_name: content.displayname,
120						avatar_url: content.avatar_url,
121					};
122
123					(sender, member)
124				})
125			})
126			.collect()
127			.boxed()
128			.await,
129	})
130}