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