diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 05698ff..09fe643 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -128,6 +128,7 @@ impl DataManager { channel_mutes: serde_json::from_str(&get!(x->28(String)).to_string()).unwrap(), is_deactivated: get!(x->29(i32)) as i8 == 1, ban_expire: get!(x->30(i64)) as usize, + coins: get!(x->31(i32)), } } @@ -284,7 +285,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32)", params![ &(data.id as i64), &(data.created as i64), @@ -317,6 +318,7 @@ impl DataManager { &serde_json::to_string(&data.channel_mutes).unwrap(), &if data.is_deactivated { 1_i32 } else { 0_i32 }, &(data.ban_expire as i64), + &(data.coins as i32) ] ); @@ -1058,6 +1060,7 @@ impl DataManager { auto_method!(update_user_ban_reason(&str)@get_user_by_id -> "UPDATE users SET ban_reason = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(update_user_channel_mutes(Vec)@get_user_by_id -> "UPDATE users SET channel_mutes = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_ban_expire(i64)@get_user_by_id -> "UPDATE users SET ban_expire = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); + auto_method!(update_user_coins(i32)@get_user_by_id -> "UPDATE users SET coins = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User); auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index e68a27c..4cb9b03 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -29,5 +29,6 @@ CREATE TABLE IF NOT EXISTS users ( ban_reason TEXT NOT NULL, channel_mutes TEXT NOT NULL, is_deactivated INT NOT NULL, - ban_expire BIGINT NOT NULL + ban_expire BIGINT NOT NULL, + coins INT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/version_migrations.sql b/crates/core/src/database/drivers/sql/version_migrations.sql index 5f3148c..47bcb04 100644 --- a/crates/core/src/database/drivers/sql/version_migrations.sql +++ b/crates/core/src/database/drivers/sql/version_migrations.sql @@ -33,3 +33,7 @@ ADD COLUMN IF NOT EXISTS ban_expire BIGINT DEFAULT 0; -- remove users seller_data ALTER TABLE users DROP COLUMN IF EXISTS seller_data; + +-- users coins +ALTER TABLE users +ADD COLUMN IF NOT EXISTS coins INT DEFAULT 0; diff --git a/crates/core/src/database/economy.rs b/crates/core/src/database/economy.rs new file mode 100644 index 0000000..f652e22 --- /dev/null +++ b/crates/core/src/database/economy.rs @@ -0,0 +1,91 @@ +use crate::model::economy::CoinTransfer; +use crate::model::{Error, Result}; +use crate::{auto_method, DataManager}; +use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow}; + +impl DataManager { + /// Get a [`Letter`] from an SQL row. + pub(crate) fn get_transfer_from_row(x: &PostgresRow) -> CoinTransfer { + CoinTransfer { + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + sender: get!(x->2(i64)) as usize, + receiver: get!(x->3(i64)) as usize, + amount: get!(x->4(i32)), + } + } + + auto_method!(get_transfer_by_id(usize as i64)@get_transfer_from_row -> "SELECT * FROM transfers WHERE id = $1" --name="transfer" --returns=CoinTransfer --cache-key-tmpl="atto.transfer:{}"); + + /// Get all transfers by user. + /// + /// # Arguments + /// * `id` - the ID of the user to fetch transfers for + /// * `batch` - the limit of items in each page + /// * `page` - the page number + pub async fn get_transfers_by_user( + &self, + id: usize, + batch: usize, + page: usize, + ) -> Result> { + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM transfers WHERE sender = $1 OR receiver = $1 ORDER BY created DESC LIMIT $2 OFFSET $3", + &[&(id as i64), &(batch as i64), &((page * batch) as i64)], + |x| { Self::get_transfer_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("transfer".to_string())); + } + + Ok(res.unwrap()) + } + + /// Create a new transfer in the database. + /// + /// # Arguments + /// * `data` - a mock [`Letter`] object to insert + pub async fn create_transfer(&self, data: CoinTransfer) -> Result { + // check values + let mut sender = self.get_user_by_id(data.sender).await?; + let mut receiver = self.get_user_by_id(data.receiver).await?; + let (sender_bankrupt, receiver_bankrupt) = data.apply(&mut sender, &mut receiver); + + if sender_bankrupt | receiver_bankrupt { + return Err(Error::MiscError( + "One party of this transfer cannot afford this".to_string(), + )); + } + + // ... + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "INSERT INTO transfers VALUES ($1, $2, $3, $4, $5)", + params![ + &(data.id as i64), + &(data.created as i64), + &(data.sender as i64), + &(data.receiver as i64), + &data.amount + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(data) + } +} diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 5650d7d..c49a71f 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -9,6 +9,7 @@ pub mod connections; mod domains; mod drafts; mod drivers; +mod economy; mod emojis; mod invite_codes; mod ipbans; diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index d730e13..680d059 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -92,6 +92,9 @@ pub struct User { /// The time at which the user's ban will automatically expire. #[serde(default)] pub ban_expire: usize, + /// The number of coins the user has. + #[serde(default)] + pub coins: i32, } pub type UserConnections = @@ -397,6 +400,7 @@ impl User { channel_mutes: Vec::new(), is_deactivated: false, ban_expire: 0, + coins: 0, } } diff --git a/crates/core/src/model/economy.rs b/crates/core/src/model/economy.rs new file mode 100644 index 0000000..95d8e01 --- /dev/null +++ b/crates/core/src/model/economy.rs @@ -0,0 +1,36 @@ +use serde::{Serialize, Deserialize}; +use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; + +use super::auth::User; + +#[derive(Serialize, Deserialize)] +pub struct CoinTransfer { + pub id: usize, + pub created: usize, + pub sender: usize, + pub receiver: usize, + pub amount: i32, +} + +impl CoinTransfer { + /// Create a new [`CoinTransfer`]. + pub fn new(sender: usize, receiver: usize, amount: i32) -> Self { + Self { + id: Snowflake::new().to_string().parse::().unwrap(), + created: unix_epoch_timestamp(), + sender, + receiver, + amount, + } + } + + /// Apply the effects of this transaction onto the sender and receiver balances. + /// + /// # Returns + /// `(sender bankrupt, receiver bankrupt)` + pub fn apply(&self, sender: &mut User, receiver: &mut User) -> (bool, bool) { + sender.coins -= self.amount; + receiver.coins += self.amount; + (sender.coins < 0, receiver.coins < 0) + } +} diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index b78dd37..9b46412 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -5,6 +5,7 @@ pub mod carp; pub mod channels; pub mod communities; pub mod communities_permissions; +pub mod economy; pub mod journals; pub mod littleweb; pub mod mail;