Skip to main content

tuwunel_service/pusher/
mod.rs

1mod append;
2mod badge;
3mod notification;
4mod request;
5mod send;
6mod suppressed;
7#[cfg(test)]
8mod tests;
9
10use std::{
11	collections::BTreeMap,
12	sync::{Arc, LazyLock},
13};
14
15use futures::{Stream, StreamExt, TryFutureExt, future::join};
16use ruma::{
17	DeviceId, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
18	api::client::push::{Pusher, PusherKind, set_pusher::v3::PusherAction},
19	events::{AnySyncTimelineEvent, room::power_levels::RoomPowerLevels},
20	push::{Action, FlattenedJson, PushConditionPowerLevelsCtx, PushConditionRoomCtx, Ruleset},
21	serde::Raw,
22	uint,
23};
24use serde::Deserialize;
25use tuwunel_core::{
26	Err, Result, err, implement,
27	matrix::Event,
28	utils::{
29		MutexMap,
30		future::TryExtExt,
31		stream::{BroadbandExt, IterStream, ReadyExt, TryIgnore, WidebandExt},
32	},
33};
34use tuwunel_database::{Database, Deserialized, Ignore, Interfix, Json, Map};
35use url::Url;
36
37pub use self::append::Notified;
38use self::badge::SentBadges;
39
40/// The events an event relates to, keyed by relation type, for MSC3664.
41type RelatedEvents = BTreeMap<String, FlattenedJson>;
42
43/// The inputs for evaluating one user's push rules against one event.
44pub struct Evaluate<'a, 'b> {
45	/// The user whose rules are evaluated, and who is notified.
46	pub user: &'b UserId,
47	/// The user's ruleset, which outlives the borrowed actions returned.
48	pub ruleset: &'a Ruleset,
49	/// The room's power levels, without which some conditions never match.
50	pub power_levels: Option<&'b RoomPowerLevels>,
51	/// The event being evaluated, in the format the conditions flatten.
52	pub pdu: &'b Raw<AnySyncTimelineEvent>,
53	/// The room the event was sent to.
54	pub room_id: &'b RoomId,
55	/// The events it relates to, or `None` where they are not resolved at all.
56	pub related_events: Option<&'b Arc<RelatedEvents>>,
57}
58
59/// MSC3664 matches a reply as if it carried a relation of this type.
60const IN_REPLY_TO: &str = "m.in_reply_to";
61
62/// Shared by every event that resolves no relations, which is every event
63/// while MSC3664 evaluation is disabled.
64static NO_RELATED_EVENTS: LazyLock<Arc<RelatedEvents>> = LazyLock::new(Arc::default);
65
66#[derive(Deserialize)]
67struct ExtractRelatesTo {
68	#[serde(rename = "m.relates_to")]
69	relates_to: RelatesTo,
70}
71
72#[derive(Deserialize)]
73struct RelatesTo {
74	rel_type: Option<String>,
75
76	event_id: Option<OwnedEventId>,
77
78	#[serde(rename = "m.in_reply_to")]
79	in_reply_to: Option<InReplyTo>,
80}
81
82#[derive(Deserialize)]
83struct InReplyTo {
84	event_id: OwnedEventId,
85}
86
87pub struct Service {
88	services: Arc<crate::services::OnceServices>,
89	notification_increment_mutex: MutexMap<(OwnedRoomId, OwnedUserId), ()>,
90	highlight_increment_mutex: MutexMap<(OwnedRoomId, OwnedUserId), ()>,
91	db: Data,
92	suppressed: suppressed::SuppressedQueue,
93	sent_badges: SentBadges,
94}
95
96struct Data {
97	db: Arc<Database>,
98	senderkey_pusher: Arc<Map>,
99	pushkey_deviceid: Arc<Map>,
100	useridcount_notification: Arc<Map>,
101	userroomid_highlightcount: Arc<Map>,
102	userroomid_notificationcount: Arc<Map>,
103	roomuserid_lastnotificationread: Arc<Map>,
104}
105
106impl crate::Service for Service {
107	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
108		Ok(Arc::new(Self {
109			services: args.services.clone(),
110			notification_increment_mutex: MutexMap::new(),
111			highlight_increment_mutex: MutexMap::new(),
112			db: Data {
113				db: args.db.clone(),
114				senderkey_pusher: args.db["senderkey_pusher"].clone(),
115				pushkey_deviceid: args.db["pushkey_deviceid"].clone(),
116				useridcount_notification: args.db["useridcount_notification"].clone(),
117				userroomid_highlightcount: args.db["userroomid_highlightcount"].clone(),
118				userroomid_notificationcount: args.db["userroomid_notificationcount"].clone(),
119				roomuserid_lastnotificationread: args.db["roomuserid_lastnotificationread"]
120					.clone(),
121			},
122			suppressed: suppressed::SuppressedQueue::default(),
123			sent_badges: SentBadges::default(),
124		}))
125	}
126
127	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
128}
129
130#[implement(Service)]
131pub async fn set_pusher(
132	&self,
133	sender: &UserId,
134	sender_device: &DeviceId,
135	pusher: &PusherAction,
136) -> Result {
137	match pusher {
138		| PusherAction::Delete(ids) =>
139			self.set_pusher_delete(sender, ids.pushkey.as_str())
140				.await,
141		| PusherAction::Post(data) =>
142			self.set_pusher_post(sender, sender_device, pusher, &data.pusher)?,
143	}
144
145	Ok(())
146}
147
148#[implement(Service)]
149async fn set_pusher_delete(&self, sender: &UserId, pushkey: &str) {
150	self.delete_pusher(sender, pushkey).await;
151}
152
153#[implement(Service)]
154fn set_pusher_post(
155	&self,
156	sender: &UserId,
157	sender_device: &DeviceId,
158	action: &PusherAction,
159	pusher: &Pusher,
160) -> Result {
161	let pushkey = pusher.ids.pushkey.as_str();
162
163	if pushkey.len() > 512 {
164		return Err!(Request(InvalidParam("Push key length cannot be greater than 512 bytes.")));
165	}
166
167	if pusher.ids.app_id.as_str().len() > 64 {
168		return Err!(Request(InvalidParam("App ID length cannot be greater than 64 bytes.")));
169	}
170
171	if let PusherKind::Http(http) = &pusher.kind {
172		let url = &http.url;
173		let url = Url::parse(&http.url).map_err(|e| {
174			err!(Request(InvalidParam(warn!(%url, "HTTP pusher URL is not a valid URL: {e}"))))
175		})?;
176
177		self.check_http_pusher_url(&url)?;
178	}
179
180	let key = (sender, pushkey);
181	self.db.senderkey_pusher.put(key, Json(action));
182	self.db
183		.pushkey_deviceid
184		.insert(pushkey, sender_device);
185
186	self.forget_sent_badge(sender, pushkey);
187
188	Ok(())
189}
190
191#[implement(Service)]
192fn check_http_pusher_url(&self, url: &Url) -> Result {
193	if ["http", "https"]
194		.iter()
195		.all(|&scheme| !scheme.eq_ignore_ascii_case(url.scheme()))
196	{
197		return Err!(Request(InvalidParam(
198			warn!(%url, "HTTP pusher URL is not a valid HTTP/HTTPS URL")
199		)));
200	}
201
202	if self.services.client.proxy.resolver_alias(url) {
203		return Err!(Request(InvalidParam(
204			warn!(%url, "HTTP pusher URL is a forbidden proxy endpoint")
205		)));
206	}
207
208	if !self.services.client.valid_cidr_range_url(url) {
209		return Err!(Request(InvalidParam(
210			warn!(%url, "HTTP pusher URL is a forbidden remote address")
211		)));
212	}
213
214	Ok(())
215}
216
217#[implement(Service)]
218pub async fn delete_pusher(&self, sender: &UserId, pushkey: &str) {
219	let key = (sender, pushkey);
220	self.db.senderkey_pusher.del(key);
221	self.db.pushkey_deviceid.remove(pushkey);
222	self.clear_suppressed_pushkey(sender, pushkey);
223	self.forget_sent_badge(sender, pushkey);
224
225	self.services
226		.sending
227		.cleanup_events(None, Some(sender), Some(pushkey))
228		.await
229		.ok();
230}
231
232#[implement(Service)]
233pub async fn get_device_pushkeys(&self, sender: &UserId, device_id: &DeviceId) -> Vec<String> {
234	self.get_pushkeys(sender)
235		.map(ToOwned::to_owned)
236		.broad_filter_map(async |pushkey| {
237			self.get_pusher_device(&pushkey)
238				.await
239				.ok()
240				.as_ref()
241				.is_some_and(|pusher_device| pusher_device == device_id)
242				.then_some(pushkey)
243		})
244		.collect()
245		.await
246}
247
248#[implement(Service)]
249pub async fn get_pusher_device(&self, pushkey: &str) -> Result<OwnedDeviceId> {
250	self.db
251		.pushkey_deviceid
252		.get(pushkey)
253		.await
254		.deserialized()
255}
256
257#[implement(Service)]
258pub async fn get_pusher(&self, sender: &UserId, pushkey: &str) -> Result<Pusher> {
259	let senderkey = (sender, pushkey);
260	self.db
261		.senderkey_pusher
262		.qry(&senderkey)
263		.await
264		.deserialized()
265}
266
267#[implement(Service)]
268pub async fn get_pushers(&self, sender: &UserId) -> Vec<Pusher> {
269	let prefix = (sender, Interfix);
270	self.db
271		.senderkey_pusher
272		.stream_prefix(&prefix)
273		.ignore_err()
274		.map(|(_, pusher): (Ignore, Pusher)| pusher)
275		.collect()
276		.await
277}
278
279#[implement(Service)]
280pub fn get_pushkeys<'a>(&'a self, sender: &'a UserId) -> impl Stream<Item = &str> + Send + 'a {
281	let prefix = (sender, Interfix);
282	self.db
283		.senderkey_pusher
284		.keys_prefix(&prefix)
285		.ignore_err()
286		.map(|(_, pushkey): (Ignore, &str)| pushkey)
287}
288
289#[implement(Service)]
290#[tracing::instrument(level = "debug", skip_all)]
291pub fn get_notifications<'a>(
292	&'a self,
293	sender: &'a UserId,
294	from: Option<u64>,
295) -> impl Stream<Item = (u64, Notified)> + Send + 'a {
296	let from = from
297		.map(|from| from.saturating_sub(1))
298		.unwrap_or(u64::MAX);
299
300	self.db
301		.useridcount_notification
302		.rev_stream_from(&(sender, from))
303		.ignore_err()
304		.map(|item: ((&UserId, u64), _)| (item.0, item.1))
305		.ready_take_while(move |((user_id, _count), _)| sender == *user_id)
306		.map(|((_, count), notified)| (count, notified))
307}
308
309#[implement(Service)]
310#[tracing::instrument(level = "debug", skip_all)]
311pub async fn get_actions<'a>(
312	&self,
313	Evaluate {
314		user,
315		ruleset,
316		power_levels,
317		pdu,
318		room_id,
319		related_events,
320	}: Evaluate<'a, '_>,
321) -> &'a [Action] {
322	let user_display_name = self
323		.services
324		.profile
325		.displayname(user)
326		.unwrap_or_else(|_| user.localpart().to_owned());
327
328	let room_joined_count = self
329		.services
330		.state_cache
331		.room_joined_count(room_id)
332		.map_ok(TryInto::try_into)
333		.map_ok(|res| res.unwrap_or_else(|_| uint!(1)))
334		.unwrap_or_default();
335
336	let (room_joined_count, user_display_name) = join(room_joined_count, user_display_name).await;
337
338	let power_levels = power_levels.map(|power_levels| PushConditionPowerLevelsCtx {
339		users: power_levels.users.clone(),
340		users_default: power_levels.users_default,
341		notifications: power_levels.notifications.clone(),
342		rules: power_levels.rules.clone(),
343	});
344
345	let ctx = PushConditionRoomCtx::new(
346		room_id.to_owned(),
347		room_joined_count,
348		user.to_owned(),
349		user_display_name,
350	);
351
352	let ctx = match related_events {
353		| Some(related_events) => ctx.with_related_events(related_events.clone()),
354		| None => ctx,
355	};
356
357	let ctx = match power_levels {
358		| Some(pl) => ctx.with_power_levels(pl),
359		| None => ctx,
360	};
361
362	ruleset.get_actions(pdu, &ctx).await
363}
364
365/// Resolve the events an event relates to, for the MSC3664 push condition.
366///
367/// `None` while `msc3664_related_event_match` is disabled, which leaves every
368/// MSC3664 condition unable to match. The relations do not vary by recipient,
369/// so the append path resolves them once for the whole room; the push-notice
370/// path resolves them per notice, since it holds one event at a time.
371#[implement(Service)]
372#[tracing::instrument(level = "debug", skip_all)]
373pub async fn related_events<E: Event>(&self, event: &E) -> Option<Arc<RelatedEvents>> {
374	let config = &self.services.server.config;
375
376	if !config.msc3664_related_event_match {
377		return None;
378	}
379
380	let Ok(ExtractRelatesTo { relates_to }) = event.get_content() else {
381		return Some(NO_RELATED_EVENTS.clone());
382	};
383
384	let reply = relates_to
385		.in_reply_to
386		.map(|reply| (IN_REPLY_TO.to_owned(), reply.event_id));
387
388	let related = relates_to
389		.rel_type
390		.zip(relates_to.event_id)
391		.into_iter()
392		.chain(reply)
393		.stream()
394		.wide_filter_map(async |(rel_type, event_id)| {
395			let related = self
396				.services
397				.timeline
398				.get_pdu(&event_id)
399				.await
400				.ok()
401				// A relation crossing rooms is not one the sender could have made, and
402				// following it would expose an event the recipient may not be in a room for.
403				.filter(|related| related.room_id() == event.room_id())?;
404
405			let related: Raw<AnySyncTimelineEvent> = related.to_format();
406
407			Some((rel_type, FlattenedJson::from_raw(&related)))
408		})
409		.collect()
410		.await;
411
412	Some(Arc::new(related))
413}