tuwunel_api/client/membership/
members.rs1use 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
21pub(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
86pub(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}