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}