1use futures::{
2 FutureExt,
3 future::{join, join4},
4};
5use ruma::{
6 UInt, UserId,
7 api::{
8 client::push::{Pusher, PusherKind},
9 push_gateway::send_event_notification::v1::{
10 Device, Notification, NotificationCounts, NotificationPriority, Request,
11 },
12 },
13 events::TimelineEventType,
14 push::{Action, HighlightTweakValue, HttpPusherData, PushFormat, Ruleset, Tweak},
15};
16use serde_json::Value;
17use tuwunel_core::{Err, Result, err, implement, matrix::Event, utils::BoolExt, warn};
18use url::Url;
19
20use super::Evaluate;
21
22#[implement(super::Service)]
23#[tracing::instrument(level = "debug", skip_all)]
24pub async fn send_push_notice<E>(
25 &self,
26 user_id: &UserId,
27 pusher: &Pusher,
28 ruleset: &Ruleset,
29 event: &E,
30) -> Result
31where
32 E: Event,
33{
34 let mut notify = None;
35 let mut tweaks = Vec::new();
36
37 let power_levels = self
38 .services
39 .state_accessor
40 .get_power_levels(event.room_id())
41 .map(Result::ok);
42
43 let (power_levels, related_events) = join(power_levels, self.related_events(event)).await;
44
45 let serialized = event.to_format();
46 let actions = self
47 .get_actions(Evaluate {
48 user: user_id,
49 ruleset,
50 power_levels: power_levels.as_ref(),
51 pdu: &serialized,
52 room_id: event.room_id(),
53 related_events: related_events.as_ref(),
54 })
55 .await;
56
57 for action in actions {
58 let n = match action {
59 | Action::Notify => true,
60 | Action::SetTweak(tweak) => {
61 tweaks.push(tweak.clone());
62 continue;
63 },
64 | _ => false,
65 };
66
67 if notify.is_some() {
68 return Err!(Request(BadJson(
69 r#"Malformed pushrule contains more than one of these actions: ["dont_notify", "notify", "coalesce"]"#
70 )));
71 }
72
73 notify = Some(n);
74 }
75
76 if notify == Some(true) || self.services.config.push_everything {
77 self.send_notice(user_id, pusher, tweaks, event)
78 .await?;
79 }
80
81 Ok(())
82}
83
84#[implement(super::Service)]
90#[tracing::instrument(level = "debug", skip_all)]
91pub async fn send_badge_notice(&self, user_id: &UserId, pusher: &Pusher) -> Result {
92 let PusherKind::Http(http) = &pusher.kind else {
93 return Ok(());
94 };
95
96 if badge_count_disabled(http) {
97 return Ok(());
98 }
99
100 let unread = UInt::new(self.global_notification_count(user_id).await).unwrap_or(UInt::MAX);
101
102 if self.sent_badge(user_id, &pusher.ids.pushkey) == Some(unread) {
103 return Ok(());
104 }
105
106 let device = self.prepare_http_pusher(pusher, http)?;
107 let mut notify = Notification::new(vec![device]);
108 notify.counts = NotificationCounts::new_explicit(Some(unread), None);
109
110 self.send_http_notice(user_id, pusher, http, notify, Some(unread))
111 .await
112}
113
114#[implement(super::Service)]
115#[tracing::instrument(level = "debug", skip_all)]
116async fn send_notice<Pdu: Event>(
117 &self,
118 user_id: &UserId,
119 pusher: &Pusher,
120 tweaks: Vec<Tweak>,
121 event: &Pdu,
122) -> Result {
123 match &pusher.kind {
125 | PusherKind::Http(http) =>
126 self.send_http_event_notice(user_id, pusher, http, tweaks, event)
127 .await,
128 | _ => Ok(()),
131 }
132}
133
134#[implement(super::Service)]
135async fn send_http_event_notice<Pdu: Event>(
136 &self,
137 user_id: &UserId,
138 pusher: &Pusher,
139 http: &HttpPusherData,
140 tweaks: Vec<Tweak>,
141 event: &Pdu,
142) -> Result {
143 let mut device = self.prepare_http_pusher(pusher, http)?;
144
145 let event_id_only = http.format == Some(PushFormat::EventIdOnly);
147
148 if !event_id_only {
149 device.tweaks.clone_from(&tweaks);
150 }
151
152 let mut notify = Notification::new(vec![device]);
153
154 notify.event_id = Some(event.event_id().to_owned());
155 notify.room_id = Some(event.room_id().to_owned());
156
157 let unread = badge_count_disabled(http)
158 .is_false()
159 .then_async(async || {
160 UInt::new(self.global_notification_count(user_id).await).unwrap_or(UInt::MAX)
161 });
162
163 let unread = if !event_id_only {
164 if *event.kind() == TimelineEventType::RoomEncrypted
165 || tweaks.iter().any(|t| {
166 matches!(t, Tweak::Highlight(HighlightTweakValue::Yes) | Tweak::Sound(_))
167 }) {
168 notify.prio = NotificationPriority::High;
169 } else {
170 notify.prio = NotificationPriority::Low;
171 }
172 notify.sender = Some(event.sender().to_owned());
173 notify.event_type = Some(event.kind().to_owned());
174 notify.content = serde_json::value::to_raw_value(event.content()).ok();
175
176 if *event.kind() == TimelineEventType::RoomMember {
177 notify.user_is_target = event.state_key() == Some(event.sender().as_str());
178 }
179
180 let (display_name, room_name, room_alias, unread) = join4(
181 self.services.profile.displayname(event.sender()),
182 self.services
183 .state_accessor
184 .get_name(event.room_id()),
185 self.services
186 .state_accessor
187 .get_canonical_alias(event.room_id()),
188 unread,
189 )
190 .await;
191
192 notify.sender_display_name = display_name.ok();
193 notify.room_name = room_name.ok();
194 notify.room_alias = room_alias.ok();
195
196 unread
197 } else {
198 unread.await
199 };
200
201 if let Some(unread) = unread {
202 notify.counts = NotificationCounts::new_explicit(Some(unread), None);
203 }
204
205 self.send_http_notice(user_id, pusher, http, notify, unread)
206 .await
207}
208
209#[implement(super::Service)]
210fn prepare_http_pusher(&self, pusher: &Pusher, http: &HttpPusherData) -> Result<Device> {
211 let address = &http.url;
212 let url = Url::parse(address).map_err(|e| {
213 err!(Request(InvalidParam(
214 warn!(url = %address, error = %e, "HTTP pusher URL is not a valid URL")
215 )))
216 })?;
217
218 self.check_http_pusher_url(&url)?;
219
220 let mut device = Device::new(pusher.ids.app_id.clone(), pusher.ids.pushkey.clone());
221 device.data.data.clone_from(&http.data);
222 device.data.format.clone_from(&http.format);
223
224 Ok(device)
225}
226
227#[implement(super::Service)]
234#[tracing::instrument(level = "debug", skip_all)]
235async fn send_http_notice(
236 &self,
237 user_id: &UserId,
238 pusher: &Pusher,
239 http: &HttpPusherData,
240 notify: Notification,
241 unread: Option<UInt>,
242) -> Result {
243 let response = self
244 .send_request(&http.url, Request::new(notify))
245 .await?;
246
247 let pushkey = &pusher.ids.pushkey;
248
249 if response.rejected.contains(pushkey) {
250 warn!(url = %http.url, %pushkey, "Push gateway rejected the pushkey; removing pusher");
251 self.delete_pusher(user_id, pushkey).await;
252
253 return Ok(());
254 }
255
256 if let Some(unread) = unread {
257 self.record_sent_badge(user_id, pushkey, unread);
258 }
259
260 Ok(())
261}
262
263fn badge_count_disabled(http: &HttpPusherData) -> bool {
264 ["org.matrix.msc4076.disable_badge_count", "disable_badge_count"]
265 .iter()
266 .any(|key| http.data.get(*key).and_then(Value::as_bool) == Some(true))
267}