tuwunel_api/client/admin/users/
pushers.rs1use 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
16pub(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
49fn 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
70fn 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}