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
40type RelatedEvents = BTreeMap<String, FlattenedJson>;
42
43pub struct Evaluate<'a, 'b> {
45 pub user: &'b UserId,
47 pub ruleset: &'a Ruleset,
49 pub power_levels: Option<&'b RoomPowerLevels>,
51 pub pdu: &'b Raw<AnySyncTimelineEvent>,
53 pub room_id: &'b RoomId,
55 pub related_events: Option<&'b Arc<RelatedEvents>>,
57}
58
59const IN_REPLY_TO: &str = "m.in_reply_to";
61
62static 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#[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 .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}