Skip to main content

tuwunel_service/pusher/
send.rs

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/// Send an account-wide counts-only notification to a push gateway.
85///
86/// Enabled HTTP pushers emit the request, including an explicit zero. The
87/// delivery is skipped only when the gateway is known to hold the current
88/// total already; an unknown gateway is always sent to.
89#[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	// TODO: email
124	match &pusher.kind {
125		| PusherKind::Http(http) =>
126			self.send_http_event_notice(user_id, pusher, http, tweaks, event)
127				.await,
128		// TODO: Handle email
129		//PusherKind::Email(_) => Ok(()),
130		| _ => 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	// TODO (timo): can pusher/devices have conflicting formats
146	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/// Deliver one notification to the pusher's gateway and honor its verdict.
228///
229/// A pushkey the gateway names in `rejected` has its pusher removed. `unread`
230/// names the counts value on the wire; it is recorded as delivered only after
231/// the gateway accepts, so a failed or rejected send leaves the next refresh
232/// unconditional.
233#[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}