diff --git a/.env.example b/.env.example index 08b0ae9..5ed3ab3 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,7 @@ STICKBOT_REDIS_PORT="" STICKBOT_REDIS_PASSWORD="" STICKBOT_ONCORE_URL_ENV="" +STICKBOT_ADMIN_EMAILS="" +STICKBOT_MAX_ACTIVE_TABLES_PER_PLAYER= BOXBOT_WORKER_DELAY=1000 -STICKBOT_ADMIN_EMAILS="" diff --git a/Cargo.lock b/Cargo.lock index 09c764c..839389e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,7 +345,7 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bankah" -version = "0.3.3" +version = "0.3.4" dependencies = [ "bson", "chrono", @@ -445,9 +445,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "bytes" @@ -519,9 +519,9 @@ dependencies = [ [[package]] name = "const_fn" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" [[package]] name = "constant_time_eq" @@ -914,9 +914,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", @@ -2009,9 +2009,9 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" [[package]] name = "sha2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer", "cfg-if 1.0.0", @@ -2178,7 +2178,7 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" [[package]] name = "stickbot" -version = "0.3.3" +version = "0.3.4" dependencies = [ "async-std", "bankah", @@ -2253,9 +2253,9 @@ checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08" [[package]] name = "syn" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" dependencies = [ "proc-macro2", "quote", @@ -2526,7 +2526,7 @@ dependencies = [ [[package]] name = "twowaiyo" -version = "0.3.3" +version = "0.3.4" dependencies = [ "bankah", "dotenv", diff --git a/README.md b/README.md index 0e062f6..28fe47e 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,8 @@ This is a simple craps game engine implemented in rust. | :video_camera: | | --- | | ![twowaiyo](https://user-images.githubusercontent.com/1545348/139085831-df999c07-08c0-49dd-99d6-7ad987dec412.gif) | + + +#### Web Application + +The web application interface can be found at [/workspace/stickbot](/workspace/stickbot/README.md). diff --git a/workspace/bankah/Cargo.toml b/workspace/bankah/Cargo.toml index 86602aa..53c7e43 100644 --- a/workspace/bankah/Cargo.toml +++ b/workspace/bankah/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bankah" -version = "0.3.3" +version = "0.3.4" edition = "2018" [dependencies] diff --git a/workspace/bankah/src/jobs.rs b/workspace/bankah/src/jobs.rs index da37304..9a8f6e4 100644 --- a/workspace/bankah/src/jobs.rs +++ b/workspace/bankah/src/jobs.rs @@ -22,9 +22,21 @@ pub struct JobWapper { pub attempts: u8, } +impl JobWapper { + pub fn retry(self) -> Self { + let JobWapper { job, id, attempts } = self; + JobWapper { + job, + id, + attempts: attempts + 1, + } + } +} + #[derive(Debug, Deserialize, Serialize, Clone)] pub enum TableAdminJob { ReindexPopulations, + CleanupPlayerData(String), } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -43,12 +55,14 @@ impl TableJob { } } - pub fn retry(&self) -> Option { + pub fn admin(job: TableAdminJob) -> Self { + let id = uuid::Uuid::new_v4(); + TableJob::Admin(JobWapper { job, id, attempts: 0 }) + } + + pub fn retry(self) -> Option { match self { - TableJob::Bet(inner) => Some(TableJob::Bet(JobWapper { - attempts: inner.attempts + 1, - ..inner.clone() - })), + TableJob::Bet(inner) => Some(TableJob::Bet(inner.retry())), _ => None, } } diff --git a/workspace/bankah/src/state.rs b/workspace/bankah/src/state.rs index 1154a0c..924d3b6 100644 --- a/workspace/bankah/src/state.rs +++ b/workspace/bankah/src/state.rs @@ -98,4 +98,5 @@ pub struct PlayerState { pub emails: Vec, pub nickname: String, pub balance: u32, + pub tables: Vec, } diff --git a/workspace/stickbot/Cargo.toml b/workspace/stickbot/Cargo.toml index 8c8f6de..2019e40 100644 --- a/workspace/stickbot/Cargo.toml +++ b/workspace/stickbot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stickbot" -version = "0.3.3" +version = "0.3.4" edition = "2018" [lib] diff --git a/workspace/stickbot/README.md b/workspace/stickbot/README.md new file mode 100644 index 0000000..ae4918d --- /dev/null +++ b/workspace/stickbot/README.md @@ -0,0 +1,31 @@ +## Stickbot + +This a [rust] application is the http-based web api backend for [twowaiyo], using a simple REST api to allow players +to join, participate and leave games. The [emberjs] web-browser frontend is located at [github.com/dadleyy/oncore]. + +#### Developing Locally + +The web backend currently relies on three third party services: + +1. [mongodb] for peristing table and player state. +2. [redis] for the asynchronous background job queue. +3. [auth0] for handling the integration with third party OAuth providers. + +At this time, free, hosted instances of mongodb and redis are available at [mongodb.com/cloud][mdbc] and [redislabs] +respectively. After creating the necessary accounts (or leveraging on-premise version of these), the `.env.example` +file located at the root of this repository can be used as a template for setting the credentials for authenticating +with these services. + +Once the environment variables have been prepared, the main web process and background worker can be started using the +appropriate `cargo` aliases: + + +[rust]: https://www.rust-lang.org/ +[twowaiyo]: https://github.com/dadleyy/twowaiyo +[mongodb]: https://www.mongodb.com/ +[redis]: https://redis.io/ +[mdbc]: https://www.mongodb.com/cloud +[redislabs]: https://app.redislabs.com/#/login +[auth0]: https://auth0.com/ +[github.com/dadleyy/oncore]: https://github.com/dadleyy/oncore +[emberjs]: https://emberjs.com/ diff --git a/workspace/stickbot/src/bin/boxbot.rs b/workspace/stickbot/src/bin/boxbot.rs index 99e29c8..57e5c4b 100644 --- a/workspace/stickbot/src/bin/boxbot.rs +++ b/workspace/stickbot/src/bin/boxbot.rs @@ -3,7 +3,7 @@ use std::time::Duration; use stickbot; -use bankah::jobs::TableJob; +use bankah::jobs::{TableAdminJob, TableJob}; const POP_CMD: kramer::Command<&'static str, &'static str> = kramer::Command::List::<_, &str>(kramer::ListCommand::Pop( @@ -62,7 +62,10 @@ async fn work(services: &stickbot::Services) -> Result<()> { let id = job.id(); let result = match &job { - TableJob::Admin(inner) => stickbot::processors::admin::reindex(&services, &inner.job).await, + TableJob::Admin(inner) => match &inner.job { + TableAdminJob::ReindexPopulations => stickbot::processors::admin::reindex(&services, &inner.job).await, + TableAdminJob::CleanupPlayerData(id) => stickbot::processors::admin::cleanup(&services, &id).await, + }, TableJob::Bet(inner) => stickbot::processors::bet(&services, &inner.job).await, TableJob::Roll(inner) => stickbot::processors::roll(&services, &inner.job).await, }; diff --git a/workspace/stickbot/src/constants.rs b/workspace/stickbot/src/constants.rs index ebc3ce2..ec75c5a 100644 --- a/workspace/stickbot/src/constants.rs +++ b/workspace/stickbot/src/constants.rs @@ -34,3 +34,6 @@ pub const EMPTY_RESPONSE: &'static str = ""; pub const BOXBOT_DELAY_ENV: &'static str = "BOXBOT_WORKER_DELAY"; pub const STICKBOT_ADMIN_EMAILS_ENV: &'static str = "STICKBOT_ADMIN_EMAILS"; + +pub const STICKBOT_DEFAULT_MAX_ACTIVE_TABLES_PER_PLAYER: usize = 2; +pub const STICKBOT_MAX_ACTIVE_TABLES_PER_PLAYER_ENV: &'static str = "STICKBOT_MAX_ACTIVE_TABLES_PER_PLAYER"; diff --git a/workspace/stickbot/src/processors/admin.rs b/workspace/stickbot/src/processors/admin.rs index d2ee040..b1986da 100644 --- a/workspace/stickbot/src/processors/admin.rs +++ b/workspace/stickbot/src/processors/admin.rs @@ -1,9 +1,11 @@ -use bankah::jobs::{JobError, TableAdminJob, TableJobOutput}; +use async_std::stream::StreamExt; -use crate::db::doc; +use bankah::jobs::{JobError, TableAdminJob, TableJob, TableJobOutput}; + +use crate::db::{doc, FindOneAndReplaceOptions, ReturnDocument}; use crate::Services; -pub async fn reindex<'a>(services: &Services, job: &TableAdminJob) -> Result { +pub async fn reindex(services: &Services, job: &TableAdminJob) -> Result { log::info!("attempting to reindex table populations - {:?}", job); let tables = services.tables(); let pipeline = vec![ @@ -23,3 +25,57 @@ pub async fn reindex<'a>(services: &Services, job: &TableAdminJob) -> Result Result { + log::debug!("cleaning up player '{}'", id); + + let mut tables = match services + .tables() + .find(doc! { format!("seats.{}", id): { "$exists": true } }, None) + .await + { + Err(error) => { + log::warn!("unable to find any tables for user - {}", error); + return Ok(TableJobOutput::AdminOk); + } + Ok(cursor) => cursor, + }; + + while let Some(doc) = tables.next().await { + let mut state = match doc { + Err(error) => { + log::warn!("error loading next able - {}", error); + continue; + } + Ok(table) => table, + }; + + state.seats = state + .seats + .into_iter() + .filter(|(seat, _)| &seat.to_string() != id) + .collect(); + + let opts = FindOneAndReplaceOptions::builder() + .return_document(ReturnDocument::After) + .build(); + + if let Err(error) = services + .tables() + .find_one_and_replace(crate::db::lookup_for_uuid(&state.id), &state, opts) + .await + { + log::warn!("failed cleanup '{}' on table '{}': {}", id, &state.id, error); + } + + log::debug!("next table - {:?}", state); + } + + log::debug!("cleanup for player '{}' complete, reindexing tables", id); + + if let Err(error) = services.queue(&TableJob::reindex()).await { + log::warn!("unable to queue reindexing job - {}", error); + } + + Ok(TableJobOutput::AdminOk) +} diff --git a/workspace/stickbot/src/routes/account.rs b/workspace/stickbot/src/routes/account.rs index b150e62..a0d87ea 100644 --- a/workspace/stickbot/src/routes/account.rs +++ b/workspace/stickbot/src/routes/account.rs @@ -1,4 +1,12 @@ -use crate::web::{cookie as get_cookie, Error, Request, Result}; +use serde::Serialize; + +use crate::db; +use crate::web::{cookie as get_cookie, Body, Error, Request, Response, Result}; + +#[derive(Serialize)] +struct DeletionResponsePayload { + id: String, +} pub async fn delete(request: Request) -> Result { let cookie = get_cookie(&request).ok_or(Error::from_str(404, "no-cook"))?; @@ -9,7 +17,28 @@ pub async fn delete(request: Request) -> Result { .and_then(|authority| authority.player()) .ok_or(Error::from_str(404, ""))?; + let players = request.state().players(); + log::debug!("player {} deleting account", player.id); - Ok("".into()) + players + .delete_one(db::doc! { "id": player.id.to_string() }, None) + .await + .map_err(|error| { + log::warn!("unable to delete player record: {}", error); + error + })?; + + log::debug!("player document '{}' deleted, queuing cleanup job", player.id); + + let job = bankah::jobs::TableJob::admin(bankah::jobs::TableAdminJob::CleanupPlayerData(player.id.to_string())); + request.state().queue(&job).await.map_err(|error| { + log::warn!("unable to schedule player cleanup job - {}", error); + error + })?; + + Body::from_json(&DeletionResponsePayload { + id: player.id.to_string(), + }) + .map(|body| Response::builder(200).body(body).build()) } diff --git a/workspace/stickbot/src/routes/auth.rs b/workspace/stickbot/src/routes/auth.rs index 964cc21..d6db9d7 100644 --- a/workspace/stickbot/src/routes/auth.rs +++ b/workspace/stickbot/src/routes/auth.rs @@ -104,6 +104,7 @@ fn mkplayer(userinfo: &UserInfo) -> std::io::Result { nickname, balance: 10000, emails: vec![userinfo.email.clone()], + tables: vec![], }; bson::to_bson(&state) diff --git a/workspace/stickbot/src/routes/tables.rs b/workspace/stickbot/src/routes/tables.rs index 6538d51..ff37f91 100644 --- a/workspace/stickbot/src/routes/tables.rs +++ b/workspace/stickbot/src/routes/tables.rs @@ -2,12 +2,21 @@ use async_std::stream::StreamExt; use serde::Deserialize; use crate::db::{doc, FindOneAndReplaceOptions, FindOneAndUpdateOptions, ReturnDocument}; -use crate::names; use crate::web::{cookie as get_cookie, Body, Error, Request, Response, Result}; +use crate::{constants, names}; use bankah::state::{PlayerState, TableState}; use twowaiyo::{Player, Table}; +fn can_join(ps: &PlayerState) -> bool { + let max = std::env::var(constants::STICKBOT_MAX_ACTIVE_TABLES_PER_PLAYER_ENV) + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(constants::STICKBOT_DEFAULT_MAX_ACTIVE_TABLES_PER_PLAYER); + + return ps.tables.len() < max; +} + // During conversion between our `twowaiyo` engine types, sever fields will be lost that need to be updated with the // correct state. fn sit_player(mut ts: TableState, mut ps: PlayerState) -> Result<(TableState, PlayerState)> { @@ -17,6 +26,9 @@ fn sit_player(mut ts: TableState, mut ps: PlayerState) -> Result<(TableState, Pl let next = TableState::from(&table); ps.balance = player.balance; + + ps.tables = ps.tables.drain(0..).chain(Some(ts.id.to_string())).collect(); + ts.roller = next.roller; ts.seats = next @@ -48,6 +60,10 @@ fn stand_player(mut ts: TableState, mut ps: PlayerState) -> Result<(TableState, let table = table.stand(&mut player); let next = TableState::from(&table); + if next.seats.contains_key(&ps.id) == false { + ps.tables = ps.tables.drain(0..).filter(|id| id != &ts.id.to_string()).collect(); + } + log::trace!("new player state - {:?}", player); ps.balance = player.balance; @@ -154,6 +170,11 @@ pub async fn join(mut request: Request) -> Result { .and_then(|auth| auth.player()) .ok_or(Error::from_str(404, "no-player"))?; + if can_join(&player) != true { + let body = Body::from_string("too-many-active-seats".into()); + return Ok(Response::builder(422).body(body).build()); + } + if player.balance == 0 { let body = Body::from_string("no-balance".into()); return Ok(Response::builder(422).body(body).build()); @@ -185,7 +206,7 @@ pub async fn join(mut request: Request) -> Result { players .find_one_and_update( doc! { "id": ps.id.to_string() }, - doc! { "$set": { "balance": ps.balance } }, + doc! { "$set": { "balance": ps.balance, "tables": ps.tables } }, opts, ) .await @@ -228,6 +249,11 @@ pub async fn create(request: Request) -> Result { .and_then(|auth| auth.player()) .ok_or(Error::from_str(404, "no-player"))?; + if can_join(&player) != true { + let body = Body::from_string("too-many-active-seats".into()); + return Ok(Response::builder(422).body(body).build()); + } + let tables = request.state().tables(); let players = request.state().players(); @@ -241,7 +267,7 @@ pub async fn create(request: Request) -> Result { // is being serialized and persisted as a string. Without the explicit `to_string` here, the query attempts to // search for a binary match of the `uuid:Uuid`. let query = doc! { "id": ps.id.to_string() }; - let updates = doc! { "$set": { "balance": ps.balance } }; + let updates = doc! { "$set": { "balance": ps.balance, "tables": ps.tables } }; let opts = FindOneAndUpdateOptions::builder() .return_document(ReturnDocument::After) .build(); @@ -334,7 +360,7 @@ pub async fn leave(mut request: Request) -> Result { players .update_one( doc! { "id": ps.id.to_string() }, - doc! { "$set": { "balance": ps.balance } }, + doc! { "$set": { "balance": ps.balance, "tables": ps.tables } }, None, ) .await diff --git a/workspace/twowaiyo/Cargo.toml b/workspace/twowaiyo/Cargo.toml index 3124d3d..54ede17 100644 --- a/workspace/twowaiyo/Cargo.toml +++ b/workspace/twowaiyo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "twowaiyo" -version = "0.3.3" +version = "0.3.4" edition = "2018" [lib] diff --git a/workspace/twowaiyo/src/player.rs b/workspace/twowaiyo/src/player.rs index 6a9c1ae..af51ca9 100644 --- a/workspace/twowaiyo/src/player.rs +++ b/workspace/twowaiyo/src/player.rs @@ -14,6 +14,7 @@ impl From<&Player> for PlayerState { id: state.id.clone(), balance: state.balance, emails: vec![], + tables: vec![], nickname: String::default(), oid: String::default(), }