use std::collections::HashMap;
use std::time::{Duration, Instant};
const LEFT_AFTER: Duration = Duration::from_secs(20);
const HIDDEN_LEFT_AFTER: Duration = Duration::from_secs(90);
const IDLE_AFTER: u64 = 30;
const MAX_VISITORS: usize = 200;
const GONE_GRACE: Duration = Duration::from_secs(3);
#[derive(Debug, Clone)]
pub struct Presence {
pub sid: String,
pub device: String,
pub page: String,
pub visible: bool,
pub idle: u32,
pub clicks: u32,
pub gone: bool,
}
impl Presence {
pub fn status(&self, away: Duration) -> String {
let idle = u64::from(self.idle) + away.as_secs();
let patience = if self.visible {
LEFT_AFTER
} else {
HIDDEN_LEFT_AFTER
};
if self.gone || away > patience {
"left".into()
} else if !self.visible {
"tab hidden".into()
} else if idle >= IDLE_AFTER {
format!("idle {idle}s")
} else {
"active".into()
}
}
}
pub struct Visitor {
pub presence: Presence,
pub seen: Instant,
pub first: Instant,
}
#[derive(Default)]
pub struct Roster {
visitors: HashMap<String, Visitor>,
}
impl Roster {
pub fn update(&mut self, presence: Presence, now: Instant) -> bool {
let known = self.visitors.get(&presence.sid);
if known.is_none() && (presence.gone || self.full(now)) {
return false;
}
let arrived = known.is_none();
let first = known.map_or(now, |v| v.first);
let sid = presence.sid.clone();
let visitor = Visitor {
presence,
seen: now,
first,
};
self.visitors.insert(sid, visitor);
arrived
}
fn full(&self, now: Instant) -> bool {
let here = self
.visitors
.values()
.filter(|v| v.presence.status(now.saturating_duration_since(v.seen)) != "left")
.count();
here >= MAX_VISITORS
}
pub fn departures(&mut self) -> Vec<Presence> {
let gone: Vec<String> = self
.visitors
.iter()
.filter(|(_, v)| {
let away = v.seen.elapsed();
(v.presence.gone && away >= GONE_GRACE) || v.presence.status(away) == "left"
})
.map(|(sid, _)| sid.clone())
.collect();
gone.iter()
.filter_map(|sid| self.visitors.remove(sid))
.map(|v| v.presence)
.collect()
}
pub fn by_arrival(&self) -> Vec<&Visitor> {
let mut visitors: Vec<&Visitor> = self.visitors.values().collect();
visitors.sort_by_key(|v| v.first);
visitors
}
pub fn get(&self, sid: &str) -> Option<&Visitor> {
self.visitors.get(sid)
}
pub fn len(&self) -> usize {
self.visitors.len()
}
pub fn is_empty(&self) -> bool {
self.visitors.is_empty()
}
}
pub fn device(agent: &str, hints: &str) -> String {
let system = [
("iPhone", "iPhone"),
("iPad", "iPad"),
("Android", "Android"),
("Macintosh", "Mac"),
("Windows", "Windows"),
("Linux", "Linux"),
]
.into_iter()
.find(|(needle, _)| agent.contains(needle))
.map_or("Unknown", |(_, name)| name);
let brave = agent.contains("Brave") || hints.contains("\"Brave\"");
let browser = [
("Edg/", "Edge"),
("EdgiOS/", "Edge"),
("OPR/", "Opera"),
("OPT/", "Opera"),
("Firefox/", "Firefox"),
("FxiOS/", "Firefox"),
("CriOS/", "Chrome"),
("Chrome/", "Chrome"),
("Safari/", "Safari"),
]
.into_iter()
.find(|(needle, _)| agent.contains(needle))
.map_or("browser", |(_, name)| name);
let browser = if brave { "Brave" } else { browser };
format!("{system} · {browser}")
}