use std::time::{Duration, SystemTime};
use crate::moderation::policy::StrikePolicy;
use crate::moderation::types::ActionRecord;
pub fn compute_position_in_window(
history: &[ActionRecord],
policy: &StrikePolicy,
now: SystemTime,
) -> u32 {
let window = Duration::from_secs(policy.decay_window_days as u64 * 86_400);
let earliest_in_window = now.checked_sub(window).unwrap_or(SystemTime::UNIX_EPOCH);
let count = history
.iter()
.filter(|a| a.action_type.contributes_strikes())
.filter(|a| a.revoked_at.is_none())
.filter(|a| a.was_dampened)
.filter(|a| a.effective_at >= earliest_in_window && a.effective_at <= now)
.count();
let count_u32: u32 = u32::try_from(count).unwrap_or(u32::MAX);
count_u32.saturating_add(1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::moderation::policy::DecayFunction;
use crate::moderation::types::ActionType;
fn t0() -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000)
}
fn days(n: u64) -> Duration {
Duration::from_secs(n * 86_400)
}
fn action(at: SystemTime, kind: ActionType, was_dampened: bool) -> ActionRecord {
ActionRecord {
strike_value_applied: 1,
effective_at: at,
revoked_at: None,
action_type: kind,
expires_at: None,
was_dampened,
}
}
fn revoked(at: SystemTime, kind: ActionType, was_dampened: bool) -> ActionRecord {
ActionRecord {
strike_value_applied: 1,
effective_at: at,
revoked_at: Some(at + Duration::from_secs(60)),
action_type: kind,
expires_at: None,
was_dampened,
}
}
fn policy(threshold: u32, window_days: u32) -> StrikePolicy {
StrikePolicy {
good_standing_threshold: threshold,
dampening_curve: (1..threshold).collect(),
decay: DecayFunction::Linear,
decay_window_days: window_days,
suspension_freezes_decay: true,
cache_freshness_window_seconds: 3600,
}
}
#[test]
fn empty_history_returns_position_one() {
let p = policy(3, 90);
assert_eq!(compute_position_in_window(&[], &p, t0()), 1);
}
#[test]
fn single_in_window_dampened_action_returns_position_two() {
let now = t0();
let p = policy(3, 90);
let h = vec![action(now - days(10), ActionType::Takedown, true)];
assert_eq!(compute_position_in_window(&h, &p, now), 2);
}
#[test]
fn two_in_window_dampened_actions_return_position_three() {
let now = t0();
let p = policy(3, 90);
let h = vec![
action(now - days(40), ActionType::Takedown, true),
action(now - days(10), ActionType::Takedown, true),
];
assert_eq!(compute_position_in_window(&h, &p, now), 3);
}
#[test]
fn action_just_outside_window_excluded() {
let now = t0();
let p = policy(3, 90);
let h = vec![action(now - days(91), ActionType::Takedown, true)];
assert_eq!(compute_position_in_window(&h, &p, now), 1);
}
#[test]
fn action_at_exact_window_boundary_included() {
let now = t0();
let p = policy(3, 90);
let h = vec![action(now - days(90), ActionType::Takedown, true)];
assert_eq!(compute_position_in_window(&h, &p, now), 2);
}
#[test]
fn mix_of_in_and_out_of_window_actions_only_in_window_counted() {
let now = t0();
let p = policy(5, 90);
let h = vec![
action(now - days(200), ActionType::Takedown, true),
action(now - days(100), ActionType::Takedown, true),
action(now - days(60), ActionType::Takedown, true),
action(now - days(10), ActionType::Takedown, true),
];
assert_eq!(compute_position_in_window(&h, &p, now), 3);
}
#[test]
fn out_of_good_standing_action_excluded() {
let now = t0();
let p = policy(3, 90);
let h = vec![action(now - days(10), ActionType::Takedown, false)];
assert_eq!(compute_position_in_window(&h, &p, now), 1);
}
#[test]
fn mixed_dampened_and_undampened_only_dampened_count() {
let now = t0();
let p = policy(5, 90);
let h = vec![
action(now - days(60), ActionType::Takedown, true),
action(now - days(40), ActionType::Takedown, false),
action(now - days(20), ActionType::Takedown, true),
];
assert_eq!(compute_position_in_window(&h, &p, now), 3);
}
#[test]
fn revoked_action_excluded() {
let now = t0();
let p = policy(3, 90);
let h = vec![revoked(now - days(10), ActionType::Takedown, true)];
assert_eq!(compute_position_in_window(&h, &p, now), 1);
}
#[test]
fn revoked_action_does_not_consume_a_position_slot() {
let now = t0();
let p = policy(5, 90);
let h = vec![
revoked(now - days(40), ActionType::Takedown, true),
action(now - days(10), ActionType::Takedown, true),
];
assert_eq!(compute_position_in_window(&h, &p, now), 2);
}
#[test]
fn warning_and_note_excluded_even_with_was_dampened_true() {
let now = t0();
let p = policy(3, 90);
let h = vec![
action(now - days(20), ActionType::Warning, true),
action(now - days(10), ActionType::Note, true),
];
assert_eq!(compute_position_in_window(&h, &p, now), 1);
}
#[test]
fn temp_suspension_counts_when_dampened_in_window() {
let now = t0();
let p = policy(3, 90);
let h = vec![action(now - days(10), ActionType::TempSuspension, true)];
assert_eq!(compute_position_in_window(&h, &p, now), 2);
}
#[test]
fn indef_suspension_counts_when_dampened_in_window() {
let now = t0();
let p = policy(3, 90);
let h = vec![action(now - days(10), ActionType::IndefSuspension, true)];
assert_eq!(compute_position_in_window(&h, &p, now), 2);
}
#[test]
fn deterministic_for_same_inputs() {
let now = t0();
let p = policy(5, 90);
let h = vec![
action(now - days(60), ActionType::Takedown, true),
action(now - days(20), ActionType::Takedown, true),
];
let a = compute_position_in_window(&h, &p, now);
let b = compute_position_in_window(&h, &p, now);
assert_eq!(a, b);
}
#[test]
fn combined_filters_pruning_correctly() {
let now = t0();
let p = policy(5, 90);
let h = vec![
action(now - days(200), ActionType::Takedown, true), revoked(now - days(60), ActionType::Takedown, true), action(now - days(40), ActionType::Takedown, false), action(now - days(30), ActionType::Warning, true), action(now - days(20), ActionType::Takedown, true), action(now - days(10), ActionType::TempSuspension, true), ];
assert_eq!(compute_position_in_window(&h, &p, now), 3);
}
}