use diesel::connection::Connection; use lootalot_db::{self as db, DbConnection, Update, Value}; use std::collections::HashSet; pub type IdList = Vec; pub type ItemListWithMods = Vec<(i32, Option)>; #[derive(Serialize, Deserialize, Debug)] pub struct BuySellParams { pub items: ItemListWithMods, players: Option, global_mod: Option, } /// A generic response for all queries #[derive(Serialize, Debug, Default)] pub struct ApiResponse { /// The value requested, if any pub value: Option, /// A text to notify user, if relevant pub notification: Option, /// A list of updates, if any pub updates: Option>, /// A text describing errors, if any pub errors: Option, } impl ApiResponse { fn push_update(&mut self, update: db::Update) { if let Some(v) = self.updates.as_mut() { v.push(update); } else { self.updates = Some(vec![update]); } } fn push_error>(&mut self, error: S) { if let Some(errors) = self.errors.as_mut() { *errors = format!("{}\n{}", errors, error.into()); } else { self.errors = Some(error.into()) } } fn set_value(&mut self, value: db::Value) { self.value = Some(value); } fn notify>(&mut self, text: S) { self.notification = Some(text.into()); } } pub enum ApiError { DieselError(diesel::result::Error), InvalidAction(String), } /// Every allowed queries on the database pub enum ApiActions { // Application level FetchPlayers, FetchInventory, FetchClaims, // Player level FetchPlayer(i32), FetchNotifications(i32), FetchLoot(i32), UpdateWealth(i32, f64), BuyItems(i32, BuySellParams), SellItems(i32, BuySellParams), ClaimItems(i32, IdList), ClaimItem(i32, i32), UnclaimItem(i32, i32), UndoLastAction(i32), // Group level AddLoot(Vec), // Admin level //AddPlayer(String, f64), //AddInventoryItem(pub String, pub i32), //ResolveClaims, //SetClaimsTimeout(pub i32), } pub fn execute( conn: &DbConnection, query: ApiActions, ) -> Result { let mut response = ApiResponse::default(); // Return an Option that describes what happened. // If there is some value, store the actions in db so that it can be reversed. let action_text: Option<(i32, &str)> = match query { ApiActions::FetchPlayers => { response.set_value(Value::PlayerList(db::Players(conn).all()?)); None } ApiActions::FetchInventory => { response.set_value(Value::ItemList(db::Inventory(conn).all()?)); None } ApiActions::FetchClaims => { response.set_value(Value::ClaimList(db::fetch_claims(conn)?)); None } ApiActions::FetchPlayer(id) => { response.set_value(Value::Player(db::Players(conn).find(id)?)); None } ApiActions::FetchNotifications(id) => { response.set_value(Value::Notifications( db::AsPlayer(conn, id).notifications()?, )); None } ApiActions::FetchLoot(id) => { response.set_value(Value::ItemList(db::LootManager(conn, id).all()?)); None } ApiActions::UpdateWealth(id, amount) => { response.push_update(db::AsPlayer(conn, id).update_wealth(amount)?); response.notify(format!("Mis à jour ({}po)!", amount)); Some((id, "Argent mis à jour")) } ApiActions::BuyItems(id, params) => { // TODO: check that player has enough money ! let mut gains: Vec = Vec::with_capacity(params.items.len()); for (item_id, price_mod) in params.items.into_iter() { if let Ok((item, diff)) = db::buy_item_from_inventory(conn, id, item_id, price_mod) { response.push_update(item); gains.push(diff); } else { response.push_error(format!("Error adding {}", item_id)); } } let added_items = gains.len(); let total_amount = gains .into_iter() .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i); response.notify(format!( "{} objets achetés pour {}po", added_items, total_amount.to_gp() )); response.push_update(Update::Wealth(total_amount)); Some((id, "Achat d'objets")) } // Behavior differs if player is group or regular. // Group sells item like players then split the total amount among players. ApiActions::SellItems(id, params) => { let mut gains: Vec = Vec::with_capacity(params.items.len()); for (loot_id, price_mod) in params.items.iter() { if let Ok((deleted, diff)) = db::sell_item_transaction(conn, id, *loot_id, *price_mod) { response.push_update(deleted); gains.push(diff); } else { response.push_error(format!("Erreur lors de la vente (loot_id : {})", loot_id)); } } let sold_items = gains.len(); let total_amount = gains .into_iter() .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i); match id { 0 => { let players = params .players .unwrap_or(db::Players(conn).all()?.into_iter().map(|p| p.id).collect()); let shared = db::split_and_share(conn, total_amount.to_gp() as i32, &players)?; let shared_amount = { if let Update::Wealth(amount) = shared { amount.to_gp() } else { panic!("cannot happen") } }; response.notify(format!( "Les objets ont été vendus, les joueurs ont reçu (au total) {} po", shared_amount )); //response.push_update(Update::GroupShare(players, share)); response.push_update(shared); } _ => { response.notify(format!( "{} objet(s) vendu(s) pour {} po", sold_items, total_amount.to_gp() )); response.push_update(Update::Wealth(total_amount)); } } Some((id, "Vente d'objets")) } ApiActions::ClaimItem(id, item) => { response.push_update(db::Claims(conn).add(id, item)?); response.notify("Pour moi !".to_string()); None } ApiActions::UnclaimItem(id, item) => { response.push_update(db::Claims(conn).remove(id, item)?); response.notify("Bof! Finalement non.".to_string()); None } ApiActions::ClaimItems(id, items) => { conn.transaction(|| -> Result, diesel::result::Error> { let current_claims: HashSet = db::Claims(conn).all()?.iter().filter(|c| c.player_id == id).map(|c| c.loot_id).collect(); let new_claims: HashSet = items.into_iter().collect(); // Claims to delete for item in current_claims.difference(&new_claims) { response.push_update(db::Claims(conn).remove(id, *item)?); } // Claims to add for item in new_claims.difference(¤t_claims) { response.push_update(db::Claims(conn).add(id, *item)?); } Ok(Some((id, "Requête(s) mises(s) à jour"))) })? } ApiActions::UndoLastAction(id) => { if let Ok(event) = db::models::history::get_last_of_player(conn, id) { let name = String::from(event.name()); for undone in event.undo(conn)?.into_inner().into_iter() { response.push_update(undone); } response.notify(format!("'{}' annulé(e)", name)); } else { response.push_error("Aucune action trouvée") }; None } // Group actions ApiActions::AddLoot(items) => { let mut added_items = 0; for item in items.into_iter() { if let Ok(added) = db::LootManager(conn, 0).add_from(&item) { response.push_update(added); added_items += 1; } else { response.push_error(format!("Error adding {:?}", item)); } } response.notify(format!("{} objets lootés !", added_items)); // Notify players through persistent notifications if let Err(e) = db::Players(conn) .notifiy_all("De nouveaux objets ont été ajoutés au coffre de groupe !") { response.push_error(format!("Erreur durant la notification : {:?}", e)); }; Some((0, "Nouveau loot")) } }; // Store the event if it can be undone. dbg!(&action_text); if let Some((id, text)) = action_text { db::models::history::insert_event( conn, id, text, response .updates .as_ref() .expect("there should be updates in here !"), )?; } // match _action_text -> Save updates in DB Ok(response) }