commit 0da2212258fe3f18ebc0973019344d6e4f95d716 Author: Anna Clemens Date: Wed Aug 18 01:45:44 2021 -0400 chore: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce9e957 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/config.toml +/data.json diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..22b5b51 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2146 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +dependencies = [ + "getrandom 0.2.3", + "once_cell", + "version_check", +] + +[[package]] +name = "aliri_braid" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4961918d91b2182cf3adc6e3cd8d6f44452e0f42db367691447af31afdd1ad7d" +dependencies = [ + "aliri_braid_impl", +] + +[[package]] +name = "aliri_braid_impl" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e21d6298c933984056b381a70c7248a97370b76dfdbc95dc3da0fcbcd994342" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anyhow" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "askama" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134" +dependencies = [ + "askama_derive", + "askama_escape", + "askama_shared", + "mime", + "mime_guess", +] + +[[package]] +name = "askama_derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522" +dependencies = [ + "askama_shared", + "proc-macro2", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb" + +[[package]] +name = "askama_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2582b77e0f3c506ec4838a25fa8a5f97b9bed72bb6d3d272ea1c031d8bd373bc" +dependencies = [ + "askama_escape", + "humansize", + "nom", + "num-traits", + "percent-encoding", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", +] + +[[package]] +name = "askama_warp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b48958817b96cb6898533d3e0c17cfb5d4dba1681a7a236ed4d8f13972e2054" +dependencies = [ + "askama", + "warp", +] + +[[package]] +name = "async-trait" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da1976d75adbe5fbc88130ecd119529cf1cc6a93ae1546d8696ee66f0d21af1" + +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi", +] + +[[package]] +name = "clemsbot" +version = "0.1.0" +dependencies = [ + "anyhow", + "askama", + "askama_warp", + "chrono", + "futures", + "irc", + "parking_lot", + "reqwest", + "rhai", + "serde", + "serde_json", + "serde_with", + "tokio", + "tokio-tungstenite 0.15.0", + "toml", + "twitch_api2", + "url", + "warp", +] + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "darling" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "futures" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" + +[[package]] +name = "futures-executor" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" + +[[package]] +name = "futures-macro" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" +dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" + +[[package]] +name = "futures-task" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" + +[[package]] +name = "futures-util" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +dependencies = [ + "autocfg", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "h2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "headers" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" +dependencies = [ + "base64", + "bitflags", + "bytes", + "headers-core", + "http", + "mime", + "sha-1", + "time", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" + +[[package]] +name = "httpdate" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" + +[[package]] +name = "humansize" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" + +[[package]] +name = "hyper" +version = "0.14.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "input_buffer" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" +dependencies = [ + "bytes", +] + +[[package]] +name = "instant" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + +[[package]] +name = "irc" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5510c4c4631e53c57d6b05c44ab8447d1db6beef28fb9d12c4d6a46fad9dfcc" +dependencies = [ + "chrono", + "encoding", + "futures-util", + "irc-proto", + "log", + "native-tls", + "parking_lot", + "pin-project", + "serde", + "serde_derive", + "thiserror", + "tokio", + "tokio-native-tls", + "tokio-stream", + "tokio-util", + "toml", +] + +[[package]] +name = "irc-proto" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55fa0a52d825e59ba8aea5b7503890245aea000f77e68d9b1903f3491fa33643" +dependencies = [ + "bytes", + "encoding", + "thiserror", + "tokio", + "tokio-util", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "js-sys" +version = "0.3.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" + +[[package]] +name = "lock_api" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "multipart" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050aeedc89243f5347c3e237e3e13dc76fbe4ae3742a57b94dc14f69acf76d4" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand 0.7.3", + "safemem", + "tempfile", + "twoway", +] + +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rhai" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37515c01bf9aa516d84240740bd5d02f19b04a112e537ae43807042c84ef8ed2" +dependencies = [ + "ahash", + "instant", + "num-traits", + "rhai_codegen", + "smallvec", + "smartstring", +] + +[[package]] +name = "rhai_codegen" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a340b02636b22e61d94ee53e6bcc2d01d339958766b2003b860d178ccf5ae5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +dependencies = [ + "openssl-probe", + "rustls", + "schannel", + "security-framework", +] + +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f6109f0506e20f7e0f910e51a0079acf41da8e0694e6442527c4ddf5a2b158" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad9fdbb69badc8916db738c25efd04f0a65297d26c2f8de4b62e57b8c12bc72" +dependencies = [ + "chrono", + "rustversion", + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1569374bd54623ec8bd592cf22ba6e03c0f177ff55fbc8c29a49e296e7adecf" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha-1" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "smartstring" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31aa6a31c0c2b21327ce875f7e8952322acfcfd0c27569a6e18a647281352c9b" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "socket2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite 0.12.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" +dependencies = [ + "futures-util", + "log", + "pin-project", + "rustls", + "tokio", + "tokio-rustls", + "tungstenite 0.14.0", + "webpki", + "webpki-roots", +] + +[[package]] +name = "tokio-util" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "tungstenite" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "input_buffer", + "log", + "rand 0.8.4", + "sha-1", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand 0.8.4", + "rustls", + "rustls-native-certs", + "sha-1", + "thiserror", + "url", + "utf-8", + "webpki", +] + +[[package]] +name = "twitch_api2" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003e4481fa1bc7cf6587dd212a4ce5f3b69dee893c9f7fdf8a0ed7149f470f1b" +dependencies = [ + "aliri_braid", + "async-trait", + "displaydoc", + "futures", + "http", + "once_cell", + "reqwest", + "serde", + "serde_derive", + "serde_json", + "serde_path_to_error", + "serde_repr", + "thiserror", + "twitch_oauth2", + "typed-builder", + "url", + "version_check", +] + +[[package]] +name = "twitch_oauth2" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3ac686c0fd364beafa745e4471c602d945ff5e1962e36e27c38ad92660e929" +dependencies = [ + "aliri_braid", + "async-trait", + "base64", + "displaydoc", + "http", + "once_cell", + "rand 0.8.4", + "reqwest", + "serde", + "serde_json", + "thiserror", + "url", + "version_check", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "typed-builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345426c7406aa355b60c5007c79a2d1f5b605540072795222f17f6443e6a9c6f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332d47745e9a0c38636dbd454729b147d16bd1ed08ae67b3ab281c4506771054" +dependencies = [ + "bytes", + "futures", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "multipart", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite 0.13.0", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" +dependencies = [ + "cfg-if", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16646b21c3add8e13fdb8f20172f8a28c3dbf62f45406bcff0233188226cfe0c" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2" + +[[package]] +name = "web-sys" +version = "0.3.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..25f4942 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "clemsbot" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" +askama = { version = "0.10", features = ["with-warp"] } +askama_warp = "0.11" +chrono = "0.4" +futures = "0.3" +irc = "0.15" +parking_lot = "0.11" +reqwest = "0.11" +rhai = { version = "1", features = ["sync"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_with = { version = "1", features = ["chrono"] } +toml = "0.5" +tokio-tungstenite = { version = "0.15", features = ["rustls-tls"] } +twitch_api2 = { version = "0.6.0-rc.2", features = ["twitch_oauth2", "client", "reqwest_client", "helix", "pubsub"] } +url = "2" +warp = "0.3" + +[dependencies.tokio] +version = "1" +default-features = false +features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"] diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..93d3093 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,323 @@ +pub mod config; +pub mod rhai_tools; +pub mod twitch; +pub mod user_config; +pub mod web; + +use rhai::Engine; +use irc::client::prelude::{Client as IrcClient, Config as IrcConfig, Capability}; +use anyhow::Result; +use twitch_api2::TwitchClient; +use crate::app::{ + config::Config, + rhai_tools::{ExecutorState, ExecutorOutput}, + user_config::UserConfig, + twitch::Twitch, +}; +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, +}; +use futures::{StreamExt, SinkExt}; +use twitch_api2::twitch_oauth2::{ClientSecret, UserToken, TwitchToken, ClientId, RefreshToken, AccessToken}; +use twitch_api2::helix::Scope; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; +use twitch_api2::types::UserId; +use tokio::sync::mpsc::UnboundedSender; +use tokio::runtime::Handle; +use std::collections::HashMap; +use twitch_api2::pubsub::Topic; +use tokio_tungstenite::tungstenite::Message as WsMessage; + +pub struct State { + pub user_config: UserConfig, + pub config: RwLock, + + pub channel_name: String, + + pub twitch: Arc, + pub rewards_paused: RwLock>, + + pub irc: IrcClient, + pub irc_queue: UnboundedSender, + + pub rhai: Engine, + pub script_cache: parking_lot::RwLock>, + pub runtime: Handle, +} + +impl State { + #[allow(unreachable_code)] + pub async fn new(runtime: Handle, user_config: UserConfig, mut config: Config) -> Result> { + let http_client = reqwest::Client::new(); + + println!("Verifying bot token"); + verify_token(&mut config.bot_token, &user_config, &http_client, vec![ + // IRC + Scope::ChatRead, + Scope::ChatEdit, + ]).await?; + println!("Bot token ready"); + + println!("Verifying user token"); + verify_token(&mut config.user_token, &user_config, &http_client, vec![ + // Channel points redemptions + Scope::ChannelReadRedemptions, + Scope::ChannelManageRedemptions, + + // Mod stuff + Scope::ChannelModerate, + Scope::ModerationRead, + Scope::ModeratorManageAutoMod, + ]).await?; + println!("User token ready"); + + let twitch_client = TwitchClient::with_client(http_client); + let twitch = Arc::new(Twitch { + client: twitch_client, + bot_token: config.bot_token.clone().unwrap().token, + user_token: config.user_token.clone().unwrap().token, + }); + + let user_id = UserId::new(user_config.twitch.channel_id.to_string()); + let channel_name = twitch.client.helix.get_user_from_id(user_id, &twitch.bot_token) + .await? + .ok_or_else(|| anyhow::anyhow!("no channel for id {}", user_config.twitch.channel_id))? + .login + .to_string(); + + let irc_config = IrcConfig { + server: Some("irc.chat.twitch.tv".into()), + port: Some(6697), + username: Some(twitch.bot_token.login.clone()), + nickname: Some(twitch.bot_token.login.clone()), + password: Some(format!("oauth:{}", twitch.bot_token.access_token.secret())), + ..Default::default() + }; + let mut irc = IrcClient::from_config(irc_config).await?; + irc.send_cap_req(&[ + Capability::Custom("twitch.tv/tags"), + Capability::Custom("twitch.tv/commands"), + ])?; + irc.identify()?; + let mut irc_stream = irc.stream()?; + + let mut rhai = Engine::new(); + rhai.set_max_expr_depths(0, 0); + rhai.register_type::() + .register_get("initiator", ExecutorState::initiator) + .register_get("initiator_id", ExecutorState::initiator_id) + .register_get("args", ExecutorState::args) + .register_fn("get_username", ExecutorState::get_username::<&str>) + .register_fn("get_user_id", ExecutorState::get_user_id::<&str>) + .register_fn("get_channel_info", ExecutorState::get_channel_info::<&str>); + rhai.register_type::() + .register_fn("send", ExecutorOutput::send::<&str>); + + let (queue_tx, mut queue_rx) = tokio::sync::mpsc::unbounded_channel(); + + let state = Arc::new(Self { + user_config, + config: RwLock::new(config), + + channel_name, + + twitch, + rewards_paused: Default::default(), + + irc, + irc_queue: queue_tx, + + rhai, + script_cache: Default::default(), + runtime, + }); + + // start web task + tokio::task::spawn(crate::app::web::start_web(Arc::clone(&state))); + + // start pubsub + let redemption_topic = twitch_api2::pubsub::channel_points::ChannelPointsChannelV1 { + channel_id: state.user_config.twitch.channel_id as u32, + }.into_topic(); + let auth_token: Option = state.config + .read() + .await + .user_token + .as_ref() + .map(|token| token.access_token.secret().to_string()); + let listen_command = twitch_api2::pubsub::listen_command( + &[redemption_topic], + auth_token.as_deref(), + "1", + )?; + let task_state = Arc::clone(&state); + tokio::task::spawn(async move { + let res: Result<()> = try { + let (ws, _) = tokio_tungstenite::connect_async("wss://pubsub-edge.twitch.tv").await?; + let (mut write, mut read) = ws.split(); + write.send(WsMessage::Text(listen_command)).await?; + let mut ping = tokio::time::interval(chrono::Duration::minutes(2).to_std().unwrap()); + + loop { + tokio::select! { + _ = ping.tick() => { + write.send(WsMessage::Ping(vec![1, 2, 3, 4])).await?; + write.send(WsMessage::Text(r#"{"type":"PING"}"#.into())).await?; + }, + message = read.next() => { + if let Some(Ok(message)) = message { + if let Err(e) = crate::app::twitch::handle_pubsub(Arc::clone(&task_state), message).await { + eprintln!("error in pubsub: {:?}", e); + } + } + }, + } + } + }; + + if let Err(e) = res { + eprintln!("error connecting to websocket: {:?}", e); + } + }); + + // start irc task + let task_state = Arc::clone(&state); + tokio::task::spawn(async move { + // FIXME: handle reconnects + while let Some(event) = irc_stream.next().await.transpose()? { + let task_state = Arc::clone(&task_state); + if let Err(e) = crate::app::twitch::handle_irc_event(task_state, event).await { + eprintln!("irc error: {:?}", e); + } + } + + Result::<(), anyhow::Error>::Ok(()) + }); + + // start irc message queue + let task_state = Arc::clone(&state); + tokio::task::spawn(async move { + let channel = format!("#{}", task_state.channel_name); + + while let Some(message) = queue_rx.recv().await { + if let Err(e) = task_state.irc.send_privmsg(&channel, message) { + eprintln!("error sending message: {:?}", e); + } + } + }); + + Ok(state) + } +} + +#[derive(Clone)] +pub struct FullUserToken { + pub token: UserToken, + pub client_id: ClientId, + pub client_secret: ClientSecret, +} + +impl<'de> Deserialize<'de> for FullUserToken { + fn deserialize(de: D) -> std::result::Result + where D: serde::de::Deserializer<'de>, { + let (refresh, access, login, user_id, client_id, client_secret, scopes): (RefreshToken, AccessToken, _, _, ClientId, ClientSecret, Vec) = Deserialize::deserialize(de)?; + let token = UserToken::from_existing_unchecked(access, refresh, client_id.clone(), client_secret.clone(), login, user_id, Some(scopes), None); + Ok(FullUserToken { + token, + client_id, + client_secret, + }) + } +} + +impl Serialize for FullUserToken { + fn serialize(&self, ser: S) -> std::result::Result + where S: serde::ser::Serializer, + { + let to_ser = (&self.token.refresh_token, &self.token.access_token, &self.token.login, &self.token.user_id, &self.client_id, &self.client_secret, self.token.scopes()); + to_ser.serialize(ser) + } +} + +impl Deref for FullUserToken { + type Target = UserToken; + + fn deref(&self) -> &Self::Target { + &self.token + } +} + +impl DerefMut for FullUserToken { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.token + } +} + +pub fn run_rhai>(state: Arc, script: S, fn_state: ExecutorState) { + let script = script.into(); + tokio::task::spawn_blocking(move || { + let res: anyhow::Result<()> = try { + let mut scope = rhai::Scope::new(); + let ast = state.script_cache.read().get(&script).map(ToOwned::to_owned); + let ast = match ast { + Some(ast) => ast, + None => { + let ast = state.rhai.compile(&script)?; + state.script_cache.write().insert(script.clone(), ast.clone()); + ast + }, + }; + + let mut output = rhai::Dynamic::from(ExecutorOutput::default()); + + state.rhai.call_fn_dynamic( + &mut scope, + &ast, + true, + "run", + Some(&mut output), + [rhai::Dynamic::from(fn_state)], + )?; + + let output: ExecutorOutput = output.cast(); + for message in output.to_send { + state.irc_queue.send(message).ok(); + } + }; + + if let Err(e) = res { + eprintln!("error in rhai script: {:?}", e); + } + }); +} + +async fn verify_token(full_token: &mut Option, user_config: &UserConfig, http_client: &reqwest::Client, scopes: Vec) -> anyhow::Result<()> { + match full_token { + Some(t) => if t.validate_token(http_client).await.is_err() { + println!("Refreshing token"); + t.refresh_token(http_client).await?; + }, + None => { + let mut builder = UserToken::builder( + user_config.twitch.client_id.clone(), + user_config.twitch.client_secret.clone(), + url::Url::parse("http://localhost/").unwrap(), + ).set_scopes(scopes); + let (url, csrf) = builder.generate_url(); + println!("go to {}", url); + println!("once done, paste code:"); + let mut code = String::new(); + std::io::stdin().read_line(&mut code)?; + let token: UserToken = builder.get_user_token(http_client, csrf.as_str(), code.trim()).await?; + *full_token = Some(FullUserToken { + token, + client_id: user_config.twitch.client_id.clone(), + client_secret: user_config.twitch.client_secret.clone(), + }); + } + }; + + Ok(()) +} diff --git a/src/app/config.rs b/src/app/config.rs new file mode 100644 index 0000000..120187f --- /dev/null +++ b/src/app/config.rs @@ -0,0 +1,44 @@ +use chrono::Duration; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DurationSeconds}; +use crate::app::FullUserToken; + +#[derive(Deserialize, Serialize, Default)] +pub struct Config { + #[serde(default)] + pub bot_token: Option, + #[serde(default)] + pub user_token: Option, + pub commands: Vec, + pub redemptions: Vec, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Command { + pub name: String, + pub aliases: Vec, + pub cooldowns: Cooldowns, + pub executor: CommandExecutor, +} + +#[derive(Deserialize, Serialize, Clone)] +pub enum CommandExecutor { + Text(String), + Rhai(String), +} + +#[serde_as] +#[derive(Deserialize, Serialize, Default, Clone)] +pub struct Cooldowns { + #[serde_as(as = "Option>")] + pub global: Option, + #[serde_as(as = "Option>")] + pub user: Option, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Redemption { + pub name: String, + pub twitch_id: twitch_api2::types::RewardId, + pub rhai: String, +} diff --git a/src/app/rhai_tools.rs b/src/app/rhai_tools.rs new file mode 100644 index 0000000..c5e63fa --- /dev/null +++ b/src/app/rhai_tools.rs @@ -0,0 +1,96 @@ +use crate::app::twitch::Twitch; +use std::sync::Arc; +use twitch_api2::types::{UserId, UserName}; +use tokio::runtime::Handle; +use twitch_api2::helix::channels::{GetChannelInformationRequest, ChannelInformation}; + +#[derive(Default, Clone)] +pub struct ExecutorOutput { + pub to_send: Vec, +} + +impl ExecutorOutput { + pub fn send>(&mut self, s: S) { + self.to_send.push(s.into()); + } +} + +#[derive(Clone)] +pub struct ExecutorState { + pub runtime: Handle, + pub twitch: Arc, + + pub args: Vec, + + pub initiator: String, + pub initiator_id: UserId, + // TODO + // pub moderator: bool, + // pub broadcaster: bool, + // pub vip: bool, +} + +impl ExecutorState { + // FIXME: make this return &str + pub fn initiator(&mut self) -> String { + self.initiator.clone() + } + + pub fn initiator_id(&mut self) -> String { + self.initiator_id.to_string() + } + + pub fn args(&mut self) -> rhai::Array { + self.args.clone() + } + + async fn internal_get_username(&self, id: UserId) -> Option { + self.twitch.client.helix.get_user_from_id(id, &self.twitch.bot_token) + .await + .ok()? + .map(|user| user.login.to_string()) + } + + pub fn get_username>(&mut self, id: S) -> rhai::Dynamic { + match self.runtime.block_on(self.internal_get_username(UserId::new(id.into()))) { + Some(x) => rhai::Dynamic::from(x), + None => rhai::Dynamic::from(()), + } + } + + async fn internal_get_id>(&self, username: S) -> Option { + self.twitch.client.helix.get_user_from_login(username, &self.twitch.bot_token) + .await + .ok()? + .map(|user| user.id) + } + + pub fn get_user_id>(&mut self, username: S) -> rhai::Dynamic { + match self.runtime.block_on(self.internal_get_id(username)).map(|user| user.to_string()) { + Some(x) => rhai::Dynamic::from(x), + None => rhai::Dynamic::from(()), + } + } + + async fn internal_get_channel_info>(&self, id: S) -> Option<(String, String, String)> { + let req = GetChannelInformationRequest::builder() + .broadcaster_id(id.into()) + .build(); + self.twitch.client.helix.req_get(req, &self.twitch.bot_token) + .await + .ok()? + .data + .map(|info: ChannelInformation| (info.broadcaster_name.to_string(), info.broadcaster_login.to_string(), info.game_name.to_string())) + } + + pub fn get_channel_info>(&mut self, id: S) -> rhai::Dynamic { + match self.runtime.block_on(self.internal_get_channel_info(id)) { + Some(x) => rhai::Dynamic::from(vec![ + rhai::Dynamic::from(x.0), + rhai::Dynamic::from(x.1), + rhai::Dynamic::from(x.2), + ]), + None => rhai::Dynamic::from(()), + } + } +} diff --git a/src/app/twitch.rs b/src/app/twitch.rs new file mode 100644 index 0000000..4b143e0 --- /dev/null +++ b/src/app/twitch.rs @@ -0,0 +1,128 @@ +use twitch_api2::{ + TwitchClient, + types::UserId, + twitch_oauth2::UserToken, +}; +use irc::client::prelude::Message; +use irc::proto::{Command, Response}; +use crate::app::State; +use crate::app::config::CommandExecutor; +use crate::app::rhai_tools::ExecutorState; +use std::sync::Arc; +use tokio_tungstenite::tungstenite::Message as WsMessage; + +pub struct Twitch { + pub client: TwitchClient<'static, reqwest::Client>, + pub bot_token: UserToken, + pub user_token: UserToken, +} + +pub async fn handle_irc_event(state: Arc, event: Message) -> anyhow::Result<()> { + let channel_name = format!("#{}", state.channel_name); + + match &event.command { + Command::Response(resp, _) if *resp == Response::RPL_WELCOME => { + state.irc.send_join(&channel_name)?; + }, + // FIXME: do correct checking here + Command::PRIVMSG(channel, message) if *channel == channel_name => { + on_privmsg(state, &event, &message).await?; + }, + _ => { + // eprintln!("{:#?}", c); + }, + } + + Ok(()) +} + +async fn on_privmsg(state: Arc, event: &Message, message: &str) -> anyhow::Result<()> { + let initiator = event.source_nickname() + .ok_or_else(|| anyhow::anyhow!("missing source"))? + .to_string(); + + let initiator_id = event.tags + .as_ref() + .ok_or_else(|| anyhow::anyhow!("missing tags"))? + .iter() + .find(|tag| tag.0 == "user-id") + .and_then(|tag| tag.1.clone()) + .ok_or_else(|| anyhow::anyhow!("missing user id"))?; + + let words: Vec<&str> = message.split(' ').collect(); + let command_name = words[0]; + let args = words[1..].iter() + .map(ToString::to_string) + .map(rhai::Dynamic::from) + .collect(); + + let command = state.config.read() + .await + .commands + .iter().find(|command| command.name == command_name || command.aliases.iter().any(|alias| alias == command_name)) + .map(Clone::clone); + let command = match command { + Some(c) => c, + None => return Ok(()) + }; + + match &command.executor { + CommandExecutor::Text(t) => { state.irc_queue.send(t.to_string()).ok(); }, + CommandExecutor::Rhai(t) => { + let fn_state = ExecutorState { + twitch: Arc::clone(&state.twitch), + initiator_id: UserId::new(initiator_id), + initiator, + args, + runtime: state.runtime.clone(), + }; + crate::app::run_rhai(Arc::clone(&state), t, fn_state); + }, + } + + Ok(()) +} + +use twitch_api2::pubsub::{ + Response as PubSubResponse, + TopicData, + channel_points::ChannelPointsChannelV1Reply, +}; + +pub async fn handle_pubsub(state: Arc, event: WsMessage) -> anyhow::Result<()> { + let json = match event { + WsMessage::Text(json) => json, + _ => return Ok(()), + }; + + let response = twitch_api2::pubsub::Response::parse(&json)?; + + let reply = match response { + PubSubResponse::Message { data: TopicData::ChannelPointsChannelV1 { reply, .. } } => reply, + _ => return Ok(()), + }; + + let redemption = match *reply { + ChannelPointsChannelV1Reply::RewardRedeemed { redemption, .. } => redemption, + _ => return Ok(()), + }; + + let action = match state.config.read().await.redemptions.iter().find(|re| re.twitch_id == redemption.reward.id).map(Clone::clone) { + Some(a) => a, + None => return Ok(()), + }; + + let args = redemption.user_input + .map(|input| input.split(' ').map(ToOwned::to_owned).map(rhai::Dynamic::from).collect()) + .unwrap_or_default(); + let fn_state = ExecutorState { + twitch: Arc::clone(&state.twitch), + initiator: redemption.user.login.to_string(), + initiator_id: redemption.user.id, + args, + runtime: state.runtime.clone(), + }; + crate::app::run_rhai(Arc::clone(&state), &action.rhai, fn_state); + + Ok(()) +} diff --git a/src/app/user_config.rs b/src/app/user_config.rs new file mode 100644 index 0000000..e2554ea --- /dev/null +++ b/src/app/user_config.rs @@ -0,0 +1,20 @@ +use serde::Deserialize; +use twitch_api2::twitch_oauth2::{ClientId, ClientSecret}; + +#[derive(Deserialize)] +pub struct UserConfig { + pub twitch: Twitch, + pub bot: Bot, +} + +#[derive(Deserialize)] +pub struct Twitch { + pub client_id: ClientId, + pub client_secret: ClientSecret, + pub channel_id: u64, +} + +#[derive(Deserialize)] +pub struct Bot { + pub access_token: String, +} diff --git a/src/app/web.rs b/src/app/web.rs new file mode 100644 index 0000000..01d0b28 --- /dev/null +++ b/src/app/web.rs @@ -0,0 +1,71 @@ +mod template; +mod route; + +use std::sync::Arc; +use crate::app::State; +use warp::{Filter, Rejection, Reply}; +use std::convert::Infallible; +use warp::http::StatusCode; +use std::borrow::Cow; +use self::route::*; + +pub async fn start_web(state: Arc) { + let cookie_state = Arc::clone(&state); + let authed = warp::cookie("access_token") + .or(warp::header("x-api-key")) + .unify() + .and_then(move |access_token: String| { + let state = Arc::clone(&cookie_state); + async move { + if access_token == state.user_config.bot.access_token { + Ok(()) + } else { + Err(warp::reject::custom(CustomRejection::InvalidAccessToken)) + } + } + }) + .untuple_one() + .and( + commands_routes(Arc::clone(&state)) + .or(redemptions_routes(Arc::clone(&state))) + .or(livesplit_routes(Arc::clone(&state))) + ); + + let unauthed = access_token_routes(); + + let routes = authed.or(unauthed).recover(handle_rejection); + + warp::serve(routes) + .run(([0, 0, 0, 0], 8000)) + .await; +} + +#[derive(Debug)] +enum CustomRejection { + InvalidAccessToken, + InvalidForm, + TwitchError, +} + +impl warp::reject::Reject for CustomRejection {} + +async fn handle_rejection(err: Rejection) -> Result { + let (code, message) = if err.is_not_found() { + (StatusCode::NOT_FOUND, Cow::from("404 - Not Found")) + } else if let Some(custom) = err.find::() { + match custom { + CustomRejection::InvalidAccessToken => (StatusCode::UNAUTHORIZED, Cow::from("invalid access token")), + CustomRejection::InvalidForm => (StatusCode::BAD_REQUEST, Cow::from("invalid form submission")), + CustomRejection::TwitchError => (StatusCode::INTERNAL_SERVER_ERROR, Cow::from("twitch error")), + // CustomRejection::Askama(e) => (StatusCode::INTERNAL_SERVER_ERROR, Cow::from(format!("templating error: {:#?}", e))), + } + } else if let Some(e) = err.find::() { + (StatusCode::BAD_REQUEST, Cow::from(format!("missing `{}` cookie", e.name()))) + } else if let Some(e) = err.find::() { + (StatusCode::BAD_REQUEST, Cow::from(format!("missing `{}` header", e.name()))) + } else { + (StatusCode::INTERNAL_SERVER_ERROR, Cow::from(format!("unhandled error: {:#?}", err))) + }; + + Ok(warp::reply::with_status(message, code)) +} diff --git a/src/app/web/route/access_token.rs b/src/app/web/route/access_token.rs new file mode 100644 index 0000000..4c43fa9 --- /dev/null +++ b/src/app/web/route/access_token.rs @@ -0,0 +1,40 @@ +use warp::{ + Filter, Reply, + filters::BoxedFilter, + http::Uri, +}; +use crate::app::web::{ + CustomRejection, + template::index::IndexTemplate, +}; +use std::collections::HashMap; + +pub fn access_token_routes() -> BoxedFilter<(impl Reply, )> { + warp::get().and(access_token_page()) + .or(warp::post().and(access_token_submit())) + .boxed() +} + +fn access_token_page() -> BoxedFilter<(impl Reply, )> { + warp::path::end() + .map(|| IndexTemplate) + .boxed() +} + +fn access_token_submit() -> BoxedFilter<(impl Reply, )> { + warp::path::end() + .and(warp::body::content_length_limit(1024)) + .and(warp::body::form()) + .and_then(|form: HashMap| async move { + let token = match form.get("access_token") { + Some(token) => token, + None => return Err(warp::reject::custom(CustomRejection::InvalidForm)), + }; + Ok(warp::reply::with_header( + warp::redirect(Uri::from_static("/")), + "Set-Cookie", + format!("access_token={}; SameSite=Lax; Secure; HttpOnly", token), + )) + }) + .boxed() +} diff --git a/src/app/web/route/commands.rs b/src/app/web/route/commands.rs new file mode 100644 index 0000000..29d5828 --- /dev/null +++ b/src/app/web/route/commands.rs @@ -0,0 +1,118 @@ +use warp::{ + Filter, Reply, + filters::BoxedFilter, + http::Uri, +}; +use crate::app::{ + State, + config::{CommandExecutor, Command, Cooldowns}, + web::{ + CustomRejection, + template::commands::{CommandsTemplate, AddCommandTemplate}, + }, +}; +use std::{ + collections::HashMap, + convert::Infallible, + sync::Arc, +}; + +pub fn commands_routes(state: Arc) -> BoxedFilter<(impl Reply, )> { + warp::get() + .and( + commands_get(Arc::clone(&state)) + .or(commands_add_get()) + ) + .or(warp::post().and( + commands_add_post(Arc::clone(&state)) + .or(commands_delete_post(Arc::clone(&state))) + )) + .boxed() +} + +fn commands_get(state: Arc) -> BoxedFilter<(impl Reply, )> { + warp::path("commands") + .and(warp::path::end()) + .and_then(move || { + let state = Arc::clone(&state); + async move { + Result::::Ok(CommandsTemplate { + commands: state.config.read().await.commands.clone(), + }) + } + }) + .boxed() +} + +fn commands_add_get() -> BoxedFilter<(impl Reply, )> { + warp::path("commands") + .and(warp::path("add")) + .and(warp::path::end()) + .map(|| AddCommandTemplate) + .boxed() +} + +fn commands_add_post(state: Arc) -> BoxedFilter<(impl Reply, )> { + warp::path("commands") + .and(warp::path("add")) + .and(warp::path::end()) + .and(warp::body::content_length_limit(1024 * 5)) + .and(warp::body::form()) + .and_then(move |mut form: HashMap| { + let state = Arc::clone(&state); + async move { + let form_get = try { + let name = form.remove("name")?; + let aliases = form.remove("aliases")?; + let kind = form.remove("type")?; + let script = form.remove("executor_data")?; + (name, aliases, kind, script) + }; + + let (name, aliases, kind, script) = match form_get { + Some(x) => x, + None => return Err(warp::reject::custom(CustomRejection::InvalidForm)), + }; + + let executor = match &*kind { + "Text" => CommandExecutor::Text(script), + "Rhai" => CommandExecutor::Rhai(script), + _ => return Err(warp::reject::custom(CustomRejection::InvalidForm)), + }; + + let command = Command { + name, + executor, + aliases: aliases.split("\n").map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect(), + cooldowns: Cooldowns::default(), + }; + + state.config.write().await.commands.push(command); + + Ok(warp::redirect(Uri::from_static("/commands"))) + } + }) + .boxed() +} + +fn commands_delete_post(state: Arc) -> BoxedFilter<(impl Reply, )> { + warp::path("commands") + .and(warp::path("delete")) + .and(warp::path::end()) + .and(warp::body::content_length_limit(1024 * 5)) + .and(warp::body::form()) + .and_then(move |mut form: HashMap| { + let state = Arc::clone(&state); + async move { + let name = match form.remove("name") { + Some(n) => n, + None => return Err(warp::reject::custom(CustomRejection::InvalidForm)), + }; + + state.config.write().await.commands.drain_filter(|command| command.name == name); + + Ok(warp::redirect(Uri::from_static("/commands"))) + } + }) + .boxed() +} diff --git a/src/app/web/route/livesplit.rs b/src/app/web/route/livesplit.rs new file mode 100644 index 0000000..4c0156c --- /dev/null +++ b/src/app/web/route/livesplit.rs @@ -0,0 +1,169 @@ +use futures::Future; +use serde::Deserialize; +use twitch_api2::helix::points::{UpdateCustomRewardRequest, UpdateCustomRewardBody}; +use warp::{Filter, Reply, filters::BoxedFilter, Rejection}; +use crate::app::{ + State, + web::CustomRejection, +}; +use std::sync::Arc; + +pub fn livesplit_routes(state: Arc) -> BoxedFilter<(impl Reply, )> { + warp::post() + .and( + livesplit_start(Arc::clone(&state)) + .or(livesplit_split(Arc::clone(&state))) + .or(livesplit_reset(Arc::clone(&state))) + .or(livesplit_finish(Arc::clone(&state))) + ) + .boxed() +} + +struct RewardPauseInfo { + id: &'static str, + games: &'static [(&'static str, isize)], +} + +const REWARDS: &[RewardPauseInfo] = &[ + // reset + RewardPauseInfo { + id: "8ac47f16-2396-4c2d-9c43-d30bd9074a4f", + games: &[ + // in RCT1, only enable after the third split + ("RollerCoaster Tycoon 1", 3), + ], + }, + + // no reset + RewardPauseInfo { + id: "0da50268-3237-4832-9849-9baac518e4de", + games: &[ + // in RCT1, only enable after the third split + ("RollerCoaster Tycoon 1", 3), + ], + }, +]; + +async fn set_reward_paused(state: Arc, id: String, paused: bool) -> anyhow::Result<()> { + let request = UpdateCustomRewardRequest::builder() + .broadcaster_id(state.user_config.twitch.channel_id.to_string()) + .id(id) + .build(); + let body = UpdateCustomRewardBody::builder() + .is_paused(paused) + .build(); + + state.twitch.client.helix.req_patch(request, body, &state.twitch.user_token).await?; + Ok(()) +} + +async fn set_rewards_paused(state: Arc, data: LiveSplitBody, paused: bool) -> Vec> { + let mut results = Vec::with_capacity(REWARDS.len()); + for info in REWARDS { + let is_paused = state.rewards_paused.read().await.get(info.id).copied(); + let should_apply = match info.games.iter().find(|(name, _)| data.run.game_name.as_deref().map(|run_name| run_name == *name).unwrap_or_default()) { + Some((_, split_idx)) => { + // if we're unpausing and the current split index is gte to the configured index for this game + if !paused && data.current_split_index >= *split_idx { + true + } else if !paused && data.current_split_index < *split_idx { + false + } else { + true + } + }, + _ => true, + }; + + if !should_apply || is_paused == Some(paused) { + continue; + } + + state.rewards_paused.write().await.insert(info.id.to_string(), paused); + results.push(set_reward_paused(Arc::clone(&state), info.id.to_string(), paused).await); + } + results +} + +fn rewards_filter(state: Arc, data: LiveSplitBody, paused: bool) -> impl Future> { + async move { + for result in set_rewards_paused(state, data, paused).await { + if result.is_err() { + return Err(warp::reject::custom(CustomRejection::TwitchError)); + } + } + Ok(()) + } +} + +fn livesplit_start(state: Arc) -> BoxedFilter<(impl Reply, )> { + livesplit_route(state, "start", false) +} + +fn livesplit_split(state: Arc) -> BoxedFilter<(impl Reply, )> { + livesplit_route(state, "split", false) +} + +fn livesplit_reset(state: Arc) -> BoxedFilter<(impl Reply, )> { + livesplit_route(state, "reset", true) +} + +fn livesplit_finish(state: Arc) -> BoxedFilter<(impl Reply, )> { + livesplit_route(state, "finish", true) +} + +fn livesplit_route(state: Arc, path: &'static str, paused: bool) -> BoxedFilter<(impl Reply,)> { + warp::path("livesplit") + .and(warp::path(path)) + .and(warp::path::end()) + .and(warp::body::json()) + .and_then(move |body: LiveSplitBody| { + let state = Arc::clone(&state); + rewards_filter(state, body, paused) + }) + .untuple_one() + .map(|| warp::reply()) + .boxed() +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +#[allow(dead_code)] +struct LiveSplitBody { + current_phase: usize, + run: LiveSplitRun, + attempt_started: LiveSplitAtomicTime, + attempt_ended: LiveSplitAtomicTime, + current_split_index: isize, + current_split_name: Option, + current_time: LiveSplitTime, + current_attempt_duration: String, + current_timing_method: usize, +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +#[allow(dead_code)] +struct LiveSplitRun { + game_name: Option, + category_name: Option, + offset: String, + attempt_count: usize, + segment_names: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +#[allow(dead_code)] +struct LiveSplitAtomicTime { + time: String, + synced_with_atomic_clock: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +#[allow(dead_code)] +struct LiveSplitTime { + real_time: Option, + game_time: Option, +} diff --git a/src/app/web/route/mod.rs b/src/app/web/route/mod.rs new file mode 100644 index 0000000..9a9063a --- /dev/null +++ b/src/app/web/route/mod.rs @@ -0,0 +1,9 @@ +pub mod access_token; +pub mod commands; +pub mod livesplit; +pub mod redemptions; + +pub use self::access_token::*; +pub use self::commands::*; +pub use self::livesplit::*; +pub use self::redemptions::*; diff --git a/src/app/web/route/redemptions.rs b/src/app/web/route/redemptions.rs new file mode 100644 index 0000000..fd801d7 --- /dev/null +++ b/src/app/web/route/redemptions.rs @@ -0,0 +1,111 @@ +use twitch_api2::types::RewardId; +use warp::{ + Filter, Reply, + filters::BoxedFilter, + http::Uri, +}; +use crate::app::{ + State, + config::Redemption, + web::{ + CustomRejection, + template::redemptions::{RedemptionsTemplate, AddRedemptionTemplate}, + }, +}; +use std::{ + collections::HashMap, + convert::Infallible, + sync::Arc, +}; + +pub fn redemptions_routes(state: Arc) -> BoxedFilter<(impl Reply, )> { + warp::get() + .and( + redemptions_get(Arc::clone(&state)) + .or(redemptions_add_get()) + ) + .or(warp::post().and( + redemptions_add_post(Arc::clone(&state)) + .or(redemptions_delete_post(Arc::clone(&state))) + )) + .boxed() +} + +fn redemptions_get(state: Arc) -> BoxedFilter<(impl Reply, )> { + warp::path("redemptions") + .and(warp::path::end()) + .and_then(move || { + let state = Arc::clone(&state); + async move { + Result::::Ok(RedemptionsTemplate { + redemptions: state.config.read().await.redemptions.clone(), + }) + } + }) + .boxed() +} + +fn redemptions_add_get() -> BoxedFilter<(impl Reply, )> { + warp::path("redemptions") + .and(warp::path("add")) + .and(warp::path::end()) + .map(|| AddRedemptionTemplate) + .boxed() +} + +fn redemptions_add_post(state: Arc) -> BoxedFilter<(impl Reply, )> { + warp::path("redemptions") + .and(warp::path("add")) + .and(warp::path::end()) + .and(warp::body::content_length_limit(1024 * 5)) + .and(warp::body::form()) + .and_then(move |mut form: HashMap| { + let state = Arc::clone(&state); + async move { + let form_get = try { + let name = form.remove("name")?; + let twitch_id = form.remove("twitch_id")?; + let rhai = form.remove("rhai")?; + (name, twitch_id, rhai) + }; + + let (name, twitch_id, rhai) = match form_get { + Some(x) => x, + None => return Err(warp::reject::custom(CustomRejection::InvalidForm)), + }; + + let redemption = Redemption { + name, + twitch_id: RewardId::new(twitch_id), + rhai, + }; + + state.config.write().await.redemptions.push(redemption); + + Ok(warp::redirect(Uri::from_static("/redemptions"))) + } + }) + .boxed() +} + +fn redemptions_delete_post(state: Arc) -> BoxedFilter<(impl Reply, )> { + warp::path("redemptions") + .and(warp::path("delete")) + .and(warp::path::end()) + .and(warp::body::content_length_limit(1024 * 5)) + .and(warp::body::form()) + .and_then(move |mut form: HashMap| { + let state = Arc::clone(&state); + async move { + let name = match form.remove("name") { + Some(n) => n, + None => return Err(warp::reject::custom(CustomRejection::InvalidForm)), + }; + + state.config.write().await.redemptions.drain_filter(|redemption| redemption.name == name); + + Ok(warp::redirect(Uri::from_static("/redemptions"))) + } + }) + .boxed() +} diff --git a/src/app/web/template/commands.rs b/src/app/web/template/commands.rs new file mode 100644 index 0000000..9ec1341 --- /dev/null +++ b/src/app/web/template/commands.rs @@ -0,0 +1,15 @@ +use askama::Template; +use crate::app::config::{ + Command, + CommandExecutor, +}; + +#[derive(Template)] +#[template(path = "commands.html")] +pub struct CommandsTemplate { + pub commands: Vec, +} + +#[derive(Template)] +#[template(path = "add_command.html")] +pub struct AddCommandTemplate; diff --git a/src/app/web/template/index.rs b/src/app/web/template/index.rs new file mode 100644 index 0000000..0a362ce --- /dev/null +++ b/src/app/web/template/index.rs @@ -0,0 +1,5 @@ +use askama::Template; + +#[derive(Template)] +#[template(path = "index.html")] +pub struct IndexTemplate; diff --git a/src/app/web/template/mod.rs b/src/app/web/template/mod.rs new file mode 100644 index 0000000..bf349dc --- /dev/null +++ b/src/app/web/template/mod.rs @@ -0,0 +1,3 @@ +pub mod commands; +pub mod index; +pub mod redemptions; diff --git a/src/app/web/template/redemptions.rs b/src/app/web/template/redemptions.rs new file mode 100644 index 0000000..53d4d9a --- /dev/null +++ b/src/app/web/template/redemptions.rs @@ -0,0 +1,19 @@ +use askama::Template; +use crate::app::config::Redemption; +use twitch_api2::helix::points::CustomReward; + +#[derive(Template)] +#[template(path = "redemptions.html")] +pub struct RedemptionsTemplate { + pub redemptions: Vec, +} + +#[derive(Template)] +#[template(path = "add_redemption.html")] +pub struct AddRedemptionTemplate; + +#[derive(Template)] +#[template(path = "list_redemptions.html")] +pub struct ListRedemptionsTemplate { + pub rewards: Vec, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e65e8fe --- /dev/null +++ b/src/main.rs @@ -0,0 +1,60 @@ +#![feature(try_blocks)] +#![feature(drain_filter)] + +mod app; + +use tokio::runtime::{Builder, Handle}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use crate::app::{ + State, + config::Config, + user_config::UserConfig, +}; +use std::path::Path; +use tokio::fs::OpenOptions; +use futures::FutureExt; +use std::sync::Arc; + +fn main() -> anyhow::Result<()> { + let runtime = Builder::new_multi_thread() + .enable_all() + .build()?; + + let handle = runtime.handle().clone(); + runtime.block_on(inner(handle)) +} + +async fn inner(runtime: Handle) -> anyhow::Result<()> { + let mut uc_toml = String::new(); + tokio::fs::File::open("config.toml").await?.read_to_string(&mut uc_toml).await?; + let user_config: UserConfig = toml::from_str(&uc_toml)?; + + let c_path = Path::new("data.json"); + let config: Config = if c_path.exists() { + let mut c_json = String::new(); + tokio::fs::File::open(c_path).await?.read_to_string(&mut c_json).await?; + serde_json::from_str(&c_json)? + } else { + Config::default() + }; + + let state = State::new(runtime, user_config, config).await?; + save_config(c_path, Arc::clone(&state)).await?; + + tokio::signal::ctrl_c() + .then(|_| save_config(c_path, Arc::clone(&state))) + .await?; + + Ok(()) +} + +async fn save_config(path: &Path, state: Arc) -> anyhow::Result<()> { + let mut config_file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .await?; + config_file.write_all(&serde_json::to_vec(&*state.config.read().await)?).await?; + Ok(()) +} diff --git a/templates/_base.html b/templates/_base.html new file mode 100644 index 0000000..9c89a87 --- /dev/null +++ b/templates/_base.html @@ -0,0 +1,76 @@ + + + + {% block title %}{% endblock %} + + {% block head %}{% endblock head %} + +{% block body %}{% endblock %} + diff --git a/templates/add_command.html b/templates/add_command.html new file mode 100644 index 0000000..b55dfff --- /dev/null +++ b/templates/add_command.html @@ -0,0 +1,16 @@ +{% extends "_base.html" %} + +{% block title %}Add command{% endblock %} + +{% block body %} +
+ + + + + +
+{% endblock %} diff --git a/templates/add_redemption.html b/templates/add_redemption.html new file mode 100644 index 0000000..145183f --- /dev/null +++ b/templates/add_redemption.html @@ -0,0 +1,12 @@ +{% extends "_base.html" %} + +{% block title %}Add redemption{% endblock %} + +{% block body %} +
+ + + + +
+{% endblock %} diff --git a/templates/commands.html b/templates/commands.html new file mode 100644 index 0000000..82153ec --- /dev/null +++ b/templates/commands.html @@ -0,0 +1,47 @@ +{% extends "_base.html" %} + +{% block title %}Commands{% endblock %} + +{% block head %} + +{% endblock %} + +{% block body %} +
+ Add +
+ +{% for command in commands %} +
+ {{ command.name }} + {% if !command.aliases.is_empty() %} + {{ command.aliases.join(", ") }} + {% endif %} +

+            {%- match command.executor -%}
+                {%- when CommandExecutor::Text with (t) -%}
+                    {{ t }}
+                {%- when CommandExecutor::Rhai with (t) -%}
+                    {{ t }}
+            {%- endmatch -%}
+    
+
+ + +
+
+{% endfor %} +{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..c352d88 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,20 @@ +{% extends "_base.html" %} + +{% block title %}Test{% endblock %} + +{% block body %} +
+ + +
+ + +{% endblock %} diff --git a/templates/list_redemptions.html b/templates/list_redemptions.html new file mode 100644 index 0000000..9385ef6 --- /dev/null +++ b/templates/list_redemptions.html @@ -0,0 +1,29 @@ +{% extends "_base.html" %} + +{% block title %}List rewards{% endblock %} + +{% block head %} + +{% endblock %} + +{% block body %} +{% for reward in rewards %} +
+ {{ reward.title }} + {{ reward.id }} +
+{% endfor %} +{% endblock %} diff --git a/templates/redemptions.html b/templates/redemptions.html new file mode 100644 index 0000000..e306d76 --- /dev/null +++ b/templates/redemptions.html @@ -0,0 +1,38 @@ +{% extends "_base.html" %} + +{% block title %}Redemptions{% endblock %} + +{% block head %} + +{% endblock %} + +{% block body %} +
+ Add +
+ +{% for redemption in redemptions %} +
+ {{ redemption.name }} + {{ redemption.twitch_id }} +
{{ redemption.rhai }}
+
+ + +
+
+{% endfor %} +{% endblock %}