Skip to main content

tuwunel_service/pusher/
append.rs

1use std::{collections::HashSet, sync::Arc};
2
3use futures::{
4	FutureExt, StreamExt,
5	future::{join, join4},
6};
7use ruma::{
8	EventId, RoomId, UserId,
9	api::client::push::ProfileTag,
10	events::{
11		AnySyncTimelineEvent, GlobalAccountDataEventType, TimelineEventType,
12		push_rules::PushRulesEvent, room::power_levels::RoomPowerLevels,
13	},
14	push::{Action, Actions, HighlightTweakValue, Ruleset, Tweak},
15	serde::Raw,
16};
17use serde::{Deserialize, Serialize};
18use tuwunel_core::{
19	Result, implement,
20	matrix::{
21		event::Event,
22		pdu::{Count, Pdu, PduId, RawPduId},
23	},
24	utils::{BoolExt, ReadyExt, future::TryExtExt, option::OptionExt, time::now_millis},
25};
26use tuwunel_database::{Deserialized, Json, Map};
27
28use super::{Evaluate, RelatedEvents};
29use crate::rooms::short::ShortRoomId;
30
31/// Compact metadata stored for each notified event.
32///
33/// The database key supplies the user and PDU count. The stored `ShortRoomId`
34/// combines with that count to reconstruct the event's `PduId`.
35#[derive(Clone, Debug, Deserialize, Serialize)]
36pub struct Notified {
37	/// Milliseconds time at which the event notification was sent.
38	pub ts: u64,
39
40	/// ShortRoomId
41	pub sroomid: ShortRoomId,
42
43	/// The profile tag of the rule that matched this event.
44	#[serde(skip_serializing_if = "Option::is_none")]
45	pub tag: Option<ProfileTag>,
46
47	/// Actions vector
48	pub actions: Actions,
49}
50
51/// The values of one appended event shared by every recipient of it.
52///
53/// Each is resolved once per event and read once per recipient, so grouping
54/// them keeps the per-recipient call from growing an argument per lookup.
55#[derive(Clone, Copy)]
56struct Appended<'a> {
57	pdu_id: &'a RawPduId,
58	pdu: &'a Pdu,
59	power_levels: Option<&'a RoomPowerLevels>,
60	serialized: &'a Raw<AnySyncTimelineEvent>,
61	thread_root: Option<&'a EventId>,
62	related_events: Option<&'a Arc<RelatedEvents>>,
63}
64
65/// Called by timeline append_pdu.
66#[implement(super::Service)]
67#[tracing::instrument(name = "append", level = "debug", skip_all)]
68pub(crate) async fn append_pdu(&self, pdu_id: RawPduId, pdu: &Pdu) -> Result {
69	let push_target = self
70		.services
71		.state_cache
72		.active_local_users_in_room(pdu.room_id())
73		.map(ToOwned::to_owned)
74		.ready_filter(|user| *user != pdu.sender())
75		.filter_map(async |recipient_user| {
76			self.services
77				.users
78				.user_is_ignored(pdu.sender(), &recipient_user)
79				.await
80				.is_false()
81				.then_some(recipient_user)
82		})
83		.collect::<HashSet<_>>();
84
85	let power_levels = self
86		.services
87		.state_accessor
88		.get_power_levels(pdu.room_id())
89		.ok();
90
91	let (mut push_target, power_levels) = join(push_target, power_levels).boxed().await;
92
93	if *pdu.kind() == TimelineEventType::RoomMember
94		&& let Some(Ok(target_user_id)) = pdu.state_key().map(UserId::parse)
95		&& self
96			.services
97			.users
98			.is_active_local(&target_user_id)
99			.await
100	{
101		push_target.insert(target_user_id);
102	}
103
104	if push_target.is_empty() {
105		return Ok(());
106	}
107
108	let serialized = pdu.to_format();
109	let (thread_root, related_events) =
110		join(self.services.threads.get_thread_id(pdu), self.related_events(pdu)).await;
111
112	let appended = Appended {
113		pdu_id: &pdu_id,
114		pdu,
115		power_levels: power_levels.as_ref(),
116		serialized: &serialized,
117		thread_root: thread_root.as_deref(),
118		related_events: related_events.as_ref(),
119	};
120
121	let _cork = self.db.db.cork();
122	for user in &push_target {
123		self.append_pdu_for_user(user, appended).await;
124	}
125
126	Ok(())
127}
128
129#[implement(super::Service)]
130async fn append_pdu_for_user(
131	&self,
132	user: &UserId,
133	Appended {
134		pdu_id,
135		pdu,
136		power_levels,
137		serialized,
138		thread_root,
139		related_events,
140	}: Appended<'_>,
141) {
142	let rules_for_user = self
143		.services
144		.account_data
145		.get_global(user, GlobalAccountDataEventType::PushRules)
146		.await
147		.map_or_else(|_| Ruleset::server_default(user), |ev: PushRulesEvent| ev.content.global);
148
149	let actions = self
150		.get_actions(Evaluate {
151			user,
152			ruleset: &rules_for_user,
153			power_levels,
154			pdu: serialized,
155			room_id: pdu.room_id(),
156			related_events,
157		})
158		.await;
159
160	let notify = actions
161		.iter()
162		.any(|action| matches!(action, Action::Notify));
163
164	let highlight = actions.iter().any(|action| {
165		matches!(action, Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
166	});
167
168	// Mutually-exclusive partition: each notify (and each highlight)
169	// lands in either the room-level or thread bucket, never both.
170	let main_notify = (notify && thread_root.is_none())
171		.then_async(|| self.increment_notificationcount(pdu.room_id(), user));
172
173	let main_highlight = (highlight && thread_root.is_none())
174		.then_async(|| self.increment_highlightcount(pdu.room_id(), user));
175
176	let thread_notify = thread_root
177		.filter(|_| notify)
178		.map_async(|root| self.increment_thread_notificationcount(pdu.room_id(), user, root));
179
180	let thread_highlight = thread_root
181		.filter(|_| highlight)
182		.map_async(|root| self.increment_thread_highlightcount(pdu.room_id(), user, root));
183
184	join4(main_notify, thread_notify, main_highlight, thread_highlight).await;
185
186	if notify || highlight {
187		let id: PduId = (*pdu_id).into();
188		let notified = Notified {
189			ts: now_millis(),
190			sroomid: id.shortroomid,
191			tag: None,
192			actions: actions.into(),
193		};
194
195		if matches!(id.count, Count::Normal(_)) {
196			self.db
197				.useridcount_notification
198				.put((user, id.count.into_unsigned()), Json(notified));
199		}
200	}
201
202	if notify || highlight || self.services.config.push_everything {
203		self.get_pushkeys(user)
204			.map(ToOwned::to_owned)
205			.ready_for_each(|push_key| {
206				self.services
207					.sending
208					.send_pdu_push(pdu_id, user, push_key)
209					.expect("TODO: replace with future");
210			})
211			.await;
212	}
213}
214
215#[implement(super::Service)]
216async fn increment_notificationcount(&self, room_id: &RoomId, user_id: &UserId) {
217	let db = &self.db.userroomid_notificationcount;
218	let key = (room_id.to_owned(), user_id.to_owned());
219	let _lock = self.notification_increment_mutex.lock(&key).await;
220
221	increment(db, (user_id, room_id)).await;
222}
223
224#[implement(super::Service)]
225async fn increment_highlightcount(&self, room_id: &RoomId, user_id: &UserId) {
226	let db = &self.db.userroomid_highlightcount;
227	let key = (room_id.to_owned(), user_id.to_owned());
228	let _lock = self.highlight_increment_mutex.lock(&key).await;
229
230	increment(db, (user_id, room_id)).await;
231}
232
233#[implement(super::Service)]
234async fn increment_thread_notificationcount(
235	&self,
236	room_id: &RoomId,
237	user_id: &UserId,
238	thread_root: &EventId,
239) {
240	let db = &self.db.userroomid_notificationcount;
241	let key = (room_id.to_owned(), user_id.to_owned());
242	let _lock = self.notification_increment_mutex.lock(&key).await;
243
244	increment_thread(db, (user_id, room_id, thread_root)).await;
245}
246
247#[implement(super::Service)]
248async fn increment_thread_highlightcount(
249	&self,
250	room_id: &RoomId,
251	user_id: &UserId,
252	thread_root: &EventId,
253) {
254	let db = &self.db.userroomid_highlightcount;
255	let key = (room_id.to_owned(), user_id.to_owned());
256	let _lock = self.highlight_increment_mutex.lock(&key).await;
257
258	increment_thread(db, (user_id, room_id, thread_root)).await;
259}
260
261async fn increment(db: &Arc<Map>, key: (&UserId, &RoomId)) {
262	let old: u64 = db.qry(&key).await.deserialized().unwrap_or(0);
263	let new = old.saturating_add(1);
264	db.put(key, new);
265}
266
267async fn increment_thread(db: &Arc<Map>, key: (&UserId, &RoomId, &EventId)) {
268	let old: u64 = db.qry(&key).await.deserialized().unwrap_or(0);
269	let new = old.saturating_add(1);
270	db.put(key, new);
271}