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::{Err, Result, at, err, is_equal_to, is_not_equal_to, utils::stream::ReadyExt};
14use tuwunel_matrix::{Event, PduCount};
15
16use crate::Ruma;
17
18/// Lists the room's member events at the current state or a token's snapshot.
19///
20/// Accepts sync `prev_batch`/`next_batch` and `/messages` tokens.
21/// Visibility is decided from the caller's current membership; Synapse
22/// decides it from the state at the token. A former member's snapshot never
23/// extends past their departure.
24pub(crate) async fn get_member_events_route(
25	State(services): State<crate::State>,
26	body: Ruma<get_member_events::v3::Request>,
27) -> Result<get_member_events::v3::Response> {
28	let denied = "You aren't a member of the room and weren't previously a member of the room.";
29
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("{denied}")));
36	}
37
38	let at: Option<PduCount> = body
39		.at
40		.as_deref()
41		.map(str::parse)
42		.transpose()
43		.map_err(|_| err!(Request(InvalidParam("Invalid `at` token."))))?;
44
45	let shortstatehash = services
46		.state_accessor
47		.user_visible_shortstatehash(body.sender_user(), &body.room_id, at)
48		.map(|result| result?.ok_or_else(|| err!(Request(Forbidden("{denied}")))))
49		.await?;
50
51	let membership = body.membership.as_ref();
52	let not_membership = body.not_membership.as_ref();
53	let membership_filter = |content: &RoomMemberEventContent| {
54		membership.is_none_or(is_equal_to!(&content.membership))
55			&& not_membership.is_none_or(is_not_equal_to!(&content.membership))
56	};
57
58	Ok(get_member_events::v3::Response {
59		chunk: services
60			.state_accessor
61			.state_full(shortstatehash)
62			.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
63			.map(at!(1))
64			.ready_filter(|pdu| {
65				pdu.get_content::<RoomMemberEventContent>()
66					.as_ref()
67					.is_ok_and(membership_filter)
68			})
69			.map(Event::into_pdu)
70			.map(|pdu| pdu.without_private_unless_sender(Some(body.sender_user())))
71			.map(Event::into_format)
72			.collect()
73			.await,
74	})
75}
76
77/// # `GET /_matrix/client/r0/rooms/{roomId}/joined_members`
78///
79/// Lists all members of a room.
80///
81/// - The sender user must be in the room
82/// - TODO: An appservice just needs a puppet joined
83pub(crate) async fn joined_members_route(
84	State(services): State<crate::State>,
85	body: Ruma<joined_members::v3::Request>,
86) -> Result<joined_members::v3::Response> {
87	if !services
88		.state_accessor
89		.user_can_peek(body.sender_user(), &body.room_id)
90		.await
91	{
92		return Err!(Request(Forbidden("You aren't a member of the room.")));
93	}
94
95	Ok(joined_members::v3::Response {
96		joined: services
97			.state_accessor
98			.room_state_full(&body.room_id)
99			.ready_filter_map(Result::ok)
100			.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
101			.map(at!(1))
102			.ready_filter_map(|pdu| {
103				let content = pdu.get_content::<RoomMemberEventContent>().ok()?;
104
105				let matches = content.membership == MembershipState::Join;
106
107				matches.then(|| {
108					let sender = pdu.sender().to_owned();
109					let member = RoomMember {
110						display_name: content.displayname,
111						avatar_url: content.avatar_url,
112					};
113
114					(sender, member)
115				})
116			})
117			.collect()
118			.await,
119	})
120}