Skip to main content

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}