Skip to main content

de_mls/core/peer_scoring/
plugin.rs

1//! [`PeerScoringPlugin`] — the per-conversation scoring contract.
2//!
3//! Mutating methods return [`PeerScoringEvent`]s the coordinator drains
4//! at safe points and turns into protocol actions. Storage backends and
5//! event publishing are caller concerns.
6
7use crate::core::{PeerScoringEvent, ScoreOp, ScoreSnapshot};
8
9/// Per-conversation peer-scoring plug-in. Mutating methods return any
10/// [`PeerScoringEvent`]s the call produced; the coordinator drains them
11/// at safe points and turns threshold crossings into protocol actions.
12/// Storage backends and event publishing are caller concerns.
13pub trait PeerScoringPlugin {
14    /// Start tracking a member at the plug-in's default score.
15    ///
16    /// MUST be called at most once per member: a duplicate call resets
17    /// the score to default and discards any accumulated state. Callers
18    /// guard with [`Self::score_for`] or a roster diff (see
19    /// [`super::scoring_member_diff`]).
20    ///
21    /// Emits [`PeerScoringEvent::ThresholdCrossedDown`] iff the default
22    /// score lands at-or-below threshold (unusual config); otherwise no
23    /// event.
24    fn add_member(&mut self, member_id: &[u8]) -> Vec<PeerScoringEvent>;
25
26    /// Stop tracking a member. Emits no events — removal is a membership
27    /// change, not a score event. Idempotent: a no-op on an untracked
28    /// member.
29    fn remove_member(&mut self, member_id: &[u8]);
30
31    /// Apply a single [`ScoreOp`]. Untracked members are no-ops (no
32    /// event, no auto-tracking — coordinators must call [`Self::add_member`]
33    /// before applying ops). Returns events only when the op causes a
34    /// threshold cross; repeated ops on a member already at-or-below
35    /// threshold do not re-emit.
36    fn apply_op(&mut self, op: &ScoreOp) -> Vec<PeerScoringEvent>;
37
38    /// Apply a batch of [`ScoreOp`]s. Returns the concatenated events
39    /// for every op in input order. A member crossing threshold twice
40    /// in one batch (down then up, say) yields two events in that order.
41    /// Default impl chains [`Self::apply_op`]; override for batched-write
42    /// storage where the round-trip cost dominates.
43    fn apply_ops(&mut self, ops: &[ScoreOp]) -> Vec<PeerScoringEvent> {
44        ops.iter().flat_map(|op| self.apply_op(op)).collect()
45    }
46
47    /// Apply a snapshot of absolute scores (ConversationSync bootstrap).
48    /// Unknown members are auto-tracked at the snapshot value — unlike
49    /// [`Self::apply_op`], which ignores them — because a snapshot may
50    /// arrive before the MLS membership view catches up.
51    ///
52    /// Emits a [`PeerScoringEvent`] per entry that crosses threshold;
53    /// re-applying an unchanged snapshot emits nothing.
54    fn apply_snapshot(&mut self, snapshot: &ScoreSnapshot) -> Vec<PeerScoringEvent>;
55
56    /// Sparse snapshot of non-default scores for ConversationSync send.
57    fn snapshot(&self) -> ScoreSnapshot;
58
59    fn score_for(&self, member_id: &[u8]) -> Option<i64>;
60    fn members_below_threshold(&self) -> Vec<Vec<u8>>;
61    fn all_members_with_scores(&self) -> Vec<(Vec<u8>, i64)>;
62
63    /// Current removal threshold. Coordinator reads this when building
64    /// `ConversationSync` so joiners adopt the same value.
65    fn threshold(&self) -> i64;
66
67    /// Update the threshold in place. Emits NO events — even though a
68    /// tightened threshold can re-classify members from above to
69    /// at-or-below, the plug-in does not retroactively scan and emit.
70    /// Event-driven coordinators MUST re-scan
71    /// [`Self::members_below_threshold`] after a `set_threshold` call,
72    /// or arrange for a subsequent apply that surfaces the affected
73    /// members through normal cross-detection.
74    fn set_threshold(&mut self, threshold: i64);
75
76    /// Starting score for a newly tracked member. Called by
77    /// [`Self::add_member`] and on `apply_snapshot` for unknown identities.
78    fn default_score(&self) -> i64;
79
80    /// Update the starting score in place. Applies to future
81    /// [`Self::add_member`] calls; does not retroactively rescore
82    /// already-tracked members.
83    fn set_default_score(&mut self, score: i64);
84}