Skip to main content

tuwunel_api/client/membership/
members.rs

1use axum::extract::State;
2use futures::{FutureExt, StreamExt, pin_mut};
3use ruma::{
4	api::client::membership::{
5		get_member_events::{self},
6		joined_members::{self, v3::RoomMember},
7	},
8	events::{
9		StateEventType,
10		room::{
11			history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
12			member::{MembershipState, RoomMemberEventContent},
13		},
14	},
15};
16use tuwunel_core::{
17	Err, Result, at, is_equal_to, is_not_equal_to,
18	matrix::Event,
19	utils::{
20		future::{BoolExt, TryExtExt},
21		stream::ReadyExt,
22	},
23};
24
25use crate::Ruma;
26
27/// # `POST /_matrix/client/r0/rooms/{roomId}/members`
28///
29/// Lists all joined users in a room (TODO: at a specific point in time, with a
30/// specific membership).
31///
32/// - Only works if the user is currently joined
33pub(crate) async fn get_member_events_route(
34	State(services): State<crate::State>,
35	body: Ruma<get_member_events::v3::Request>,
36) -> Result<get_member_events::v3::Response> {
37	if !services
38		.state_accessor
39		.user_can_see_state_events(body.sender_user(), &body.room_id)
40		.await
41	{
42		return Err!(Request(Forbidden(
43			"You aren't a member of the room and weren't previously a member of the room."
44		)));
45	}
46
47	let membership = body.membership.as_ref();
48	let not_membership = body.not_membership.as_ref();
49	let membership_filter = |content: &RoomMemberEventContent| {
50		membership.is_none_or(is_equal_to!(&content.membership))
51			&& not_membership.is_none_or(is_not_equal_to!(&content.membership))
52	};
53
54	Ok(get_member_events::v3::Response {
55		chunk: services
56			.state_accessor
57			.room_state_full(&body.room_id)
58			.ready_filter_map(Result::ok)
59			.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
60			.map(at!(1))
61			.ready_filter(|pdu| {
62				pdu.get_content::<RoomMemberEventContent>()
63					.as_ref()
64					.is_ok_and(membership_filter)
65			})
66			.map(Event::into_format)
67			.collect()
68			.boxed()
69			.await,
70	})
71}
72
73/// # `POST /_matrix/client/r0/rooms/{roomId}/joined_members`
74///
75/// Lists all members of a room.
76///
77/// - The sender user must be in the room
78/// - TODO: An appservice just needs a puppet joined
79pub(crate) async fn joined_members_route(
80	State(services): State<crate::State>,
81	body: Ruma<joined_members::v3::Request>,
82) -> Result<joined_members::v3::Response> {
83	let is_joined = services
84		.state_cache
85		.is_joined(body.sender_user(), &body.room_id);
86
87	let is_world_readable = services
88		.state_accessor
89		.room_state_get_content(&body.room_id, &StateEventType::RoomHistoryVisibility, "")
90		.map_ok_or(false, |c: RoomHistoryVisibilityEventContent| {
91			c.history_visibility == HistoryVisibility::WorldReadable
92		});
93
94	pin_mut!(is_joined, is_world_readable);
95	if !is_joined.or(is_world_readable).await {
96		return Err!(Request(Forbidden("You aren't a member of the room.")));
97	}
98
99	Ok(joined_members::v3::Response {
100		joined: services
101			.state_accessor
102			.room_state_full(&body.room_id)
103			.ready_filter_map(Result::ok)
104			.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
105			.map(at!(1))
106			.ready_filter_map(|pdu| {
107				let content = pdu.get_content::<RoomMemberEventContent>().ok()?;
108
109				let matches = content.membership == MembershipState::Join;
110
111				matches.then(|| {
112					let sender = pdu.sender().to_owned();
113					let member = RoomMember {
114						display_name: content.displayname,
115						avatar_url: content.avatar_url,
116					};
117
118					(sender, member)
119				})
120			})
121			.collect()
122			.boxed()
123			.await,
124	})
125}