mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-10-16 00:29:59 +00:00
feat(admin): List user memberships route
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
pub mod create;
|
||||
pub mod list;
|
||||
pub mod rooms;
|
||||
|
||||
@@ -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
@@ -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 } }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user