mostro_core/user.rs
1//! Persistent user representation and reputation helpers.
2//!
3//! The [`User`] struct is the database-backed record Mostro keeps for every
4//! identity that has interacted with the system. It tracks rating aggregates,
5//! administrative flags and the last trade index used by the user, which is
6//! required so that new orders always carry a strictly increasing trade index.
7//!
8//! [`UserInfo`] is a lightweight view of the same data that can safely be
9//! shared with a counterpart during a trade without leaking internals.
10
11use chrono::Utc;
12use serde::{Deserialize, Serialize};
13#[cfg(feature = "sqlx")]
14use sqlx::FromRow;
15
16/// Public snapshot of a user's reputation shared with peers during a trade.
17///
18/// Unlike [`User`], `UserInfo` contains only the values a counterpart needs
19/// to decide whether to trade: aggregated rating, number of reviews and how
20/// many days the user has been operating on Mostro.
21#[derive(Debug, Default, Deserialize, Serialize, Clone)]
22
23pub struct UserInfo {
24 /// Aggregated rating value for the user (see [`crate::rating::Rating`]).
25 pub rating: f64,
26 /// Total number of ratings received.
27 pub reviews: i64,
28 /// Number of days since the user account was created.
29 pub operating_days: u64,
30}
31
32/// Database representation of a Mostro user.
33///
34/// This is the canonical row stored on the Mostro node. It tracks identity
35/// data (`pubkey`), administrative role flags, the last trade index used by
36/// the user and rating aggregates used to compute reputation.
37#[cfg_attr(feature = "sqlx", derive(FromRow))]
38#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
39pub struct User {
40 /// Master public key of the user, hex encoded.
41 pub pubkey: String,
42 /// `1` when the user has admin privileges, `0` otherwise. Stored as
43 /// `i64` to match the underlying SQLite representation.
44 pub is_admin: i64,
45 /// Optional password used to authenticate privileged admin actions.
46 pub admin_password: Option<String>,
47 /// `1` when the user is a dispute solver, `0` otherwise.
48 pub is_solver: i64,
49 /// `1` when the user is banned from the platform, `0` otherwise.
50 pub is_banned: i64,
51 /// Free-form category bucket. Reserved for future segmentation.
52 pub category: i64,
53 /// Last trade index used by this user. When a user creates a new order
54 /// (or takes one) the incoming trade index must be strictly greater than
55 /// this value, or the request is rejected.
56 pub last_trade_index: i64,
57 /// Total number of ratings the user has received.
58 pub total_reviews: i64,
59 /// Weighted rating average computed from all received ratings.
60 pub total_rating: f64,
61 /// Most recent rating received, in the `MIN_RATING..=MAX_RATING` range.
62 pub last_rating: i64,
63 /// Highest rating ever received.
64 pub max_rating: i64,
65 /// Lowest rating ever received.
66 pub min_rating: i64,
67 /// Unix timestamp (seconds) when the user record was created.
68 pub created_at: i64,
69}
70
71impl User {
72 /// Create a new [`User`] with fresh rating aggregates.
73 ///
74 /// `trade_index` becomes the user's `last_trade_index`. The `created_at`
75 /// timestamp is set to the current system time.
76 pub fn new(
77 pubkey: String,
78 is_admin: i64,
79 is_solver: i64,
80 is_banned: i64,
81 category: i64,
82 trade_index: i64,
83 ) -> Self {
84 Self {
85 pubkey,
86 is_admin,
87 admin_password: None,
88 is_solver,
89 is_banned,
90 category,
91 last_trade_index: trade_index,
92 total_reviews: 0,
93 total_rating: 0.0,
94 last_rating: 0,
95 max_rating: 0,
96 min_rating: 0,
97 created_at: Utc::now().timestamp(),
98 }
99 }
100
101 /// Record a new rating for the user and refresh the aggregates.
102 ///
103 /// The first vote is weighted by `1/2` so that a single review cannot
104 /// anchor a perfect or disastrous reputation. Subsequent votes update
105 /// `total_rating` with an incremental running-average formula.
106 /// `min_rating` and `max_rating` are tightened as new extremes arrive.
107 ///
108 /// # Example
109 ///
110 /// ```
111 /// use mostro_core::user::User;
112 ///
113 /// let mut user = User::new("pubkey".into(), 0, 0, 0, 0, 0);
114 /// user.update_rating(5);
115 /// assert_eq!(user.total_reviews, 1);
116 /// assert_eq!(user.max_rating, 5);
117 /// ```
118 pub fn update_rating(&mut self, rating: u8) {
119 // Update user reputation
120 // increment first
121 self.total_reviews += 1;
122 let old_rating = self.total_rating;
123 // recompute new rating
124 if self.total_reviews <= 1 {
125 // New logic with weight 1/2 for first vote.
126 let first_rating = rating as f64;
127 self.total_rating = first_rating / 2.0;
128 self.max_rating = rating.into();
129 self.min_rating = rating.into();
130 } else {
131 self.total_rating =
132 old_rating + ((self.last_rating as f64) - old_rating) / (self.total_reviews as f64);
133 if self.max_rating < rating.into() {
134 self.max_rating = rating.into();
135 }
136 if self.min_rating > rating.into() {
137 self.min_rating = rating.into();
138 }
139 }
140 // Store last rating
141 self.last_rating = rating.into();
142 }
143}