feat(admin): List user memberships route

This commit is contained in:
timedout
2026-10-10 23:58:24 +01:00
parent 02f4f9d88d
commit ce178d86d5
5 changed files with 204 additions and 1 deletions
+1
View File
@@ -1,2 +1,3 @@
pub mod create;
pub mod list;
pub mod rooms;
+134
View File
@@ -0,0 +1,134 @@
use axum::extract::State;
use conduwuit::{
Err, Event, at,
utils::{IterStream, ReadyExt, TryFutureExtExt, stream::WidebandExt},
};
use futures::StreamExt;
use ruma::events::room::member::MembershipState;
use ruminuwuity::admin::continuwuity::users::rooms::v1::{Request, Response, RoomInfo};
use serde::Deserialize;
use crate::router::Ruma;
/// # `GET /_continuwuity/admin/v1/users/{user_id}/rooms`
///
/// Lists the memberships of a user on this server.
pub(crate) async fn list_user_rooms(
State(services): State<crate::State>,
body: Ruma<Request>,
) -> conduwuit::Result<Response> {
if !services
.users
.is_admin(body.identity.expect_sender_user()?)
.await
{
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
let limit = body.limit.unwrap_or(100).try_into()?;
let offset = body
.limit
.unwrap_or(100)
.saturating_mul(body.page.unwrap_or_default())
.try_into()?;
let room_ids: Vec<_> = match body.state {
| MembershipState::Join =>
services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.skip(offset)
.take(limit)
.collect()
.await,
| MembershipState::Invite =>
services
.rooms
.state_cache
.rooms_invited(&body.user_id)
.skip(offset)
.take(limit)
.map(at!(0))
.collect()
.await,
| MembershipState::Knock =>
services
.rooms
.state_cache
.rooms_knocked(&body.user_id)
.skip(offset)
.take(limit)
.map(at!(0))
.collect()
.await,
| MembershipState::Leave =>
services
.rooms
.state_cache
.rooms_left(&body.user_id)
.skip(offset)
.take(limit)
.ready_filter_map(|(room_id, pdu)| {
pdu.is_some_and(|pdu| {
pdu.get_content::<ExtractMembership>()
.expect("membership PDU must have a membership field")
.membership
== MembershipState::Leave
})
.then_some(room_id)
})
.collect()
.await,
| MembershipState::Ban =>
services
.rooms
.state_cache
.rooms_left(&body.user_id)
.skip(offset)
.take(limit)
.ready_filter_map(|(room_id, pdu)| {
pdu.is_some_and(|pdu| {
pdu.get_content::<ExtractMembership>()
.expect("membership PDU must have a membership field")
.membership
== MembershipState::Ban
})
.then_some(room_id)
})
.collect()
.await,
| _ => return Err!(Request(InvalidParam("Unknown membership kind: {:?}", body.state))),
};
let resp = room_ids
.into_iter()
.stream()
.wide_then(|room_id| async move {
let (name, avatar, alias, members) = tokio::join!(
services.rooms.state_accessor.get_name(&room_id).ok(),
services.rooms.state_accessor.get_avatar(&room_id),
services
.rooms
.state_accessor
.get_canonical_alias(&room_id)
.ok(),
services.rooms.state_cache.room_members(&room_id).count(),
);
(room_id, RoomInfo {
name,
avatar_url: avatar.into_option().and_then(|a| a.url),
canonical_alias: alias,
member_count: members.try_into().unwrap_or(0),
})
})
.collect()
.await;
Ok(Response::new(resp))
}
#[derive(Deserialize)]
struct ExtractMembership {
pub membership: MembershipState,
}
+2 -1
View File
@@ -199,7 +199,8 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
.ruma_route(&admin::rooms::ban::ban_room)
.ruma_route(&admin::rooms::list::list_rooms)
.ruma_route(&admin::users::list::list_users)
.ruma_route(&admin::users::create::create_user);
.ruma_route(&admin::users::create::create_user)
.ruma_route(&admin::users::rooms::list_user_rooms);
if config.allow_federation {
router = router
@@ -1,2 +1,3 @@
pub mod create;
pub mod list;
pub mod rooms;
@@ -0,0 +1,66 @@
pub mod v1 {
use std::collections::BTreeMap;
use ruma::{
OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
api::{OAuthClientScope, auth_scheme::AccessToken, request, response},
events::room::member::MembershipState,
metadata,
};
use serde::{Deserialize, Serialize};
metadata! {
method: GET,
rate_limited: false,
authentication: AccessToken,
history: {
1.0 => "/_continuwuity/admin/v1/users/{user_id}/rooms",
},
required_client_scopes: [OAuthClientScope::ServerAdministration]
}
#[request]
pub struct Request {
#[ruma_api(path)]
pub user_id: OwnedUserId,
pub state: MembershipState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct RoomInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<OwnedMxcUri>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canonical_alias: Option<OwnedRoomAliasId>,
pub member_count: u32,
}
#[response]
pub struct Response {
pub rooms: BTreeMap<OwnedRoomId, RoomInfo>,
}
impl Request {
#[must_use]
pub fn new(
user_id: OwnedUserId,
state: MembershipState,
limit: Option<u32>,
page: Option<u32>,
) -> Self {
Self { user_id, state, limit, page }
}
}
impl Response {
#[must_use]
pub fn new(rooms: BTreeMap<OwnedRoomId, RoomInfo>) -> Self { Self { rooms } }
}
}