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::{Err, Result, at, err, is_equal_to, is_not_equal_to, utils::stream::ReadyExt};
14use tuwunel_matrix::{Event, PduCount};
15
16use crate::Ruma;
17
18pub(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
77pub(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}