Skip to main content

tuwunel_api/client/admin/users/
pushers.rs

1use axum::extract::State;
2use futures::StreamExt;
3use ruma::{
4	OwnedDeviceId,
5	api::client::push::{Pusher as RumaPusher, PusherKind},
6	serde::JsonObject,
7};
8use synapse_admin_api::users::pushers::v1 as pushers;
9use tuwunel_core::{
10	Result,
11	utils::{IterStream, math::ruma_from_usize, stream::WidebandExt},
12};
13
14use crate::{Ruma, client::admin::require_admin};
15
16/// # `GET /_synapse/admin/v1/users/{user_id}/pushers`
17///
18/// MSC3881 is unimplemented, so `enabled` is always true; `device_id` is
19/// resolved per pusher from its pushkey.
20pub(crate) async fn admin_pushers_route(
21	State(services): State<crate::State>,
22	body: Ruma<pushers::Request>,
23) -> Result<pushers::Response> {
24	require_admin(&services, body.sender_user()).await?;
25
26	let list: Vec<pushers::Pusher> = services
27		.pusher
28		.get_pushers(&body.user_id)
29		.await
30		.into_iter()
31		.stream()
32		.wide_then(async |pusher| {
33			let device_id = services
34				.pusher
35				.get_pusher_device(&pusher.ids.pushkey)
36				.await
37				.ok();
38
39			admin_pusher(pusher, device_id)
40		})
41		.collect()
42		.await;
43
44	let total = ruma_from_usize(list.len());
45
46	Ok(pushers::Response::new(list, total))
47}
48
49/// Projects a Matrix pusher into the Synapse admin pusher shape.
50fn admin_pusher(pusher: RumaPusher, device_id: Option<OwnedDeviceId>) -> pushers::Pusher {
51	let (kind, data) = split_kind(&pusher.kind);
52
53	pushers::Pusher {
54		app_display_name: pusher.app_display_name.to_string(),
55		app_id: pusher.ids.app_id,
56		data,
57		device_display_name: pusher.device_display_name.to_string(),
58		kind,
59		lang: Some(pusher.lang.to_string()),
60		profile_tag: pusher
61			.profile_tag
62			.map(|tag| tag.to_string())
63			.unwrap_or_default(),
64		pushkey: pusher.ids.pushkey,
65		enabled: true,
66		device_id,
67	}
68}
69
70/// Recovers the kind string and data object from the pusher kind's wire form.
71fn split_kind(kind: &PusherKind) -> (String, Option<JsonObject>) {
72	let Ok(serde_json::Value::Object(mut map)) = serde_json::to_value(kind) else {
73		return (String::new(), None);
74	};
75
76	let kind = match map.remove("kind") {
77		| Some(serde_json::Value::String(kind)) => kind,
78		| _ => String::new(),
79	};
80
81	let data = match map.remove("data") {
82		| Some(serde_json::Value::Object(data)) => Some(data),
83		| _ => None,
84	};
85
86	(kind, data)
87}
88
89#[cfg(test)]
90mod tests {
91	use ruma::{
92		api::client::push::{Pusher, PusherIds, PusherKind},
93		push::HttpPusherData,
94	};
95
96	use super::{admin_pusher, split_kind};
97
98	fn http_pusher() -> Pusher {
99		Pusher {
100			ids: PusherIds::new("pushkey123".to_owned(), "im.vector.app".to_owned()),
101			kind: PusherKind::Http(HttpPusherData::new("https://push.example".to_owned())),
102			app_display_name: "Element".into(),
103			device_display_name: "Phone".into(),
104			profile_tag: None,
105			lang: "en".into(),
106		}
107	}
108
109	#[test]
110	fn split_kind_recovers_kind_and_data() {
111		let (kind, data) =
112			split_kind(&PusherKind::Http(HttpPusherData::new("https://push.example".to_owned())));
113
114		assert_eq!(kind, "http");
115		let data = data.expect("http pusher carries a data object");
116		assert_eq!(data.get("url").and_then(|v| v.as_str()), Some("https://push.example"));
117	}
118
119	#[test]
120	fn admin_pusher_maps_required_fields() {
121		let mapped = admin_pusher(http_pusher(), None);
122
123		assert_eq!(mapped.app_id, "im.vector.app");
124		assert_eq!(mapped.pushkey, "pushkey123");
125		assert_eq!(mapped.app_display_name, "Element");
126		assert_eq!(mapped.device_display_name, "Phone");
127		assert_eq!(mapped.kind, "http");
128		assert_eq!(mapped.lang.as_deref(), Some("en"));
129		assert_eq!(mapped.profile_tag, "");
130		assert!(mapped.enabled);
131		assert!(mapped.device_id.is_none());
132		assert!(mapped.data.is_some());
133	}
134}