Skip to main content

de_mls/peer_scoring/
plugin.rs

1//! [`PeerScoringPlugin`] — the per-conversation scoring contract.
2//!
3//! Mutating methods return `true` when the call drove a member down across
4//! the threshold; the coordinator re-scans [`PeerScoringPlugin::members_below_threshold`]
5//! at a safe point to act. Storage backends are caller concerns.
6
7use crate::{ScoreOp, ScoreSnapshot};
8
9/// Per-conversation peer-scoring plug-in. Mutating methods return `true`
10/// iff the call moved at least one member down to at-or-below threshold;
11/// the coordinator re-scans for the actual targets. Storage backends are
12/// caller concerns.
13pub trait PeerScoringPlugin {
14    /// Start tracking a member at the plug-in's default score. Returns
15    /// `true` iff the default score lands at-or-below threshold (unusual
16    /// config).
17    ///
18    /// MUST be called at most once per member: a duplicate call resets
19    /// the score to default and discards any accumulated state. Callers
20    /// guard with [`Self::score_for`] or a roster diff (see
21    /// [`super::scoring_member_diff`]).
22    fn add_member(&mut self, member_id: &[u8]) -> bool;
23
24    /// Stop tracking a member. Idempotent: a no-op on an untracked member.
25    fn remove_member(&mut self, member_id: &[u8]);
26
27    /// Apply a single [`ScoreOp`]. Untracked members are no-ops (no
28    /// auto-tracking — coordinators must call [`Self::add_member`] first).
29    /// Returns `true` only when the op crosses the member down; repeated
30    /// ops on a member already at-or-below threshold return `false`.
31    fn apply_op(&mut self, op: &ScoreOp) -> bool;
32
33    /// Apply a batch of [`ScoreOp`]s. Returns `true` iff any op in the
34    /// batch crossed its member down. Default impl chains [`Self::apply_op`]
35    /// (applying all ops); override for batched-write storage.
36    fn apply_ops(&mut self, ops: &[ScoreOp]) -> bool {
37        let mut crossed = false;
38        for op in ops {
39            crossed |= self.apply_op(op);
40        }
41        crossed
42    }
43
44    /// Apply a snapshot of absolute scores (ConversationSync bootstrap).
45    /// Unknown members are auto-tracked at the snapshot value — unlike
46    /// [`Self::apply_op`], which ignores them — because a snapshot may
47    /// arrive before the MLS membership view catches up.
48    ///
49    /// Returns `true` iff any entry crossed its member down; re-applying an
50    /// unchanged snapshot returns `false`.
51    fn apply_snapshot(&mut self, snapshot: &ScoreSnapshot) -> bool;
52
53    /// Sparse snapshot of non-default scores for ConversationSync send.
54    fn snapshot(&self) -> ScoreSnapshot;
55
56    fn score_for(&self, member_id: &[u8]) -> Option<i64>;
57    fn members_below_threshold(&self) -> Vec<Vec<u8>>;
58    fn all_members_with_scores(&self) -> Vec<(Vec<u8>, i64)>;
59
60    /// Current removal threshold. Coordinator reads this when building
61    /// `ConversationSync` so joiners adopt the same value.
62    fn threshold(&self) -> i64;
63
64    /// Update the threshold in place. Emits NO events — even though a
65    /// tightened threshold can re-classify members from above to
66    /// at-or-below, the plug-in does not retroactively scan and emit.
67    /// Event-driven coordinators MUST re-scan
68    /// [`Self::members_below_threshold`] after a `set_threshold` call,
69    /// or arrange for a subsequent apply that surfaces the affected
70    /// members through normal cross-detection.
71    fn set_threshold(&mut self, threshold: i64);
72
73    /// Starting score for a newly tracked member. Called by
74    /// [`Self::add_member`] and on `apply_snapshot` for unknown identities.
75    fn default_score(&self) -> i64;
76}