feat(presence,push) optionally suppress push notifications for active users
This commit is contained in:
committed by
Jason Volk
parent
1be7fd9247
commit
b5a9884194
@@ -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
|
||||
|
||||
@@ -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) won’t 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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) won’t 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
|
||||
|
||||
Reference in New Issue
Block a user