Skip to main content

tuwunel_api/client/admin/rooms/
room_details.rs

1use axum::extract::State;
2use futures::{FutureExt, StreamExt, TryFutureExt, future::join4};
3use ruma::{
4	RoomId, UInt,
5	events::{StateEventType, room::tombstone::RoomTombstoneEventContent},
6	uint,
7};
8use synapse_admin_api::rooms::room_details::v1::{Request, Response};
9use tuwunel_core::{
10	Err, Result,
11	utils::{
12		TryFutureExtExt,
13		stream::{BroadbandExt, ReadyExt},
14	},
15};
16use tuwunel_service::Services;
17
18use super::{room_row, usize_to_uint};
19use crate::{Ruma, client::admin::require_admin};
20
21/// # `GET /_synapse/admin/v1/rooms/{room_id}`
22///
23/// Reports the shared room summary plus the details-only fields: topic, avatar,
24/// local device count, forgotten flag, and tombstone replacement.
25pub(crate) async fn admin_room_details_route(
26	State(services): State<crate::State>,
27	body: Ruma<Request>,
28) -> Result<Response> {
29	require_admin(&services, body.sender_user()).await?;
30
31	let room_id: &RoomId = &body.room_id;
32
33	if !services.metadata.exists(room_id).await {
34		return Err!(Request(NotFound("Room not found")));
35	}
36
37	let row = room_row(&services, room_id);
38
39	let topic = services
40		.state_accessor
41		.get_room_topic(room_id)
42		.ok();
43
44	let avatar = services
45		.state_accessor
46		.get_avatar(room_id)
47		.map_ok(|content| content.url)
48		.map(|url| url.ok().flatten());
49
50	let tombstone = services
51		.state_accessor
52		.room_state_get_content(room_id, &StateEventType::RoomTombstone, "")
53		.map(|content: Result<RoomTombstoneEventContent>| content.ok());
54
55	let (row, topic, avatar, tombstone) = join4(row, topic, avatar, tombstone).boxed().await;
56
57	let joined_local_devices = local_device_count(&services, room_id).await;
58
59	let forgotten = row.joined_local_members == uint!(0)
60		&& services
61			.state_cache
62			.room_useroncejoined(room_id)
63			.ready_any(|user_id| services.globals.user_is_local(user_id))
64			.await;
65
66	let (tombstoned, replacement_room) = tombstone
67		.map(|content| (true, Some(content.replacement_room)))
68		.unwrap_or((false, None));
69
70	Ok(Response {
71		room_id: row.room_id,
72		name: row.name,
73		topic,
74		avatar,
75		canonical_alias: row.canonical_alias,
76		joined_members: row.joined_members,
77		joined_local_members: row.joined_local_members,
78		joined_local_devices,
79		version: row.version,
80		creator: row.creator,
81		encryption: row.encryption,
82		federatable: row.federatable,
83		public: row.public,
84		join_rules: row.join_rules,
85		guest_access: row.guest_access,
86		history_visibility: row.history_visibility,
87		state_events: row.state_events,
88		room_type: row.room_type,
89		forgotten,
90		tombstoned,
91		replacement_room,
92	})
93}
94
95/// Sums the device counts of every local user currently joined to the room.
96async fn local_device_count(services: &Services, room_id: &RoomId) -> UInt {
97	let count: usize = services
98		.state_cache
99		.local_users_in_room(room_id)
100		.map(ToOwned::to_owned)
101		.broad_then(async |user_id| {
102			services
103				.users
104				.all_device_ids(&user_id)
105				.count()
106				.await
107		})
108		.ready_fold(0_usize, usize::saturating_add)
109		.await;
110
111	usize_to_uint(count)
112}