feat(presence,push) optionally suppress push notifications for active users

This commit is contained in:
tototomate123
2025-09-02 14:26:12 +02:00
committed by Jason Volk
parent 1be7fd9247
commit b5a9884194
5 changed files with 67 additions and 4 deletions

View File

@@ -133,6 +133,9 @@ pub(crate) async fn sync_events_route(
.await
.log_err()
.ok();
// Record that this user was actively syncing now (for push suppression heuristic)
services.presence.note_sync(sender_user).await;
}
let mut since = body

View File

@@ -1288,6 +1288,18 @@ pub struct Config {
#[serde(default = "true_fn")]
pub presence_timeout_remote_users: bool,
/// Suppresses push notifications for users marked as active.
///
/// when enabled, users with `Online` presence and recent activity
/// (based on presence state and sync activity) wont receive push
/// notifications, reducing duplicate alerts while they're active
/// elsewhere.
///
/// Disabled by default to preserve legacy behavior.
#[serde(default)]
pub suppress_push_when_active: bool,
/// Allow receiving incoming read receipts from remote servers.
#[serde(default = "true_fn")]
pub allow_incoming_read_receipts: bool,

View File

@@ -1,7 +1,8 @@
mod data;
mod presence;
use std::{sync::Arc, time::Duration};
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::RwLock;
use async_trait::async_trait;
use futures::{Stream, StreamExt, TryFutureExt, stream::FuturesUnordered};
@@ -19,6 +20,7 @@ pub struct Service {
offline_timeout: u64,
db: Data,
services: Arc<crate::services::OnceServices>,
last_sync_seen: RwLock<HashMap<OwnedUserId, u64>>,
}
type TimerType = (OwnedUserId, Duration);
@@ -36,6 +38,7 @@ impl crate::Service for Service {
offline_timeout: checked!(offline_timeout_s * 1_000)?,
db: Data::new(&args),
services: args.services.clone(),
last_sync_seen: RwLock::new(HashMap::new()),
}))
}
@@ -88,6 +91,21 @@ impl crate::Service for Service {
}
impl Service {
/// record that a user has just successfully completed a /sync (or equivalent activity)
pub async fn note_sync(&self, user_id: &UserId) {
let now = tuwunel_core::utils::millis_since_unix_epoch();
self.last_sync_seen.write().await.insert(user_id.to_owned(), now);
}
/// Returns milliseconds since last observed sync for user (if any)
pub async fn last_sync_gap_ms(&self, user_id: &UserId) -> Option<u64> {
let now = tuwunel_core::utils::millis_since_unix_epoch();
self.last_sync_seen
.read()
.await
.get(user_id)
.map(|ts| now.saturating_sub(*ts))
}
/// Returns the latest presence event for the given user.
pub async fn get_presence(&self, user_id: &UserId) -> Result<PresenceEvent> {
self.db

View File

@@ -19,14 +19,13 @@ use ruma::{
MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName,
UInt,
api::{
appservice::event::push_events::v1::EphemeralData,
federation::transactions::{
appservice::event::push_events::v1::EphemeralData, federation::transactions::{
edu::{
DeviceListUpdateContent, Edu, PresenceContent, PresenceUpdate, ReceiptContent,
ReceiptData, ReceiptMap,
},
send_transaction_message,
},
}
},
device_id,
events::{
@@ -834,6 +833,26 @@ impl Service {
continue;
}
// optional suppression: heuristic combining presence age and recent sync activity.
if self.services.server.config.suppress_push_when_active {
if let Ok(presence) = self.services.presence.get_presence(&user_id).await {
let is_online = presence.content.presence == ruma::presence::PresenceState::Online;
let presence_age_ms = presence
.content
.last_active_ago
.map(u64::from)
.unwrap_or(u64::MAX);
let sync_gap_ms = self.services.presence.last_sync_gap_ms(&user_id).await;
let considered_active = is_online
&& presence_age_ms < 65_000
&& sync_gap_ms.is_some_and(|gap| gap < 32_000);
if considered_active {
trace!(?user_id, presence_age_ms, sync_gap_ms, "suppressing push: active heuristic");
continue;
}
}
}
let rules_for_user = self
.services
.account_data

View File

@@ -1094,6 +1094,17 @@
#
#presence_timeout_remote_users = true
# Suppresses push notifications for users marked as active.
#
# When enabled, users with `Online` presence and recent activity
# (based on presence state and sync activity) wont receive push
# notifications, reducing duplicate alerts while they're active
# elsewhere.
#
# Disabled by default to preserve legacy behavior.
#
#suppress_push_when_active = false
# Allow receiving incoming read receipts from remote servers.
#
#allow_incoming_read_receipts = true