Skip to main content

tuwunel_api/client/admin/users/
account_data.rs

1use std::collections::BTreeMap;
2
3use axum::extract::State;
4use futures::StreamExt;
5use ruma::{
6	OwnedRoomId, UserId,
7	events::{
8		AnyGlobalAccountDataEventContent, AnyRawAccountDataEvent, AnyRoomAccountDataEventContent,
9	},
10	serde::Raw,
11};
12use synapse_admin_api::users::account_data::v1 as account_data;
13use tuwunel_core::{
14	Err, Result, extract_variant,
15	utils::{ReadyExt, stream::BroadbandExt},
16};
17
18use crate::{Ruma, client::admin::require_admin};
19
20type GlobalData = BTreeMap<String, Raw<AnyGlobalAccountDataEventContent>>;
21type RoomData = BTreeMap<String, Raw<AnyRoomAccountDataEventContent>>;
22
23/// # `GET /_synapse/admin/v1/users/{user_id}/accountdata`
24pub(crate) async fn admin_account_data_route(
25	State(services): State<crate::State>,
26	body: Ruma<account_data::Request>,
27) -> Result<account_data::Response> {
28	require_admin(&services, body.sender_user()).await?;
29
30	if !services.globals.user_is_local(&body.user_id) {
31		return Err!(Request(InvalidParam("Can only look up local users")));
32	}
33
34	if !services.users.exists(&body.user_id).await {
35		return Err!(Request(NotFound("User not found")));
36	}
37
38	let global: GlobalData = services
39		.account_data
40		.changes_since(None, &body.user_id, 0, None)
41		.ready_filter_map(|event| extract_variant!(event, AnyRawAccountDataEvent::Global))
42		.ready_filter_map(|event| split_type_content(&event))
43		.collect()
44		.await;
45
46	// Per-room account data has no single-scan primitive; scan each room the user
47	// has a membership row in (rooms with no membership row are missed).
48	let rooms = services
49		.state_cache
50		.user_memberships(&body.user_id, None)
51		.map(|(_, room_id)| room_id.to_owned())
52		.broad_filter_map(async |room_id| {
53			room_account_data(services, &body.user_id, room_id).await
54		})
55		.collect()
56		.await;
57
58	let data = account_data::AccountData { global, rooms };
59
60	Ok(account_data::Response::new(data))
61}
62
63async fn room_account_data(
64	services: crate::State,
65	user_id: &UserId,
66	room_id: OwnedRoomId,
67) -> Option<(OwnedRoomId, RoomData)> {
68	let data: RoomData = services
69		.account_data
70		.changes_since(Some(&room_id), user_id, 0, None)
71		.ready_filter_map(|event| extract_variant!(event, AnyRawAccountDataEvent::Room))
72		.ready_filter_map(|event| split_type_content(&event))
73		.collect()
74		.await;
75
76	(!data.is_empty()).then_some((room_id, data))
77}
78
79/// Splits a raw account-data event into its `type` key and the `content`
80/// re-cast to the content type, dropping any event missing either field.
81fn split_type_content<E, C>(event: &Raw<E>) -> Option<(String, Raw<C>)> {
82	let event_type = event.get_field::<String>("type").ok().flatten()?;
83	let content = event
84		.get_field::<Raw<C>>("content")
85		.ok()
86		.flatten()?;
87
88	Some((event_type, content))
89}