Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rate user feature #404

Merged
merged 18 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ uuid = { version = "1.8.0", features = [
"serde",
] }
reqwest = { version = "0.12.1", features = ["json"] }
mostro-core = { version = "0.6.18", features = ["sqlx"] }
# mostro-core = { version = "0.6.18", features = ["sqlx"] }
mostro-core = { git = "https://github.com/MostroP2P/mostro-core.git", branch = "user-table-pubkey", features = ["sqlx"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
config = "0.14.0"
clap = { version = "4.5.19", features = ["derive"] }
lnurl-rs = "0.9.0"
openssl = { version = "0.10.66", features = ["vendored"] }
once_cell = "1.20.2"
bitcoin = "0.32.5"
bitcoin = "0.32.5"
3 changes: 1 addition & 2 deletions migrations/20231005195154_users.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
CREATE TABLE IF NOT EXISTS users (
id char(36) primary key not null,
pubkey char(64) unique not null,
pubkey char(64) primary key not null,
is_admin integer not null default 0,
is_solver integer not null default 0,
is_banned integer not null default 0,
Expand Down
24 changes: 18 additions & 6 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ use crate::app::release::release_action;
use crate::app::take_buy::take_buy_action;
use crate::app::take_sell::take_sell_action;

use crate::db::update_user_trade_index;
// Core functionality imports
use crate::db::add_new_user;
use crate::lightning::LndConnector;
use crate::nip59::unwrap_gift_wrap;
use crate::util::send_cant_do_msg;
Expand All @@ -44,7 +46,6 @@ use mostro_core::message::{Action, CantDoReason, Message};
use mostro_core::user::User;
use nostr_sdk::prelude::*;
use sqlx::{Pool, Sqlite};
use sqlx_crud::Crud;
use std::sync::Arc;
use tokio::sync::Mutex;
/// Helper function to log warning messages for action errors
Expand Down Expand Up @@ -79,8 +80,10 @@ async fn check_trade_index(pool: &Pool<Sqlite>, event: &UnwrappedGift, msg: &Mes
.verify_signature(event.sender, sig)
{
user.last_trade_index = index;
if user.update(pool).await.is_ok() {
tracing::info!("Update user trade index");
if let Err(e) =
update_user_trade_index(pool, user.pubkey, user.last_trade_index).await
{
tracing::error!("Error updating user trade index: {}", e);
}
} else {
tracing::info!("Invalid signature or trade index");
Expand All @@ -96,13 +99,22 @@ async fn check_trade_index(pool: &Pool<Sqlite>, event: &UnwrappedGift, msg: &Mes
}
Err(_) => {
if let (true, last_trade_index) = message_kind.has_trade_index() {
let new_user = User {
let new_user: User = User {
pubkey: event.sender.to_string(),
last_trade_index,
..Default::default()
};
if new_user.create(pool).await.is_ok() {
tracing::info!("Added new user for rate");
if let Err(e) =
add_new_user(pool, new_user.pubkey, new_user.last_trade_index).await
{
tracing::error!("Error creating new user: {}", e);
send_cant_do_msg(
None,
msg.get_inner_message_kind().id,
Some(CantDoReason::InvalidTextMessage),
&event.rumor.pubkey,
)
.await;
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/app/admin_add_solver.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::db::add_new_user;
use crate::util::{send_cant_do_msg, send_dm};

use anyhow::Result;
Expand All @@ -6,7 +7,6 @@ use mostro_core::user::User;
use nostr::nips::nip59::UnwrappedGift;
use nostr_sdk::prelude::*;
use sqlx::{Pool, Sqlite};
use sqlx_crud::Crud;
use tracing::{error, info};

pub async fn admin_add_solver_action(
Expand Down Expand Up @@ -42,7 +42,7 @@ pub async fn admin_add_solver_action(
let public_key = PublicKey::from_bech32(npubkey)?.to_hex();
let user = User::new(public_key, 0, 1, 0, 0, trade_index);
// Use CRUD to create user
match user.create(pool).await {
match add_new_user(pool, user.pubkey, user.last_trade_index).await {
Ok(r) => info!("Solver added: {:#?}", r),
Err(ee) => error!("Error creating solver: {:#?}", ee),
}
Expand Down
69 changes: 42 additions & 27 deletions src/app/rate_user.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::util::{send_cant_do_msg, send_new_order_msg, update_user_rating_event};
use crate::NOSTR_CLIENT;

use crate::db::{is_user_present, update_user_rating};
use anyhow::{Error, Result};
use mostro_core::message::{Action, Message, Payload};
use mostro_core::order::{Order, Status};
Expand Down Expand Up @@ -90,10 +91,10 @@ pub async fn update_user_reputation_action(

// Find the counterpart public key
if message_sender == buyer {
counterpart = seller;
counterpart = order.master_seller_pubkey.unwrap();
buyer_rating = true;
} else if message_sender == seller {
counterpart = buyer;
counterpart = order.master_buyer_pubkey.unwrap();
seller_rating = true;
grunch marked this conversation as resolved.
Show resolved Hide resolved
};

Expand Down Expand Up @@ -132,39 +133,53 @@ pub async fn update_user_reputation_action(
return Err(Error::msg("No rating present"));
}

// Ask counterpart reputation
let rep = get_user_reputation(&counterpart, my_keys).await?;
// Here we have to update values of the review of the counterpart
let mut reputation;

if let Some(r) = rep {
// Update user reputation
// Going on with calculation
reputation = r;
let old_rating = reputation.total_rating;
let last_rating = reputation.last_rating;
let new_rating =
old_rating + (last_rating as f64 - old_rating) / (reputation.total_reviews as f64);

reputation.last_rating = rating;
reputation.total_reviews += 1;
// Format with two decimals
let new_rating = format!("{:.2}", new_rating).parse::<f64>()?;

// Assing new total rating to review
reputation.total_rating = new_rating;
} else {
reputation = Rating::new(1, rating as f64, rating, MIN_RATING, MAX_RATING);
// Get counter to vote from db
let mut user_to_vote = is_user_present(pool, counterpart.clone()).await?;

// Update user reputation
// Going on with calculation
let old_rating = user_to_vote.total_rating;
let last_rating = user_to_vote.last_rating;
let new_rating = old_rating + (last_rating - old_rating) / (user_to_vote.total_reviews);

user_to_vote.last_rating = rating.into();
user_to_vote.total_reviews += 1;

// Assign new total rating to review
user_to_vote.total_rating = new_rating;

grunch marked this conversation as resolved.
Show resolved Hide resolved
// Create new rating event
let reputation_event = Rating::new(
user_to_vote.total_reviews as u64,
user_to_vote.total_rating as f64,
user_to_vote.last_rating as u8,
user_to_vote.min_rating as u8,
user_to_vote.max_rating as u8,
)
.to_tags()?;

// Save new rating to db
if let Err(e) = update_user_rating(
pool,
user_to_vote.pubkey,
user_to_vote.last_rating,
user_to_vote.min_rating,
user_to_vote.max_rating,
user_to_vote.total_reviews,
user_to_vote.total_rating,
)
.await
{
return Err(Error::msg(format!("Error updating user rating : {}", e)));
}
let reputation = reputation.to_tags()?;

if buyer_rating || seller_rating {
// Update db with rate flags
update_user_rating_event(
&counterpart,
update_buyer_rate,
update_seller_rate,
reputation,
reputation_event,
order.id,
my_keys,
pool,
Expand Down
98 changes: 98 additions & 0 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,104 @@ pub async fn is_user_present(pool: &SqlitePool, public_key: String) -> anyhow::R
Ok(user)
}

pub async fn add_new_user(
pool: &SqlitePool,
public_key: String,
last_trade_index: i64,
) -> anyhow::Result<User> {
// Validate public key format (32-bytes hex)
if !public_key.chars().all(|c| c.is_ascii_hexdigit()) || public_key.len() != 64 {
return Err(anyhow::anyhow!("Invalid public key format"));
}
let created_at: Timestamp = Timestamp::now();
let user = sqlx::query_as::<_, User>(
r#"
INSERT INTO users (pubkey, last_trade_index, created_at)
VALUES (?1, ?2, ?3)
RETURNING *
"#,
)
.bind(public_key)
.bind(last_trade_index)
.bind(created_at.to_string())
.fetch_one(pool)
.await?;

Ok(user)
}

pub async fn update_user_trade_index(
pool: &SqlitePool,
public_key: String,
trade_index: i64,
) -> anyhow::Result<User> {
if let Ok(user) = sqlx::query_as::<_, User>(
r#"
UPDATE users SET trade_index = ?1 WHERE pubkey = ?2
RETURNING *
"#,
)
.bind(trade_index)
.bind(public_key)
.fetch_one(pool)
.await
{
Ok(user)
} else {
Err(anyhow::anyhow!("No user found"))
}
}
grunch marked this conversation as resolved.
Show resolved Hide resolved

pub async fn update_user_rating(
pool: &SqlitePool,
public_key: String,
last_rating: i64,
min_rating: i64,
max_rating: i64,
total_reviews: i64,
total_rating: i64,
) -> anyhow::Result<User> {
// Validate public key format (32-bytes hex)
if !public_key.chars().all(|c| c.is_ascii_hexdigit()) || public_key.len() != 64 {
return Err(anyhow::anyhow!("Invalid public key format"));
}
// Validate rating values
if !(0..=5).contains(&last_rating) {
return Err(anyhow::anyhow!("Invalid rating value"));
}
if !(0..=5).contains(&min_rating) || !(0..=5).contains(&max_rating) {
return Err(anyhow::anyhow!("Invalid min/max rating values"));
}
if min_rating > last_rating || last_rating > max_rating {
return Err(anyhow::anyhow!("Rating values must satisfy: min_rating <= last_rating <= max_rating"));
}
if total_reviews < 0 {
return Err(anyhow::anyhow!("Invalid total reviews"));
}
if total_rating < 0 || total_rating > total_reviews * 5 {
return Err(anyhow::anyhow!("Invalid total rating"));
}
if let Ok(user) = sqlx::query_as::<_, User>(
r#"
UPDATE users SET last_rating = ?1, min_rating = ?2, max_rating = ?3, total_reviews = ?4, total_rating = ?5 WHERE pubkey = ?6
RETURNING *
"#,
)
.bind(last_rating)
.bind(min_rating)
.bind(max_rating)
.bind(total_reviews)
.bind(total_rating)
.bind(public_key)
.fetch_one(pool)
.await{
Ok(user)
}
else {
Err(anyhow::anyhow!("No user found"))
}
}

pub async fn is_assigned_solver(
pool: &SqlitePool,
solver_pubkey: &str,
Expand Down
Loading