Skip to main content

tuwunel_api_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, 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	// size firewall
56	let (row, topic, avatar, tombstone) = join4(row, topic, avatar, tombstone).boxed().await;
57
58	let joined_local_devices = local_device_count(&services, room_id).await;
59
60	let forgotten = row.joined_local_members == uint!(0)
61		&& services
62			.state_cache
63			.room_useroncejoined(room_id)
64			.ready_any(|user_id| services.globals.user_is_local(user_id))
65			.await;
66
67	let (tombstoned, replacement_room) = tombstone
68		.map(|content| (true, Some(content.replacement_room)))
69		.unwrap_or((false, None));
70
71	Ok(Response {
72		room_id: row.room_id,
73		name: row.name,
74		topic,
75		avatar,
76		canonical_alias: row.canonical_alias,
77		joined_members: row.joined_members,
78		joined_local_members: row.joined_local_members,
79		joined_local_devices,
80		version: row.version,
81		creator: row.creator,
82		encryption: row.encryption,
83		federatable: row.federatable,
84		public: row.public,
85		join_rules: row.join_rules,
86		guest_access: row.guest_access,
87		history_visibility: row.history_visibility,
88		state_events: row.state_events,
89		room_type: row.room_type,
90		forgotten,
91		tombstoned,
92		replacement_room,
93	})
94}
95
96/// Sums the device counts of every local user currently joined to the room.
97async fn local_device_count(services: &Services, room_id: &RoomId) -> UInt {
98	let count: usize = services
99		.state_cache
100		.local_users_in_room(room_id)
101		.map(ToOwned::to_owned)
102		.broad_then(async |user_id| {
103			services
104				.users
105				.all_device_ids(&user_id)
106				.count()
107				.await
108		})
109		.ready_fold(0_usize, usize::saturating_add)
110		.await;
111
112	usize_to_uint(count)
113}