diff --git a/Cargo.lock b/Cargo.lock index 78c1180..161a0fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,30 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -76,6 +100,15 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +[[package]] +name = "cc" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -88,9 +121,34 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", + "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "chrono-tz" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", + "serde", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", ] [[package]] @@ -103,6 +161,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.14" @@ -112,6 +176,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cron-parser" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec3fb0ecd16373b24b010094aba123e810f5f778b419bf826cb36919b8a42fe" +dependencies = [ + "chrono", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -273,6 +346,29 @@ dependencies = [ "http", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -359,7 +455,10 @@ dependencies = [ name = "nextgame" version = "0.1.0" dependencies = [ + "chrono", + "chrono-tz", "console_error_panic_hook", + "cron-parser", "getrandom", "hex", "minijinja", @@ -396,12 +495,59 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -471,6 +617,50 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -559,6 +749,18 @@ dependencies = [ "keccak", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -779,6 +981,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index e78495d..a59dbe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ getrandom = { version = "*", features = ["js"] } sha3 = { version = "0.10", default-features = false } hex = { version = "0.4" } pulldown-cmark = "0.12" +cron-parser = "0.9" +chrono = "0.4" +chrono-tz = { version = "0.10", features = ["serde"] } [profile.release] opt-level = "s" diff --git a/src/lib.rs b/src/lib.rs index e49447c..2fa4340 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ use std::collections::{HashMap, HashSet}; // use std::result::Result as StdRst; +use chrono::Utc; +use chrono_tz::Tz; use minijinja::{context as mjctx, Environment as MiniJinjaEnv}; use serde::{Deserialize, Serialize}; use worker::*; @@ -15,6 +17,9 @@ struct Team { name: String, secret: String, next_game: Option, + auto_expire_cron: Option, + #[serde(default)] + timezone: Tz, // NB: https://github.com/RReverser/serde-wasm-bindgen/issues/10 // so currently we need to manually serde_json it players: HashMap, @@ -35,7 +40,7 @@ struct Team { struct Game { description: String, // see Team struct comment - players: HashMap, // TODO: maybe??? + players: HashMap, guests: Vec, // TODO: comments?? } @@ -60,6 +65,7 @@ async fn main(req: Request, env: Env, _: Context) -> Result { .post_async("/admin/:teamkey/:teamsecret/player", add_player) .post_async("/admin/:teamkey/:teamsecret/player/:playerid/delete", delete_player) .post_async("/admin/:teamkey/:teamsecret/reset_game", reset_game) + .post_async("/admin/:teamkey/:teamsecret/cron", set_cron) .get_async("/team/:teamkey", team) .post_async("/team/:teamkey/new_game", new_game) .post_async("/team/:teamkey/player/:playerid/play", play) @@ -103,6 +109,8 @@ async fn new_team(req: Request, ctx: RouteContext) -> Result { name, secret, next_game: None, + auto_expire_cron: None, + timezone: Tz::default(), players: HashMap::new(), }; @@ -254,6 +262,7 @@ async fn reset_game(req: Request, ctx: RouteContext) -> Result let secret = ctx.param("teamsecret").unwrap(); let teams_kv = ctx.kv("teams")?; + let games_kv = ctx.kv("games")?; let mut team: Team = { let t = teams_kv.get(key).text().await?; @@ -268,7 +277,10 @@ async fn reset_game(req: Request, ctx: RouteContext) -> Result return auth_err; } - team.next_game = None; + if let Some(ng_key) = team.next_game.take() { + // try our best to clean up + let _ = games_kv.delete(&ng_key).await; + } return match teams_kv .put(key, serde_json::to_string(&team).unwrap())? @@ -285,6 +297,58 @@ async fn reset_game(req: Request, ctx: RouteContext) -> Result }; } +async fn set_cron(req: Request, ctx: RouteContext) -> Result { + let auth_err = Response::error("team not found", 404); + + let key = ctx.param("teamkey").unwrap(); + let secret = ctx.param("teamsecret").unwrap(); + + let teams_kv = ctx.kv("teams")?; + + let mut team: Team = { + let t = teams_kv.get(key).text().await?; + if t.is_none() { + return auth_err; + } + let t = t.unwrap(); + serde_json::from_str(&t).unwrap() + }; + + if &team.secret != secret { + return auth_err; + } + + let mut r = req.clone_mut()?; + let f = r.form_data().await?; + let tz = f.get_field("tz").map(|t| t.parse::().ok()).flatten(); + if tz.is_none() { + return Response::error("invalid time zone", 400); + } + let tz = tz.unwrap(); + + let cron = f.get_field("cron").unwrap_or_default(); + if cron_parser::parse(&cron, &Utc::now().with_timezone(&tz)).is_err() { + return Response::error("invalid cron expression", 400); + } + + team.auto_expire_cron = Some(cron); + team.timezone = tz; + + return match teams_kv + .put(key, serde_json::to_string(&team).unwrap())? + .execute() + .await + { + Ok(_) => { + let mut admin_link = req.url()?.clone(); + admin_link.set_path(&format!("/admin/{}/{}", key, secret)); + + Response::redirect(admin_link) + } + Err(_) => Response::error("failed to set cron and tz", 500), + }; +} + async fn team(_: Request, ctx: RouteContext) -> Result { let not_found = Response::error("team not found", 404); @@ -418,14 +482,15 @@ async fn new_game(req: Request, ctx: RouteContext) -> Result { let ng_key = random::hex_string(); - if games_kv - .put(&ng_key, serde_json::to_string(&ng).unwrap())? - // MAYBE: this may need adjustment - .expiration_ttl(30 * 86400) - .execute() - .await - .is_err() - { + let mut pob = games_kv.put(&ng_key, serde_json::to_string(&ng).unwrap())?; + + if let Some(cron) = team.auto_expire_cron.clone() { + if let Ok(exp) = cron_parser::parse(&cron, &Utc::now().with_timezone(&team.timezone)) { + pob = pob.expiration(exp.timestamp() as u64); + } + } + + if pob.execute().await.is_err() { return Response::error("failed to create next game", 500); } diff --git a/templates/team.html b/templates/team.html index e750341..33e57ca 100644 --- a/templates/team.html +++ b/templates/team.html @@ -20,7 +20,7 @@

-
+
{{ description|safe }}
diff --git a/templates/team_admin.html b/templates/team_admin.html index e092bff..e4ee94f 100644 --- a/templates/team_admin.html +++ b/templates/team_admin.html @@ -17,7 +17,7 @@

{% if team.next_game %} -
+
@@ -31,6 +31,39 @@

{% endif %}
+ +
+ +
+ +
+

Time to EXPIRE the game - set to a time after the game finishes. Use this + to help with the cron expression. (Note this does not affect existing games, you may need to manually reset + it)

+
+ +
+ +
+ +
+

Use "TZ Identifier" listed here.

+
+ +
+
+ +
+
+ +
+

+ +
+
@@ -44,11 +77,15 @@

-
-
- +
+ + + + + + {% for id, pn in team.players|items %}
Manage