diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 00000000..87e03376 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,44 @@ +# Communication Guidelines + +## Avoid Sycophantic Language +- **NEVER** use phrases like "You're absolutely right!", "You're absolutely correct!", "Excellent point!", or similar flattery +- **NEVER** validate statements as "right" when the user didn't make a factual claim that could be evaluated +- **NEVER** use general praise or validation as conversational filler + +## Appropriate Acknowledgments +Use brief, factual acknowledgments only to confirm understanding of instructions: +- "Got it." +- "Ok, that makes sense." +- "I understand." +- "I see the issue." + +These should only be used when: +1. You genuinely understand the instruction and its reasoning +2. The acknowledgment adds clarity about what you'll do next +3. You're confirming understanding of a technical requirement or constraint + +## Examples + +### โŒ Inappropriate (Sycophantic) +User: "Yes please." +Assistant: "You're absolutely right! That's a great decision." + +User: "Let's remove this unused code." +Assistant: "Excellent point! You're absolutely correct that we should clean this up." + +### โœ… Appropriate (Brief Acknowledgment) +User: "Yes please." +Assistant: "Got it." [proceeds with the requested action] + +User: "Let's remove this unused code." +Assistant: "I'll remove the unused code path." [proceeds with removal] + +### โœ… Also Appropriate (No Acknowledgment) +User: "Yes please." +Assistant: [proceeds directly with the requested action] + +## Rationale +- Maintains professional, technical communication +- Avoids artificial validation of non-factual statements +- Focuses on understanding and execution rather than praise +- Prevents misrepresenting user statements as claims that could be "right" or "wrong" diff --git a/Cargo.lock b/Cargo.lock index 81964deb..0099ceb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,44 +19,42 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aegl_bot" -version = "0.3.0" +version = "0.4.0" dependencies = [ + "anyhow", "chrono", - "chrono-english", "chrono-tz 0.10.4", - "diesel", - "diesel_derives_extra", - "diesel_derives_traits", - "diesel_logger", - "diesel_migrations", + "culpa", "dotenv", + "entity", "fern", "futures", - "futures-retry", "include_dir", - "itertools", + "itertools 0.14.0", + "kameo", + "libbot", "log", + "migration", "paste", "procfs", - "r2d2", "regex", - "riker", - "serde 1.0.219", - "serde_json", + "sea-orm", + "serde", "teloxide", "tera", "thousands", "tokio", - "tokio-util 0.6.10", ] [[package]] name = "ahash" -version = "0.3.8" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "const-random", + "getrandom 0.2.16", + "once_cell", + "version_check", ] [[package]] @@ -68,6 +66,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -84,28 +94,129 @@ dependencies = [ ] [[package]] -name = "arc-swap" -version = "1.7.1" +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "aquamarine" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" +dependencies = [ + "include_dir", + "itertools 0.10.5", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] [[package]] name = "arrayvec" -version = "0.5.2" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "atoi" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -119,7 +230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cfg-if 1.0.1", + "cfg-if", "libc", "miniz_oxide", "object", @@ -129,21 +240,50 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bitflags" -version = "1.3.2" +name = "base64ct" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bigdecimal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "block-buffer" @@ -154,6 +294,29 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "bstr" version = "1.12.0" @@ -161,7 +324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "serde 1.0.219", + "serde", ] [[package]] @@ -170,6 +333,34 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" + [[package]] name = "byteorder" version = "1.5.0" @@ -184,24 +375,24 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.31" +version = "1.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" dependencies = [ "shlex", ] [[package]] name = "cfg-if" -version = "0.1.10" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] -name = "cfg-if" -version = "1.0.1" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" @@ -212,21 +403,12 @@ dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", - "num-traits 0.2.19", + "num-traits", + "serde", "wasm-bindgen", "windows-link", ] -[[package]] -name = "chrono-english" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73d909da7eb4a7d88c679c3f5a1bc09d965754e0adb2e7627426cef96a00d6f" -dependencies = [ - "chrono", - "scanlex", -] - [[package]] name = "chrono-tz" version = "0.9.0" @@ -260,65 +442,84 @@ dependencies = [ ] [[package]] -name = "cloudabi" -version = "0.0.3" +name = "clap" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ - "bitflags 1.3.2", + "clap_builder", + "clap_derive", ] [[package]] -name = "colored" -version = "2.2.0" +name = "clap_builder" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ - "lazy_static", - "windows-sys 0.59.0", + "anstream", + "anstyle", + "clap_lex", + "strsim", ] [[package]] -name = "config" -version = "0.10.1" +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "nom", - "rust-ini", - "serde 1.0.219", - "serde-hjson", - "serde_json", - "toml", - "yaml-rust", + "windows-sys 0.59.0", ] [[package]] -name = "const-random" -version = "0.1.18" +name = "colored" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "const-random-macro", + "windows-sys 0.59.0", ] [[package]] -name = "const-random-macro" -version = "0.1.16" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ - "getrandom 0.2.16", - "once_cell", - "tiny-keccak", + "crossbeam-utils", ] [[package]] -name = "convert_case" -version = "0.4.0" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" @@ -345,13 +546,28 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cfg-if 1.0.1", + "cfg-if", ] [[package]] @@ -360,23 +576,8 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "crossbeam-epoch 0.9.18", - "crossbeam-utils 0.8.21", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" -dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "crossbeam-utils 0.7.2", - "lazy_static", - "maybe-uninit", - "memoffset", - "scopeguard", + "crossbeam-epoch", + "crossbeam-utils", ] [[package]] @@ -385,18 +586,16 @@ version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "crossbeam-utils 0.8.21", + "crossbeam-utils", ] [[package]] -name = "crossbeam-utils" -version = "0.7.2" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "lazy_static", + "crossbeam-utils", ] [[package]] @@ -405,12 +604,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.6" @@ -422,137 +615,107 @@ dependencies = [ ] [[package]] -name = "darling" -version = "0.13.4" +name = "culpa" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +checksum = "5ae0bfe9317b1cb4ff5a56d766ee4b157b3e1f47f11979253570e88d10fd1fd3" dependencies = [ - "darling_core", - "darling_macro", + "culpa-macros", ] [[package]] -name = "darling_core" -version = "0.13.4" +name = "culpa-macros" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +checksum = "1234e1717066d3c71dcf89b75e7b586299e41204d361db56ec51e6ded5014279" dependencies = [ - "fnv", - "ident_case", - "proc-macro2 1.0.95", - "quote 1.0.40", - "strsim", - "syn 1.0.109", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] -name = "darling_macro" -version = "0.13.4" +name = "darling" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", - "quote 1.0.40", - "syn 1.0.109", -] - -[[package]] -name = "dashmap" -version = "3.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f260e2fc850179ef410018660006951c1b55b79e8087e87111a2c388994b9b5" -dependencies = [ - "ahash", - "cfg-if 0.1.10", - "num_cpus", + "darling_macro", ] [[package]] -name = "derive_more" -version = "0.99.20" +name = "darling_core" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ - "convert_case", - "proc-macro2 1.0.95", - "quote 1.0.40", - "rustc_version", - "syn 2.0.104", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", ] [[package]] -name = "deunicode" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" - -[[package]] -name = "diesel" -version = "1.4.8" +name = "darling_macro" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "bitflags 1.3.2", - "byteorder", - "chrono", - "diesel_derives", - "pq-sys", - "r2d2", - "serde_json", + "darling_core", + "quote", + "syn 2.0.106", ] [[package]] -name = "diesel_derives" -version = "1.4.1" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 1.0.109", + "const-oid", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "diesel_derives_extra" -version = "0.2.0" +name = "deranged" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ed24b0993147beb58b8725b1180012e2320a96d15889600df0cd31afd3c15b" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ - "diesel", - "diesel_derives_traits", - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 1.0.109", + "powerfmt", + "serde", ] [[package]] -name = "diesel_derives_traits" -version = "0.2.0" +name = "derive_more" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d0822c6cb583609dc780222064ec55d69228d2163cf69dd4f9b11fa9b8eb99" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "diesel", + "derive_more-impl", ] [[package]] -name = "diesel_logger" -version = "0.1.1" +name = "derive_more-impl" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1793935ad14586bf2aa51574a7157932640c345205ccfb2db431846d846e3db7" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ - "diesel", - "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "unicode-xid", ] [[package]] -name = "diesel_migrations" -version = "1.4.0" +name = "deunicode" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" -dependencies = [ - "migrations_internals", - "migrations_macros", -] +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" [[package]] name = "digest" @@ -561,7 +724,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -570,9 +735,9 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -582,49 +747,119 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] -name = "either" -version = "1.15.0" +name = "dotenvy" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "downcast-rs" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "dptree" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "db96968fcf52fe063a98c75df1d1f2b1fba304e7ae29b72fdc81c1165b7e2fd0" dependencies = [ - "cfg-if 1.0.1", + "colored 3.0.0", + "futures", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "dyn-clone" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] -name = "errno" -version = "0.3.13" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" dependencies = [ - "libc", - "windows-sys 0.60.2", + "serde", ] [[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fern" +name = "entity" +version = "0.4.0" +dependencies = [ + "anyhow", + "chrono", + "culpa", + "dotenv", + "futures", + "libbot", + "regex", + "sea-orm", + "serde", + "tokio", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erasable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437cfb75878119ed8265685c41a115724eae43fb7cc5a0bf0e4ecc3b803af1c4" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fern" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" dependencies = [ - "colored", + "colored 2.2.0", "log", ] @@ -639,15 +874,14 @@ dependencies = [ ] [[package]] -name = "flurry" -version = "0.3.1" +name = "flume" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c0a35f7b50e99185a2825541946252f669f3c3ca77801357cd682a1b356bb3e" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ - "ahash", - "crossbeam-epoch 0.8.2", - "num_cpus", - "parking_lot 0.10.2", + "futures-core", + "futures-sink", + "spin", ] [[package]] @@ -656,6 +890,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -680,6 +920,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -720,7 +966,17 @@ dependencies = [ "futures-core", "futures-task", "futures-util", - "num_cpus", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", ] [[package]] @@ -735,20 +991,9 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", -] - -[[package]] -name = "futures-retry" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde5a672a61f96552aa5ed9fd9c81c3fbdae4be9b1e205d6eaf17c83705adc0f" -dependencies = [ - "futures", - "pin-project-lite", - "tokio", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -791,24 +1036,13 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if 1.0.1", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if 1.0.1", + "cfg-if", "libc", "wasi 0.11.1+wasi-snapshot-preview1", ] @@ -819,7 +1053,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ - "cfg-if 1.0.1", + "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", @@ -833,9 +1067,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" @@ -846,8 +1080,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -856,41 +1090,51 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.9.1", + "bitflags", "ignore", "walkdir", ] [[package]] -name = "h2" -version = "0.3.27" +name = "hashbrown" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util 0.7.15", - "tracing", + "ahash", ] [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] -name = "hermit-abi" -version = "0.5.2" +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" @@ -898,11 +1142,38 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" -version = "0.2.12" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -911,12 +1182,24 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", + "futures-core", "http", + "http-body", "pin-project-lite", ] @@ -926,12 +1209,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "humansize" version = "2.1.3" @@ -943,39 +1220,63 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.32" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.10", + "pin-utils", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", ] [[package]] @@ -1125,7 +1426,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata", + "regex-automata 0.4.9", "same-file", "walkdir", "winapi-util", @@ -1147,8 +1448,19 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1158,7 +1470,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.5", + "serde", +] + +[[package]] +name = "inherent" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c38228f24186d9cc68c729accb4d413be9eaed6ad07ff79e0270d9e56f3de13" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -1167,8 +1491,8 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ - "bitflags 2.9.1", - "cfg-if 1.0.1", + "bitflags", + "cfg-if", "libc", ] @@ -1178,6 +1502,31 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1203,30 +1552,60 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kameo" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a73be96f616ca2784f597b5b6635582f5a7b3ba73b1dbe7afa5d9667955d39" +dependencies = [ + "downcast-rs", + "dyn-clone", + "futures", + "kameo_macros", + "once_cell", + "serde", + "tokio", + "tracing", +] + +[[package]] +name = "kameo_macros" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3f384b32bf6426ae93a8b37da62c85073b676a31a82a86d608ad86453878de0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "uuid", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] -name = "lexical-core" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +name = "libbot" +version = "0.4.0" dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "cfg-if 1.0.1", - "ryu", - "static_assertions", + "anyhow", + "chrono", + "chrono-tz 0.10.4", + "culpa", + "two_timer", ] [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libm" @@ -1235,20 +1614,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] -name = "linked-hash-map" -version = "0.3.0" +name = "libredox" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "serde 0.8.23", - "serde_test", + "bitflags", + "libc", + "redox_syscall", ] [[package]] -name = "linked-hash-map" -version = "0.5.6" +name = "libsqlite3-sys" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] [[package]] name = "linux-raw-sys" @@ -1268,15 +1652,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" -[[package]] -name = "lock_api" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" -dependencies = [ - "scopeguard", -] - [[package]] name = "lock_api" version = "0.4.13" @@ -1294,45 +1669,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] -name = "maybe-uninit" -version = "2.0.0" +name = "matchers" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "memoffset" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "autocfg", + "regex-automata 0.1.10", ] [[package]] -name = "migrations_internals" -version = "1.4.1" +name = "md-5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "diesel", + "cfg-if", + "digest", ] [[package]] -name = "migrations_macros" -version = "1.4.2" +name = "memchr" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "migration" +version = "0.4.0" dependencies = [ - "migrations_internals", - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 1.0.109", + "sea-orm-migration", + "tokio", ] [[package]] @@ -1389,48 +1755,66 @@ dependencies = [ ] [[package]] -name = "never" -version = "0.1.0" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] [[package]] -name = "nom" -version = "5.1.3" +name = "num-bigint-dig" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ - "lexical-core", - "memchr", - "version_check", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", ] [[package]] -name = "num-traits" -version = "0.1.43" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "num-traits 0.2.19", + "num-traits", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "num-iter" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", + "num-integer", + "num-traits", ] [[package]] -name = "num_cpus" -version = "1.17.0" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "hermit-abi", - "libc", + "autocfg", + "libm", ] [[package]] @@ -1448,14 +1832,20 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl" version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", - "cfg-if 1.0.1", + "bitflags", + "cfg-if", "foreign-types", "libc", "once_cell", @@ -1469,9 +1859,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -1493,37 +1883,52 @@ dependencies = [ ] [[package]] -name = "parking_lot" -version = "0.10.2" +name = "ordered-float" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ - "lock_api 0.3.4", - "parking_lot_core 0.7.3", + "num-traits", ] [[package]] -name = "parking_lot" -version = "0.12.4" +name = "ouroboros" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" dependencies = [ - "lock_api 0.4.13", - "parking_lot_core 0.9.11", + "aliasable", + "ouroboros_macro", + "static_assertions", ] [[package]] -name = "parking_lot_core" -version = "0.7.3" +name = "ouroboros_macro" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93f386bb233083c799e6e642a9d73db98c24a5deeb95ffc85bf281255dffc98" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" dependencies = [ - "cfg-if 0.1.10", - "cloudabi", - "libc", - "redox_syscall 0.1.57", - "smallvec", - "winapi", + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", ] [[package]] @@ -1532,9 +1937,9 @@ version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ - "cfg-if 1.0.1", + "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -1554,6 +1959,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1567,7 +1981,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror", "ucd-trie", ] @@ -1589,9 +2003,9 @@ checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -1604,6 +2018,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + [[package]] name = "phf" version = "0.11.3" @@ -1639,7 +2062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand", ] [[package]] @@ -1660,6 +2083,18 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pidgin" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a2a37d2d894d50891a12fba6dd7b84292caf2f14fd0086a2af2404be2d8ebb" +dependencies = [ + "lazy_static", + "regex", + "serde", + "serde_regex", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -1675,9 +2110,9 @@ version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -1692,6 +2127,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1707,6 +2163,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1717,39 +2179,65 @@ dependencies = [ ] [[package]] -name = "pq-sys" -version = "0.4.8" +name = "proc-macro-crate" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "vcpkg", + "toml_edit", ] [[package]] -name = "proc-macro2" -version = "0.4.30" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "unicode-xid", + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "version_check", + "yansi", +] + [[package]] name = "procfs" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "bitflags 2.9.1", + "bitflags", "chrono", "flate2", "hex", @@ -1763,18 +2251,38 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.9.1", + "bitflags", "chrono", "hex", ] [[package]] -name = "quote" -version = "0.6.13" +name = "psm" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" dependencies = [ - "proc-macro2 0.4.30", + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -1783,7 +2291,7 @@ version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ - "proc-macro2 1.0.95", + "proc-macro2", ] [[package]] @@ -1793,28 +2301,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot 0.12.4", - "scheduled-thread-pool", -] - -[[package]] -name = "rand" -version = "0.7.3" +name = "radium" +version = "0.7.0" 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", -] +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" @@ -1823,18 +2313,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[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", + "rand_chacha", + "rand_core", ] [[package]] @@ -1844,49 +2324,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] [[package]] name = "rand_core" -version = "0.5.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.1.16", + "getrandom 0.2.16", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "rc-box" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "897fecc9fac6febd4408f9e935e86df739b0023b625e610e0357535b9c8adad0" dependencies = [ - "getrandom 0.2.16", + "erasable", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "redox_syscall" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "rand_core 0.5.1", + "bitflags", ] [[package]] -name = "redox_syscall" -version = "0.1.57" +name = "ref-cast" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] [[package]] -name = "redox_syscall" -version = "0.5.17" +name = "ref-cast-impl" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ - "bitflags 2.9.1", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -1897,8 +2382,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1909,118 +2403,171 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-core", "futures-util", - "h2", "http", "http-body", + "http-body-util", "hyper", "hyper-tls", - "ipnet", + "hyper-util", "js-sys", "log", - "mime", "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", - "serde 1.0.219", + "rustls-pki-types", + "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", - "tokio-util 0.7.15", + "tokio-util", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg", ] [[package]] -name = "riker" -version = "0.4.2" +name = "rgb" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abff93ece5a5d3d7f2c54dfba7550657a644c9dc0a871c7ddf8c31381971c41b" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" dependencies = [ - "chrono", - "config", - "dashmap", - "futures", - "num_cpus", - "pin-utils", - "rand 0.7.3", - "regex", - "riker-macros", - "slog", - "slog-scope", - "slog-stdlog", - "uuid", + "bytemuck", ] [[package]] -name = "riker-macros" -version = "0.2.0" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a8e8f71c9e7980a596c39c7e3537ea8563054526e15712a610ac97a02dba15" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "rust-ini" -version = "0.13.0" +name = "rkyv" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] [[package]] -name = "rustc-demangle" -version = "0.1.26" +name = "rkyv_derive" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] -name = "rustc_version" -version = "0.4.1" +name = "rsa" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ - "semver", + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", ] +[[package]] +name = "rust_decimal" +version = "1.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2033,7 +2580,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", @@ -2041,19 +2588,44 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ - "base64", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2070,12 +2642,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scanlex" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088c5d71572124929ea7549a8ce98e1a6fd33d0a38367b09027b382e67c033db" - [[package]] name = "schannel" version = "0.1.27" @@ -2086,12 +2652,27 @@ dependencies = [ ] [[package]] -name = "scheduled-thread-pool" -version = "0.2.7" +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ - "parking_lot 0.12.4", + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] @@ -2100,13 +2681,181 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sea-orm" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34963b2d68331ef5fbc8aa28a53781471c15f90ba1ad4f2689d21ce8b9a9d1f1" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "futures-util", + "log", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc17cb2b24e93fc1d56de7751a12222f2303c06e83ed4d7a1e929e39f30c7d7" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "regex", + "sea-schema", + "sqlx", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a489127c872766445b4e28f846825f89a076ac3af2591d1365503a68f93e974c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.106", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695e830a1332a4e3e57b5972eee00574a36060e1938afca7041a524e0955d5ba" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "0.32.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" +dependencies = [ + "bigdecimal", + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-binder" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.106", + "thiserror", +] + +[[package]] +name = "sea-schema" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2239ff574c04858ca77485f112afea1a15e53135d3097d0c86509cef1def1338" +dependencies = [ + "futures", + "sea-query", + "sea-query-binder", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -2123,18 +2872,6 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[package]] -name = "serde" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" - [[package]] name = "serde" version = "1.0.219" @@ -2144,49 +2881,37 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-hjson" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" -dependencies = [ - "lazy_static", - "linked-hash-map 0.3.0", - "num-traits 0.1.43", - "regex", - "serde 0.8.23", -] - [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", "ryu", - "serde 1.0.219", + "serde", ] [[package]] -name = "serde_test" -version = "0.8.23" +name = "serde_regex" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" dependencies = [ - "serde 0.8.23", + "regex", + "serde", ] [[package]] @@ -2198,19 +2923,50 @@ dependencies = [ "form_urlencoded", "itoa", "ryu", - "serde 1.0.219", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", ] [[package]] name = "serde_with_macros" -version = "1.5.2" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling", - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 1.0.109", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -2219,11 +2975,20 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if 1.0.1", + "cfg-if", "cpufeatures", "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2232,13 +2997,29 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.1" @@ -2247,102 +3028,320 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] -name = "slog" -version = "2.7.0" +name = "socket2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] [[package]] -name = "slog-scope" -version = "4.4.0" +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bigdecimal", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.10.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.106", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.106", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ - "arc-swap", - "lazy_static", - "slog", + "atoi", + "base64", + "bigdecimal", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", ] [[package]] -name = "slog-stdlog" -version = "4.1.1" +name = "sqlx-postgres" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6706b2ace5bbae7291d3f8d2473e2bfab073ccd7d03670946197aec98471fa3e" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", "log", - "slog", - "slog-scope", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", ] [[package]] -name = "slug" -version = "0.1.6" +name = "sqlx-sqlite" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ - "deunicode", - "wasm-bindgen", + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "time", + "tracing", + "url", + "uuid", ] [[package]] -name = "smallvec" -version = "1.15.1" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] -name = "socket2" -version = "0.5.10" +name = "stacker" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" dependencies = [ + "cc", + "cfg-if", "libc", - "windows-sys 0.52.0", + "psm", + "windows-sys 0.59.0", ] [[package]] -name = "socket2" -version = "0.6.0" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "stringprep" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] [[package]] -name = "static_assertions" -version = "1.1.0" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "strsim" -version = "0.10.0" +name = "strum" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" [[package]] -name = "syn" -version = "0.15.44" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -2350,27 +3349,30 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", + "proc-macro2", + "quote", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", + "proc-macro2", + "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2378,63 +3380,62 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] -name = "system-configuration" -version = "0.5.1" +name = "take_mut" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" [[package]] -name = "system-configuration-sys" -version = "0.5.0" +name = "takecell" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] +checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "teloxide" -version = "0.5.3" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9964854e5ec3a5a44a9f50ebb7641327f1084ab4fc37a6c4a23cc011a388dc2e" +checksum = "84992abeed3ae42e8401b25d266d12bcba1def0abe59d22f6b9781167545f71e" dependencies = [ - "async-trait", + "aquamarine", "bytes", "derive_more", - "flurry", + "dptree", + "either", "futures", "log", "mime", "pin-project", - "serde 1.0.219", + "serde", "serde_json", - "serde_with_macros", "teloxide-core", "teloxide-macros", - "thiserror 1.0.69", + "thiserror", "tokio", "tokio-stream", - "tokio-util 0.6.10", + "tokio-util", + "url", ] [[package]] name = "teloxide-core" -version = "0.3.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "114c9057a3a2f74d937ece64029b362f583a69fb4b7405722c6dc03cd5bb4658" +checksum = "7f7a34ca8e971fa892e633858c07547fe138ef4a02e4a4eaa1d35e517d6e0bc4" dependencies = [ + "bitflags", "bytes", "chrono", "derive_more", @@ -2442,42 +3443,47 @@ dependencies = [ "futures", "log", "mime", - "never", "once_cell", "pin-project", + "rc-box", "reqwest", - "serde 1.0.219", + "rgb", + "serde", "serde_json", - "serde_with_macros", - "thiserror 1.0.69", + "serde_with", + "stacker", + "take_mut", + "takecell", + "thiserror", "tokio", - "tokio-util 0.6.10", + "tokio-util", "url", "uuid", ] [[package]] name = "teloxide-macros" -version = "0.4.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb7e97b8bef2231aea6643558147c7f9c112675c4ca49f24d8fac2edff1216d" +checksum = "300fadcaf0c182f19b5ca10bf23a45dc9a48925f00c704405fd90ee2c03942f9" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 1.0.109", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2494,9 +3500,9 @@ dependencies = [ "percent-encoding", "pest", "pest_derive", - "rand 0.8.5", + "rand", "regex", - "serde 1.0.219", + "serde", "serde_json", "slug", "unic-segment", @@ -2504,57 +3510,68 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 1.0.69", + "thiserror-impl", ] [[package]] -name = "thiserror" -version = "2.0.12" +name = "thiserror-impl" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ - "thiserror-impl 2.0.12", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] -name = "thiserror-impl" -version = "1.0.69" +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + +[[package]] +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "cfg-if", ] [[package]] -name = "thiserror-impl" -version = "2.0.12" +name = "time" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", ] [[package]] -name = "thousands" -version = "0.2.0" +name = "time-core" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] -name = "tiny-keccak" -version = "2.0.2" +name = "time-macros" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ - "crunchy", + "num-conv", + "time-core", ] [[package]] @@ -2567,6 +3584,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.47.1" @@ -2581,8 +3613,9 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", + "tracing", "windows-sys 0.59.0", ] @@ -2592,9 +3625,9 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -2620,40 +3653,73 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.10" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", "futures-sink", - "log", "pin-project-lite", "tokio", ] [[package]] -name = "tokio-util" -version = "0.7.15" +name = "toml_datetime" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.10.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ - "bytes", "futures-core", - "futures-sink", + "futures-util", "pin-project-lite", + "sync_wrapper", "tokio", + "tower-layer", + "tower-service", ] [[package]] -name = "toml" -version = "0.5.11" +name = "tower-http" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "serde 1.0.219", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", ] +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -2666,10 +3732,23 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "tracing-core" version = "0.1.34" @@ -2679,12 +3758,38 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", +] + [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "two_timer" +version = "2.2.1" +source = "git+https://github.com/berkus/two-timer.git?branch=updated#2becda6a3a9c7c99bb0fc538079649ef43d67eb2" +dependencies = [ + "chrono", + "pidgin", + "regex", + "serde_json", +] + [[package]] name = "typenum" version = "1.18.0" @@ -2753,17 +3858,44 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-xid" -version = "0.1.0" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" @@ -2774,7 +3906,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde 1.0.219", + "serde", ] [[package]] @@ -2783,13 +3915,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" -version = "0.8.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", ] [[package]] @@ -2823,12 +3964,6 @@ dependencies = [ "try-lock", ] -[[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.11.1+wasi-snapshot-preview1" @@ -2844,13 +3979,19 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if 1.0.1", + "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -2864,9 +4005,9 @@ checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -2876,7 +4017,7 @@ version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "cfg-if 1.0.1", + "cfg-if", "js-sys", "once_cell", "wasm-bindgen", @@ -2889,7 +4030,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ - "quote 1.0.40", + "quote", "wasm-bindgen-macro-support", ] @@ -2899,9 +4040,9 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2939,35 +4080,41 @@ dependencies = [ ] [[package]] -name = "winapi" -version = "0.3.9" +name = "webpki-roots" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "webpki-roots 1.0.2", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "webpki-roots" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] [[package]] -name = "winapi-util" -version = "0.1.9" +name = "whoami" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "windows-sys 0.59.0", + "libredox", + "wasite", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "winapi-util" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] [[package]] name = "windows-core" @@ -2988,9 +4135,9 @@ version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -2999,9 +4146,9 @@ version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -3251,13 +4398,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] -name = "winreg" -version = "0.50.0" +name = "winnow" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ - "cfg-if 1.0.1", - "windows-sys 0.48.0", + "memchr", ] [[package]] @@ -3266,7 +4412,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags", ] [[package]] @@ -3276,21 +4422,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] -name = "yaml-rust" -version = "0.4.5" +name = "wyz" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ - "linked-hash-map 0.5.6", + "tap", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ - "serde 1.0.219", + "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3302,9 +4454,9 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", "synstructure", ] @@ -3323,9 +4475,9 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -3343,12 +4495,18 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" @@ -3362,9 +4520,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -3377,7 +4535,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ - "proc-macro2 1.0.95", - "quote 1.0.40", - "syn 2.0.104", + "proc-macro2", + "quote", + "syn 2.0.106", ] diff --git a/Cargo.toml b/Cargo.toml index 6ac1f9bd..37c44526 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,44 +1,38 @@ -[package] -authors = ["Berkus Decker "] -edition = "2021" -name = "aegl_bot" -version = "0.3.0" +[workspace] +members = ["bot", "entity", "migration"] +resolver = "2" -[dependencies] -regex = "1" +[workspace.dependencies] +anyhow = "1" chrono = "0.4" chrono-tz = "0.10" -chrono-english = "0.1" -diesel_derives_extra = "0.2" -diesel_derives_traits = "0.2" -#diesel-derive-more = "1.1" -diesel_logger = "0.1" -diesel_migrations = "1.4" # -- retry with "extern crate" macros -diesel = { version = "1.4", features = [ - "postgres", - "chrono", - "serde_json", - "r2d2", -] } -r2d2 = "0.8" +culpa = "1.0" dotenv = "0.15" -#anyhow = "1.0" -futures = "0.3" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -teloxide = { version = "0.5", features = ["macros"] } -log = "0.4" +entity = { version = "0.4", path = "./entity" } fern = { version = "0.7", features = ["colored"] } -futures-retry = "0.6" +futures = "0.3" +include_dir = { version = "0.7.4", features = ["glob", "nightly"] } itertools = "0.14" -# plurals = "0.3" -thousands = "0.2.0" -tokio = { version = "1.12", features = ["macros", "rt-multi-thread"] } -tokio-util = { version = "0.6", features = ["codec"] } -riker = "0.4" +kameo = "0.17" +libbot = { path = "./lib" } +log = "0.4" +migration = { version = "0.4", path = "./migration" } paste = "1" +# plurals = "0.3" +regex = "1" +sea-orm = { version = "1.1", features = [ + "macros", + "runtime-tokio-rustls", + "sqlx-postgres", + "with-chrono", +] } +sea-orm-migration = { version = "1.1", features = [ + "runtime-tokio-rustls", + "sqlx-postgres", + "with-chrono", +] } +serde = { version = "1.0", features = ["derive"] } +teloxide = { version = "0.17", features = ["macros"] } tera = "1" -include_dir = { version = "0.7.4", features = ["glob", "metadata", "nightly"] } - -[target.'cfg(target_os="linux")'.dependencies] -procfs = "0.17" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +two_timer = { version = "2.2", git = "https://github.com/berkus/two-timer.git", branch = "updated" } diff --git a/Justfile b/Justfile index 07f01eb1..fa0b3e44 100644 --- a/Justfile +++ b/Justfile @@ -4,7 +4,10 @@ build: cargo build --release test: cargo test +run: + cargo run --bin bot alias d := deploy alias b := build alias t := test +alias r := run diff --git a/TODO.md b/TODO.md index 66c5eb00..3eaa2c17 100644 --- a/TODO.md +++ b/TODO.md @@ -1,11 +1,44 @@ More features: +`Impl`s for the SeaORM entities: + +```rust +// src/entity/pet.rs + +// Model - for views and other state/display things +impl Model { + pub fn display_name(&self) -> String { + format!("Pet: {} (ID: {})", self.name, self.id) + } +} + +use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait}; + +// Entity - for reading data from DB +impl Entity { + pub async fn find_by_name( + db: &DatabaseConnection, + name: &str, + ) -> sea_orm::Result> { + Entity::find() + .filter(Column::Name.eq(name)) + .one(db) + .await + } +} + +// ActiveModel - for changing data in the DB +impl ActiveModel { + pub fn set_name(&mut self, name: String) { + self.name = sea_orm::ActiveValue::Set(name); + } +} +``` + + - [ ] Add waiting-list for activities. - [ ] Mark past WF events with "(ended) " prefix. -- [ ] Handle diesel in async using blocking() handoff - - https://docs.rs/tokio-threadpool/0.1/tokio_threadpool/fn.blocking.html - - tokio_diesel - [ ] HANDLE ERRORS - [x] match guardian- and telegram-names case-insensitive (use filter(`.ilike`(match))) @done (19-05-21 22:09) - [ ] GUARDIAN_ID could be int, telegram name or psn name @@ -18,19 +51,13 @@ More features: - [ ] Interactive calendar + clock picker to plan raids - [ ] this may require full actor framework already to track states properly - [ ] actor per user creating raid? -- [ ] Rewrite with actors, self-healing and other nice-to-have things. +- [x] Rewrite with actors, self-healing and other nice-to-have things. - actix / riker could be used for structuring the bot as independent entities 1. supervisor to restart failing telegram connection - see telegram-event-bot for structure idea - telecord uses a simplistic approach - 2. actix-diesel thingie to run blocking diesel in a separate actor - - https://github.com/actix/examples/blob/f8e3570bd16bcf816070af20f210ce1b4f2fca69/diesel/src/main.rs#L64-L70 3. futures-cpupool as a very primitive wrapper - https://github.com/diesel-rs/diesel/issues/399 - 4. tokio threadpool blocking tasks - - https://github.com/tokio-rs/tokio/pull/317 - 5. bb8 as diesel pool - - could this help? 6. Marat promised actix-telegram crate on 2018-09-12 in @rustlang_ru chat: https://github.com/jeizsm/actix-telegram/blob/autogenerated_types/examples/polling.rs โœ” it's now available @done (18-09-22 19:41) @@ -41,3 +68,19 @@ Remember to use `async fn`!! Send function: - https://github.com/bytesnake/telebot/blob/master/telebot-derive/src/lib.rs#L239 +Update riker to kameo: +- [x] https://tqwewe.com/blog/comparing-rust-actor-libraries/ + +A version of two-timer that can use "in x hours" and supports timezones (default doesn't work with TZs properly): +- https://github.com/JellyWX/two-timer/tree/master + +// in manage_command: +// Need to invent some sort of match string format for matching subcommands +// Some are `/command subcommand [args]`, some are `/command arg subcommand args` etc. +// Can encode this string in prefix() for subcommands and make them match, maybe even directly? +// i.e. add subcommands together with master command to the general list of commands (need to sort properly too) +// if match_subcommand(message, ListAdminsSubcommand) { +// return ListAdminsSubcommand::execute(); +// } else if match_subcommand(message, AddAdminSubcommand) { +// return AddAdminSubcommand::execute(); +// } diff --git a/bot/Cargo.toml b/bot/Cargo.toml new file mode 100644 index 00000000..dfc2cac7 --- /dev/null +++ b/bot/Cargo.toml @@ -0,0 +1,33 @@ +[package] +authors = ["Berkus Decker "] +edition = "2021" +name = "aegl_bot" +version = "0.4.0" +publish = false + +[dependencies] +anyhow.workspace = true +chrono-tz.workspace = true +chrono.workspace = true +culpa.workspace = true +dotenv.workspace = true +entity.workspace = true +fern.workspace = true +futures.workspace = true +include_dir.workspace = true +itertools.workspace = true +kameo.workspace = true +libbot.workspace = true +log.workspace = true +migration.workspace = true +paste.workspace = true +regex.workspace = true +sea-orm.workspace = true +serde.workspace = true +teloxide.workspace = true +tera.workspace = true +tokio.workspace = true + +[target.'cfg(target_os="linux")'.dependencies] +procfs = "0.17" +thousands = "0.2" diff --git a/bot/src/actors/bot_actor.rs b/bot/src/actors/bot_actor.rs new file mode 100644 index 00000000..5c94f303 --- /dev/null +++ b/bot/src/actors/bot_actor.rs @@ -0,0 +1,291 @@ +use { + crate::{ + actors::reminder_actor::{ + ReminderActor, ScheduleNextDay, ScheduleNextMinute, ScheduleNextWeek, + }, + commands::*, + BotCommand, + }, + kameo::{ + actor::ActorRef, + error::Infallible, + message::{Context, Message}, + Actor, + }, + sea_orm::DatabaseConnection, + std::fmt::Formatter, + teloxide::{ + prelude::*, + types::{ChatId, ParseMode}, + }, + tokio::sync::broadcast, +}; + +pub struct BotActor { + pub bot: Bot, + bot_name: String, + lfg_chat_id: i64, + update_sender: broadcast::Sender, + connection_pool: DatabaseConnection, + commands_list: Vec<(String, String)>, +} + +unsafe impl Send for BotActor {} + +impl std::fmt::Debug for BotActor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "BotActor") + } +} + +#[derive(Debug, Clone)] +pub struct ActorUpdateMessage { + pub requester: Bot, + pub update: teloxide::types::Message, +} + +impl ActorUpdateMessage { + pub fn new(requester: Bot, update: teloxide::types::Message) -> Self { + Self { requester, update } + } +} + +impl BotActor { + // Public API + + pub async fn new( + name: &str, + bot: Bot, + update_sender: broadcast::Sender, + lfg_chat_id: i64, + ) -> Self { + let connection_pool = crate::establish_db_connection().await.unwrap(); + BotActor { + bot, + bot_name: name.to_string(), + lfg_chat_id, + update_sender, + connection_pool, + commands_list: vec![], + } + } + + pub fn list_commands(&self) -> Vec<(String, String)> { + self.commands_list.clone() + } + + // Internal helpers + + // fn handle_error(error: anyhow::Error) -> RetryPolicy { + // // count errors + // log::error!("handle_error"); + // match error.downcast_ref::() { + // Some(te) => { + // log::error!("Telegram error: {}, retrying connection.", te); + // RetryPolicy::WaitRetry(Duration::from_secs(30)) + // } + // None => { + // log::error!("handle_error didn't match, real error {:?}", error); + // //handle_error didnt match, real error Io(Custom { kind: Other, error: StringError("failed to lookup address information: nodename nor servname provided, or not known") }) + // RetryPolicy::ForwardError(error) + // } + // } + // } +} + +use crate::commands::match_command; + +impl Actor for BotActor { + type Args = Self; + type Error = Infallible; + + async fn on_start(args: Self::Args, actor_ref: ActorRef) -> Result { + macro_rules! new_command { + ($T:ident, $args:expr) => { + let cmd = $T::spawn($T::new( + actor_ref.clone(), + $args.bot_name.clone(), + $args.connection_pool.clone(), + )); + $args + .commands_list + .push(($T::prefix().into(), $T::description().into())); + + // Subscribe to updates + let mut update_receiver = $args.update_sender.subscribe(); + let cmd_clone = cmd.clone(); + let bot_name = $args.bot_name.clone(); + tokio::spawn(async move { + while let Ok(msg) = update_receiver.recv().await { + if let (Some(_), _) = + match_command(msg.update.text(), $T::prefix(), &bot_name) + { + let _ = cmd_clone.tell(msg).await; + } + } + }); + }; + } + + let mut bot_actor = args; + + new_command!(ActivitiesCommand, bot_actor); + new_command!(CancelCommand, bot_actor); + new_command!(ChatIdCommand, bot_actor); + new_command!(D1weekCommand, bot_actor); + new_command!(D2weekCommand, bot_actor); + new_command!(EditCommand, bot_actor); + new_command!(EditGuardianCommand, bot_actor); + new_command!(HelpCommand, bot_actor); + new_command!(JoinCommand, bot_actor); + new_command!(LfgCommand, bot_actor); + new_command!(ListCommand, bot_actor); + new_command!(ManageCommand, bot_actor); + new_command!(PsnCommand, bot_actor); + new_command!(UptimeCommand, bot_actor); + new_command!(WhoisCommand, bot_actor); + + // Create reminder tasks actor + let reminders = ReminderActor::spawn(ReminderActor::new( + actor_ref.clone(), + bot_actor.lfg_chat_id, + bot_actor.connection_pool.clone(), + )); + + // Schedule first run, the actor handler will reschedule. + let _ = reminders.tell(ScheduleNextMinute).await; + let _ = reminders.tell(ScheduleNextDay).await; + let _ = reminders.tell(ScheduleNextWeek).await; + + Ok(bot_actor) + } +} + +impl BotActor { + pub async fn create( + bot_name: String, + bot: Bot, + update_sender: broadcast::Sender, + lfg_chat: i64, + ) -> Self { + Self::new(&bot_name, bot, update_sender, lfg_chat).await + } +} + +#[derive(Clone, Debug)] +pub enum Format { + Plain, + Markdown, + Html, +} + +#[derive(Clone, Debug)] +pub enum Notify { + Off, + On, +} + +#[derive(Clone, Debug)] +pub struct SendMessage(pub String, pub ChatId, pub Format, pub Notify); + +#[derive(Clone, Debug)] +pub struct SendMessageReply(pub String, pub ActorUpdateMessage, pub Format, pub Notify); + +#[derive(Clone, Debug)] +pub struct ListCommands(pub ActorUpdateMessage); + +impl Message for BotActor { + type Reply = (); + + async fn handle( + &mut self, + msg: SendMessage, + _ctx: &mut Context, + ) -> Self::Reply { + log::debug!("SendMessage: {}", &msg.0); + let resp = self + .bot + .send_message(msg.1, msg.0) + .disable_notification(match msg.3 { + Notify::On => false, + Notify::Off => true, + }) + .link_preview_options(teloxide::types::LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false, + }); + + let resp = match msg.2 { + Format::Html => resp.parse_mode(ParseMode::Html), + Format::Markdown => resp.parse_mode(ParseMode::MarkdownV2), + Format::Plain => resp, + }; + + let _ = resp.send().await; + } +} + +impl Message for BotActor { + type Reply = (); + + async fn handle( + &mut self, + msg: SendMessageReply, + _ctx: &mut Context, + ) -> Self::Reply { + log::debug!("SendMessageReply: {}", &msg.0); + let message = msg.1; + + let fut = self + .bot + .send_message(message.update.chat.id, msg.0) + .reply_parameters(teloxide::types::ReplyParameters::new(message.update.id)) + .disable_notification(match msg.3 { + Notify::On => false, + Notify::Off => true, + }) + .link_preview_options(teloxide::types::LinkPreviewOptions { + is_disabled: true, + url: None, + prefer_small_media: false, + prefer_large_media: false, + show_above_text: false, + }); + + let fut = match msg.2 { + Format::Html => fut.parse_mode(ParseMode::Html), + Format::Markdown => fut.parse_mode(ParseMode::MarkdownV2), + Format::Plain => fut, + }; + + let _ = fut.send().await; + } +} + +impl Message for BotActor { + type Reply = (); + + async fn handle( + &mut self, + msg: ListCommands, + ctx: &mut Context, + ) -> Self::Reply { + log::debug!("ListCommands"); + let message = msg.0; + + let mut sorted_cmds = self.list_commands(); + sorted_cmds.sort_by_key(|v| v.0.clone()); + let reply = sorted_cmds.into_iter().fold( + "Help ๐Ÿš‘\nThese are the registered commands for this Bot:\n\n".into(), + |acc, pair| format!("{}{} โ€” {}\n\n", acc, pair.0, pair.1), + ); + + let _ = ctx + .actor_ref() + .tell(SendMessageReply(reply, message, Format::Html, Notify::Off)) + .try_send(); // @todo use unbounded mailbox for bot_actor? prolly not + } +} diff --git a/bot/src/actors/mod.rs b/bot/src/actors/mod.rs new file mode 100644 index 00000000..cf7c1736 --- /dev/null +++ b/bot/src/actors/mod.rs @@ -0,0 +1,2 @@ +pub mod bot_actor; +pub mod reminder_actor; diff --git a/bot/src/actors/reminder_actor.rs b/bot/src/actors/reminder_actor.rs new file mode 100644 index 00000000..78911561 --- /dev/null +++ b/bot/src/actors/reminder_actor.rs @@ -0,0 +1,213 @@ +use { + crate::actors::bot_actor::{Format, Notify, SendMessage}, + chrono::Timelike, + culpa::throws, + entity::prelude::*, + kameo::{actor::ActorRef, error::Infallible, message::*, Actor}, + libbot::{ + datetime::{d2_reset_time, reference_date, start_at_time, start_at_weekday_time}, + services::destiny_schedule::{this_week_in_d1, this_week_in_d2}, + }, + sea_orm::DatabaseConnection, + teloxide::types::ChatId, +}; + +#[derive(Clone)] +pub struct ReminderActor { + bot_ref: ActorRef, + lfg_chat: i64, + connection_pool: DatabaseConnection, +} + +impl Actor for ReminderActor { + type Args = Self; + type Error = Infallible; + + async fn on_start(args: Self::Args, _actor_ref: ActorRef) -> Result { + Ok(args) + } +} + +impl ReminderActor { + pub fn new( + bot_ref: ActorRef, + lfg_chat: i64, + connection_pool: DatabaseConnection, + ) -> Self { + Self { + bot_ref, + lfg_chat, + connection_pool, + } + } + + fn connection(&self) -> &DatabaseConnection { + &self.connection_pool + } +} + +#[derive(Clone, Debug)] +pub struct Reminders; + +#[derive(Clone, Debug)] +pub struct DailyReset; + +#[derive(Clone, Debug)] +pub struct WeeklyReset; + +impl Message for ReminderActor { + type Reply = (); + + async fn handle( + &mut self, + _msg: Reminders, + ctx: &mut Context, + ) -> Self::Reply { + let bot_ref = self.bot_ref.clone(); + let connection = self.connection(); + let lfg_chat = self.lfg_chat; + + let found = PlannedActivities::upcoming_activities_alert(connection).await; + + if let Some(upcoming_events) = found { + // @Todo: this text should be populated in tera template in `bot` + let text = upcoming_events + .into_iter() + .fold("Activities starting soon:\n\n".to_owned(), |acc, event| { + acc + &format!("Activity {} starting soon\n\n", event.id) + }); + + let _ = bot_ref + .tell(SendMessage( + text, + ChatId(lfg_chat), + Format::Html, + Notify::On, + )) + .await; + } + + let _ = ctx.actor_ref().tell(ScheduleNextMinute).await; + } +} + +// 1. Daily resets at 20:00 MSK (17:00 UTC) every day +#[throws(kameo::error::SendError)] +pub async fn daily_reset(bot: ActorRef, lfg_chat: ChatId) { + bot.tell(SendMessage( + "โšก๏ธ Daily reset".into(), + lfg_chat, + Format::Plain, + Notify::Off, + )) + .await?; +} + +impl Message for ReminderActor { + type Reply = anyhow::Result<()>; + + #[throws(anyhow::Error)] + async fn handle(&mut self, _msg: DailyReset, ctx: &mut Context) { + daily_reset(self.bot_ref.clone(), ChatId(self.lfg_chat)).await?; + ctx.actor_ref().tell(ScheduleNextDay).await?; + } +} + +// 2. Weekly (main) resets at 20:00 msk every Tue +// 6. On main reset: change in Dreaming City curse +// dreaming city on 3-week schedule +// 7. On main reset: change in Dreaming City Ascendant Challenges +// dreaming city challenges on 6-week schedule +#[throws(kameo::error::SendError)] +pub async fn major_weekly_reset( + bot: ActorRef, + lfg_chat: ChatId, +) { + let msg = format!( + "โšก๏ธ Weekly reset:\n\n{d1week}\n\n{d2week}", + d1week = this_week_in_d1(), + d2week = this_week_in_d2(), + ); + bot.tell(SendMessage(msg, lfg_chat, Format::Markdown, Notify::Off)) + .await?; +} + +impl Message for ReminderActor { + type Reply = anyhow::Result<()>; + + #[throws(anyhow::Error)] + async fn handle(&mut self, _msg: WeeklyReset, ctx: &mut Context) { + major_weekly_reset(self.bot_ref.clone(), ChatId(self.lfg_chat)).await?; + ctx.actor_ref().tell(ScheduleNextWeek).await?; + } +} + +#[derive(Clone, Debug)] +pub struct ScheduleNextMinute; + +#[derive(Clone, Debug)] +pub struct ScheduleNextDay; + +#[derive(Clone, Debug)] +pub struct ScheduleNextWeek; + +impl Message for ReminderActor { + type Reply = anyhow::Result<()>; + + #[throws(anyhow::Error)] + async fn handle(&mut self, _msg: ScheduleNextMinute, ctx: &mut Context) { + let target_time = (reference_date() + chrono::Duration::minutes(1)) + .with_second(0) + .unwrap(); + let actor_ref = ctx.actor_ref().clone(); + + let now = std::time::SystemTime::now(); + let target_system_time = + std::time::UNIX_EPOCH + std::time::Duration::from_secs(target_time.timestamp() as u64); + if let Ok(duration) = target_system_time.duration_since(now) { + tokio::time::sleep(duration).await; + actor_ref.tell(Reminders).try_send()?; + } + } +} + +impl Message for ReminderActor { + type Reply = (); + async fn handle( + &mut self, + _msg: ScheduleNextDay, + ctx: &mut Context, + ) -> Self::Reply { + let target_time = start_at_time(reference_date(), d2_reset_time()); + let actor_ref = ctx.actor_ref().clone(); + + let now = std::time::SystemTime::now(); + let target_system_time = + std::time::UNIX_EPOCH + std::time::Duration::from_secs(target_time.timestamp() as u64); + if let Ok(duration) = target_system_time.duration_since(now) { + tokio::time::sleep(duration).await; + let _ = actor_ref.tell(DailyReset).await; + } + } +} + +impl Message for ReminderActor { + type Reply = (); + async fn handle( + &mut self, + _msg: ScheduleNextWeek, + ctx: &mut Context, + ) -> Self::Reply { + let target_time = + start_at_weekday_time(reference_date(), chrono::Weekday::Tue, d2_reset_time()); + let actor_ref = ctx.actor_ref().clone(); + + let now = std::time::SystemTime::now(); + let target_system_time = + std::time::UNIX_EPOCH + std::time::Duration::from_secs(target_time.timestamp() as u64); + if let Ok(duration) = target_system_time.duration_since(now) { + tokio::time::sleep(duration).await; + let _ = actor_ref.tell(WeeklyReset).await; + } + } +} diff --git a/src/bin/bot.rs b/bot/src/bin/bot.rs similarity index 71% rename from src/bin/bot.rs rename to bot/src/bin/bot.rs index 49a62e68..b0d74f72 100644 --- a/src/bin/bot.rs +++ b/bot/src/bin/bot.rs @@ -1,15 +1,19 @@ // Async Rust implementation of the bot // // To make it usable it misses natty parsing lib implementation in rust -// (There are now several rust impls including https://lib.rs/crates/two_timer and https://lib.rs/crates/intervalle) +// Use https://lib.rs/crates/two_timer use { - aegl_bot::bot_actor::{ActorUpdateMessage, BotActor, UpdateMessage}, + aegl_bot::{ + actors::bot_actor::{ActorUpdateMessage, BotActor}, + establish_db_connection, + }, dotenv::dotenv, - // riker::prelude::*, doesn't work here! - riker::actors::{channel, ActorRefFactory, ActorSystem, ChannelRef, Publish, Tell}, + kameo::Actor, + migration::{Migrator, MigratorTrait}, std::env, - teloxide::{prelude::*, requests::ResponseResult}, + teloxide::{prelude::*, requests::ResponseResult, types::Message as TelegramMessage}, + tokio::sync::broadcast, }; fn setup_logging() -> Result<(), fern::InitError> { @@ -72,11 +76,11 @@ fn setup_logging() -> Result<(), fern::InitError> { } #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { dotenv().ok(); setup_logging().expect("failed to initialize logging"); - aegl_bot::datetime::bot_start_time(); // Mark start timestamp + libbot::datetime::bot_start_time(); // Mark start timestamp // TimeZone.setDefault(TimeZone.getTimeZone(config.getString("bot.timezone"))) let bot_name = env::var("TELEGRAM_BOT_NAME").expect("TELEGRAM_BOT_NAME must be set"); @@ -86,29 +90,30 @@ async fn main() { .expect("BOT_LFG_CHAT_ID must be a valid telegram chat id"); let token = env::var("TELEGRAM_BOT_TOKEN").expect("TELEGRAM_BOT_TOKEN must be set"); - let sys = ActorSystem::new().unwrap(); + + let connection = establish_db_connection().await?; + Migrator::up(&connection, None).await?; let tgbot = Bot::new(token); - let chan: ChannelRef = channel("commands", &sys).unwrap(); + let (update_sender, _) = broadcast::channel::(1000); - let _bot_ref = sys - .actor_of_args::("bot", (bot_name, tgbot.clone(), chan.clone(), lfg_chat)) - .expect("Couldn't start the bot"); + let bot_actor = + BotActor::create(bot_name, tgbot.clone(), update_sender.clone(), lfg_chat).await; + let _bot_ref = BotActor::spawn(bot_actor); - teloxide::repl(tgbot.clone(), move |message: UpdateMessage| { - let chan = chan.clone(); + teloxide::repl(tgbot.clone(), move |bot: Bot, message: TelegramMessage| { + let update_sender = update_sender.clone(); async move { - log::debug!("Processing message {}", message.update.id); - chan.tell( - Publish { - msg: message.into(), - topic: "raw-commands".into(), - }, - None, - ); + log::debug!("Processing message {}", message.id); + let _ = update_sender.send(ActorUpdateMessage { + requester: bot, + update: message, + }); ResponseResult::<()>::Ok(()) } }) .await; + + Ok(()) } diff --git a/bot/src/commands/activities_command.rs b/bot/src/commands/activities_command.rs new file mode 100644 index 00000000..8f366fb8 --- /dev/null +++ b/bot/src/commands/activities_command.rs @@ -0,0 +1,545 @@ +use { + crate::{ + actors::bot_actor::{ActorUpdateMessage, Format}, + commands::{admin_check, match_command}, + render_template_or_err, + }, + entity::{activities, activityshortcuts}, + itertools::Itertools, + kameo::message::Context, + sea_orm::{ActiveModelTrait, EntityTrait, QueryOrder, Set}, + std::collections::HashMap, +}; + +command_actor!( + ActivitiesCommand, + "activities", + "List available activity shortcuts" +); + +impl ActivitiesCommand { + async fn all_activities_list( + &self, + connection: &DatabaseConnection, + message: &ActorUpdateMessage, + ) { + let games = activityshortcuts::Entity::find() + .find_also_related(activities::Entity) + .order_by_asc(activityshortcuts::Column::Game) + .order_by_asc(activityshortcuts::Column::Name) + .all(connection) + .await + .expect("โŒ Failed to load activity shortcuts"); + + #[derive(serde::Serialize)] + struct Game { + game: String, + shortcut: String, + activity: String, + } + + let games: Vec = games + .into_iter() + .map(|game| { + let link_activity = game.1.expect("โŒ Activity not found"); + + Game { + game: game.0.game, + shortcut: game.0.name, + activity: link_activity.format_name(), + } + }) + .collect(); + + self.send_reply_with_format( + message, + render_template_or_err!("activities/list", ("games" => &games)), + Format::Html, + ) + .await; + } + + async fn activities_ids_list( + &self, + connection: &DatabaseConnection, + message: &ActorUpdateMessage, + ) { + let games = activities::Entity::find() + .all(connection) + .await + .expect("โŒ Failed to load activities"); + + let mut text = "Activities:\n\n".to_string(); + for activity in games { + text += &format!( + "{}. {} {}\n", + activity.id, + activity.name, + activity.mode.unwrap_or("".into()) + ); + } + self.send_reply(message, text).await; + } + + async fn activity_add( + &self, + connection: &DatabaseConnection, + message: &ActorUpdateMessage, + mut argmap: HashMap<&str, &str>, + ) { + let name = argmap.remove("name"); + if name.is_none() { + return self + .send_reply(message, "โŒ Must specify activity name, see help.") + .await; + } + + let min_fireteam_size = argmap.remove("min_fireteam_size"); + if min_fireteam_size.is_none() { + return self + .send_reply(message, "โŒ Must specify min_fireteam_size, see help.") + .await; + } + let min_fireteam_size = min_fireteam_size.unwrap().parse::(); + if min_fireteam_size.is_err() { + return self + .send_reply(message, "โŒ min_fireteam_size must be a number") + .await; + } + let min_fireteam_size = min_fireteam_size.unwrap(); + + let max_fireteam_size = argmap.remove("max_fireteam_size"); + if max_fireteam_size.is_none() { + return self + .send_reply(message, "โŒ Must specify max_fireteam_size, see help.") + .await; + } + let max_fireteam_size = max_fireteam_size.unwrap().parse::(); + if max_fireteam_size.is_err() { + return self + .send_reply(message, "โŒ max_fireteam_size must be a number") + .await; + } + let max_fireteam_size = max_fireteam_size.unwrap(); + + // TODO: check for no duplicates -- ? + + let mut act = activities::ActiveModel { + name: Set(name.unwrap().to_string()), + mode: Set(None), + min_fireteam_size: Set(min_fireteam_size), + max_fireteam_size: Set(max_fireteam_size), + min_level: Set(None), + min_light: Set(None), + ..Default::default() + }; + + for (key, val) in argmap { + match key { + "min_light" => { + let val = val.parse::(); + if val.is_err() { + return self + .send_reply(message, "โŒ min_light must be a number") + .await; + } + act.min_light = Set(Some(val.unwrap())); + } + "min_level" => { + let val = val.parse::(); + if val.is_err() { + return self + .send_reply(message, "โŒ min_level must be a number") + .await; + } + act.min_level = Set(Some(val.unwrap())); + } + "mode" => act.mode = Set(Some(val.to_string())), + _ => { + return self + .send_reply(message, format!("โŒ Unknown field name {}", key)) + .await; + } + } + } + + match act.insert(connection).await { + Ok(act) => { + self.send_reply(message, format!("โœ… Activity {} added.", act.format_name())) + .await + } + Err(e) => { + self.send_reply(message, format!("โŒ Error creating activity. {:?}", e)) + .await + } + } + } + + async fn activity_add_shortcut( + &self, + connection: &DatabaseConnection, + message: &ActorUpdateMessage, + link: i32, + name: String, + game: String, + ) { + let act = activities::Entity::find_by_id(link) + .one(connection) + .await + .expect("Failed to run SQL"); + + if act.is_none() { + return self + .send_reply(message, format!("โŒ Activity {} was not found.", link)) + .await; + } + + let shortcut = activityshortcuts::ActiveModel { + name: Set(name), + game: Set(game), + link: Set(link), + ..Default::default() + }; + + if shortcut.insert(connection).await.is_err() { + return self.send_reply(message, "โŒ Error creating shortcut").await; + } + + self.send_reply(message, "โœ… Shortcut added").await; + } + + async fn activity_edit( + &self, + connection: &DatabaseConnection, + message: &ActorUpdateMessage, + id: i32, + argmap: HashMap<&str, &str>, + ) { + let act = activities::Entity::find_by_id(id) + .one(connection) + .await + .expect("โŒ Failed to run SQL"); + + if act.is_none() { + return self + .send_reply(message, format!("โŒ Activity {} was not found.", id)) + .await; + } + let act = act.unwrap(); + let mut act: activities::ActiveModel = act.into(); + + for (key, val) in argmap { + match key { + "name" => act.name = Set(val.to_string()), + "min_fireteam_size" => { + let val = val.parse::(); + if val.is_err() { + return self + .send_reply(message, "โŒ min_fireteam_size must be a number") + .await; + } + act.min_fireteam_size = Set(val.unwrap()) + } + "max_fireteam_size" => { + let val = val.parse::(); + if val.is_err() { + return self + .send_reply(message, "โŒ max_fireteam_size must be a number") + .await; + } + act.max_fireteam_size = Set(val.unwrap()) + } + "min_light" => { + let val = val.parse::(); + if val.is_err() { + return self + .send_reply(message, "โŒ min_light must be a number") + .await; + } + act.min_light = Set(Some(val.unwrap())) + } + "min_level" => { + let val = val.parse::(); + if val.is_err() { + return self + .send_reply(message, "โŒ min_level must be a number") + .await; + } + act.min_level = Set(Some(val.unwrap())) + } + "mode" => act.mode = Set(Some(val.to_string())), + _ => { + return self + .send_reply(message, format!("โŒ Unknown field name {}", key)) + .await; + } + } + } + + match act.update(connection).await { + Ok(act) => { + self.send_reply( + message, + format!("โœ… Activity {} updated.", act.format_name()), + ) + .await + } + Err(e) => { + self.send_reply(message, format!("โŒ Error updating activity. {:?}", e)) + .await + } + } + } + + async fn activity_delete( + &self, + connection: &DatabaseConnection, + message: &ActorUpdateMessage, + id: i32, + ) { + let act = activities::Entity::find_by_id(id) + .one(connection) + .await + .expect("โŒ Failed to run SQL"); + + if act.is_none() { + return self + .send_reply(message, format!("โŒ Activity {} was not found.", id)) + .await; + } + let act = act.unwrap(); + + let name = act.format_name(); + + match activities::Entity::delete_by_id(id).exec(connection).await { + Ok(_) => { + self.send_reply(message, format!("โœ… Activity {} deleted.", name)) + .await + } + Err(e) => { + // TODO: error chain? + self.send_reply(message, format!("โŒ Error deleting activity. {:?}", e)) + .await + } + } + } +} + +impl Message for ActivitiesCommand { + type Reply = (); + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), args) = + match_command(message.update.text(), Self::prefix(), &self.bot_name) + { + let connection = self.connection(); + + if args.is_none() { + // Just /activities + return self.all_activities_list(connection, &message).await; + } + + // some args - pass to a subcommand + let args = args.unwrap(); + let args: Vec<&str> = args.splitn(2, ' ').collect(); + + if args.is_empty() { + return self.usage(&message).await; + } + + let admin = admin_check(&self.bot_ref, &message, connection).await; + if admin.is_none() { + return self.send_reply(&message, "โŒ You are not admin").await; + } + + // split into subcommands: + match args[0] { + "ids" => self.activities_ids_list(connection, &message).await, + "add" => { + if args.len() < 2 { + self.send_reply(&message, "โ“ Syntax: /activities add KV") + .await; + return self.usage(&message).await; + } + + let argmap = parse_kv_args(args[1]); + if argmap.is_none() { + return self + .send_reply(&message, "โŒ Invalid activity specification, see help.") + .await; + } + let argmap = argmap.unwrap(); + self.activity_add(connection, &message, argmap).await; + } + "addsc" => { + if args.len() < 2 { + return self + .send_reply( + &message, + "โ“ Syntax: /activities addsc ActivityID ShortcutName Game name", + ) + .await; + } + + let args: Vec<&str> = args[1].splitn(3, ' ').collect(); + if args.len() != 3 { + return self.send_reply( + &message, + "โŒ To add a shortcut specify 1) activity ID, 2) shortcut name and then 3) the game name", + ).await; + } + + let link = args[0].parse::(); + if link.is_err() { + return self + .send_reply(&message, "โŒ ActivityID must be a number") + .await; + } + let link = link.unwrap(); + let name = args[1].to_string(); + let game = args[2].to_string(); + + self.activity_add_shortcut(connection, &message, link, name, game) + .await; + } + "edit" => { + if args.len() < 2 { + self.send_reply(&message, "โ“ Syntax: /activities edit ID KV") + .await; + return self.usage(&message).await; + } + + let args: Vec<&str> = args[1].splitn(2, ' ').collect(); + if args.len() != 2 { + return self + .send_reply( + &message, + "โŒ To edit first specify Activity ID and then key=value pairs", + ) + .await; + } + + let id = args[0].parse::(); + if id.is_err() { + return self + .send_reply(&message, "โŒ ActivityID must be a number") + .await; + } + let id = id.unwrap(); + + let argmap = parse_kv_args(args[1]); + if argmap.is_none() { + return self + .send_reply(&message, "โŒ Invalid activity specification, see help.") + .await; + } + let argmap = argmap.unwrap(); + + self.activity_edit(connection, &message, id, argmap).await; + } + "delete" => { + if args.len() < 2 { + self.send_reply(&message, "โ“ Syntax: /activities delete ID") + .await; + return self.usage(&message).await; + } + + let id = args[1].parse::(); + if id.is_err() { + return self + .send_reply(&message, "โŒ Activity ID must be a number") + .await; + } + let id = id.unwrap(); + + self.activity_delete(connection, &message, id).await; + } + _ => { + self.send_reply(&message, "โŒ Unknown activities operation") + .await; + self.usage(&message).await; + } + } + } + } +} + +fn parse_kv_args(args: &str) -> Option> { + fn final_collect(args: Vec<&str>) -> HashMap<&str, &str> { + args.into_iter() + .tuples() + .map(|(k, v)| (k, v.trim_matches('"'))) + .collect::>() + } + + let fragments: Vec<&str> = args.split('=').collect(); + + log::trace!("{:?}", fragments); + + match fragments.len() { + x if x < 2 => None, + 2 => + // only single parameter + { + Some(final_collect(fragments)) + } + _ => { + // ['max_fireteam_size', '1', 'name', '6', 'mode', '"Last Wish, Enhance"'] + let subfrags = fragments[1..fragments.len() - 1] + .iter() + .flat_map(|x: &&str| { + x.rsplitn(2, ' ') + .collect::>() + .into_iter() + .rev() + .collect::>() + }) + .collect::>(); + + log::trace!("{:?}", subfrags); + + let mut final_ = vec![fragments[0]]; + final_.extend(subfrags); + final_.extend(vec![fragments[fragments.len() - 1]]); + + log::trace!("Final {:?}", final_); + + let the_map = final_collect(final_); + + log::trace!(".. as map {:?}", the_map); + + Some(the_map) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_split_algorithm() { + // min_fireteam_size=1 max_fireteam_size=6 name="Last Wish, Enhanced" mode="prestige" + let args = + r#"min_fireteam_size=1 max_fireteam_size=6 name="Last Wish, Enhanced" mode="prestige""#; + let result = parse_kv_args(args); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.len(), 4); + + let args = r#"name="Last Wish, Enhanced""#; + let result = parse_kv_args(args); + assert!(result.is_some()); + let mut result = result.unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result.remove("name"), Some("Last Wish, Enhanced")); + + let args = r#"whatever else"#; + let result = parse_kv_args(args); + assert!(result.is_none()); + } +} diff --git a/bot/src/commands/cancel_command.rs b/bot/src/commands/cancel_command.rs new file mode 100644 index 00000000..06f28136 --- /dev/null +++ b/bot/src/commands/cancel_command.rs @@ -0,0 +1,131 @@ +use { + crate::{ + actors::bot_actor::ActorUpdateMessage, + commands::{decapitalize, match_command, validate_username}, + render_template_or_err, + }, + chrono::Duration, + culpa::throws, + entity::{plannedactivities, plannedactivitymembers}, + kameo::message::Context, + libbot::datetime::{format_start_time, reference_date}, + sea_orm::{ColumnTrait, EntityTrait, QueryFilter}, +}; + +command_actor!(CancelCommand, "cancel", "Leave joined activity"); + +impl Message for CancelCommand { + type Reply = anyhow::Result<()>; + + #[throws(anyhow::Error)] + async fn handle(&mut self, message: ActorUpdateMessage, _ctx: &mut Context) { + if let (Some(_), activity_id) = + match_command(message.update.text(), Self::prefix(), &self.bot_name) + { + if activity_id.is_none() { + return self.usage(&message).await; + } + + let activity_id = activity_id.unwrap().parse::(); + if activity_id.is_err() { + return self.usage(&message).await; + } + + let activity_id = activity_id.unwrap(); + + let connection = self.connection(); + + if let Some(guardian) = validate_username(&self.bot_ref, &message, connection).await { + let planned = plannedactivities::Entity::find_by_id(activity_id) + .one(connection) + .await + .expect("โŒ Failed to run SQL"); + + if planned.is_none() { + return self + .send_reply( + &message, + format!("โŒ Activity {} was not found.", activity_id), + ) + .await; + } + + let planned = planned.unwrap(); + + let member = plannedactivitymembers::Entity::find() + .filter(plannedactivitymembers::Column::PlannedActivityId.eq(activity_id)) + .filter(plannedactivitymembers::Column::UserId.eq(guardian.id)) + .one(connection) + .await + .expect("โŒ Failed to find member"); + + if member.is_none() { + return self + .send_reply(&message, "โŒ You are not a part of this group.") + .await; + } + + if chrono::DateTime::::from(planned.start) + < reference_date() - Duration::hours(1) + { + return self + .send_reply(&message, "โŒ You can not leave activities from the past.") + .await; + } + + let member = member.unwrap(); + + // Delete the member + if plannedactivitymembers::Entity::delete_by_id(member.id) + .exec(connection) + .await + .is_err() + { + return self + .send_reply(&message, "โŒ Failed to remove group member".to_string()) + .await; + } + + let act_name = planned.activity(connection).await?.unwrap().format_name(); + let act_time = decapitalize(&format_start_time( + chrono::DateTime::::from(planned.start), + reference_date(), + )); + + let suffix = if planned.members_count(connection).await? == 0 { + if plannedactivities::Entity::delete_by_id(activity_id) + .exec(connection) + .await + .is_err() + { + return self + .send_reply( + &message, + "โŒ Failed to remove planned activity".to_string(), + ) + .await; + } + render_template_or_err!("cancel/disbanded") + } else { + format!( + "{} are going\n{}", + planned.members_formatted_list(connection).await?, + planned.join_prompt(connection).await? + ) + }; + + self.send_reply( + &message, + render_template_or_err!( + "cancel/left", + ("guardian_name" => &guardian.telegram_name), + ("activity_name" => &act_name), + ("activity_time" => &act_time), + ("suffix" => &suffix) + ), + ) + .await; + } + } + } +} diff --git a/bot/src/commands/chatid_command.rs b/bot/src/commands/chatid_command.rs new file mode 100644 index 00000000..d1552157 --- /dev/null +++ b/bot/src/commands/chatid_command.rs @@ -0,0 +1,18 @@ +use crate::{actors::bot_actor::ActorUpdateMessage, commands::match_command}; + +command_actor!(ChatIdCommand, "chatid", "Figure out the numeric chat ID"); + +impl Message for ChatIdCommand { + type Reply = (); + + async fn handle( + &mut self, + msg: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), _) = match_command(msg.update.text(), Self::prefix(), &self.bot_name) { + self.send_reply(&msg, format!("โœ… Current chat id: {}", msg.update.chat.id)) + .await; + } + } +} diff --git a/bot/src/commands/d2week_command.rs b/bot/src/commands/d2week_command.rs new file mode 100644 index 00000000..7f05771a --- /dev/null +++ b/bot/src/commands/d2week_command.rs @@ -0,0 +1,25 @@ +use { + crate::{ + actors::bot_actor::{ActorUpdateMessage, Format}, + commands::match_command, + }, + kameo::message::Context, + libbot::services::destiny_schedule::this_week_in_d2, +}; + +command_actor!(D2weekCommand, "d2week", "Show current Destiny 2 week"); + +impl Message for D2weekCommand { + type Reply = (); + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), _) = match_command(message.update.text(), Self::prefix(), &self.bot_name) { + self.send_reply_with_format(&message, this_week_in_d2(), Format::Markdown) + .await; + } + } +} diff --git a/bot/src/commands/dweek_command.rs b/bot/src/commands/dweek_command.rs new file mode 100644 index 00000000..e45097d8 --- /dev/null +++ b/bot/src/commands/dweek_command.rs @@ -0,0 +1,25 @@ +use { + crate::{ + actors::bot_actor::{ActorUpdateMessage, Format}, + commands::match_command, + }, + kameo::message::Context, + libbot::services::destiny_schedule::this_week_in_d1, +}; + +command_actor!(D1weekCommand, "dweek", "Show current Destiny 1 week"); + +impl Message for D1weekCommand { + type Reply = (); + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), _) = match_command(message.update.text(), Self::prefix(), &self.bot_name) { + self.send_reply_with_format(&message, this_week_in_d1(), Format::Markdown) + .await; + } + } +} diff --git a/bot/src/commands/edit_command.rs b/bot/src/commands/edit_command.rs new file mode 100644 index 00000000..1072d948 --- /dev/null +++ b/bot/src/commands/edit_command.rs @@ -0,0 +1,163 @@ +use { + crate::{ + actors::bot_actor::ActorUpdateMessage, + commands::{match_command, validate_username}, + }, + chrono::{prelude::*, Duration}, + entity::{activityshortcuts, plannedactivities}, + kameo::message::Context, + libbot::datetime::reference_date, + sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}, +}; + +command_actor!(EditCommand, "edit", "Edit existing activity"); + +impl Message for EditCommand { + type Reply = (); + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), args) = + match_command(message.update.text(), Self::prefix(), &self.bot_name) + { + if args.is_none() { + return self.usage(&message).await; + } + let args = args.unwrap(); + + let args: Vec<_> = args.splitn(3, ' ').collect(); + if args.len() != 3 { + return self.usage(&message).await; + } + + let connection = self.connection(); + + if validate_username(&self.bot_ref, &message, connection) + .await + .is_some() + { + let id = args[0].parse::(); + if id.is_err() { + return self + .send_reply(&message, "โŒ Activity ID must be a number") + .await; + } + let id = id.unwrap(); + + let planned = plannedactivities::Entity::find_by_id(id) + .one(connection) + .await + .expect("โŒ Failed to run SQL"); + + if planned.is_none() { + return self + .send_reply(&message, format!("โŒ Activity {} was not found.", id)) + .await; + } + let planned = planned.unwrap(); + + if planned.start < reference_date() - Duration::hours(1) { + return self + .send_reply(&message, "โŒ You can not edit past activities.") + .await; + } + + match args[1] { + "time" => { + let timespec = args[2]; + let start_time = match libbot::datetime::parse_time_spec(timespec) { + Ok(start) => start, //.and_utc(), + Err(_) => { + return self + .send_reply( + &message, + format!("โŒ Failed to parse time {}", timespec), + ) + .await; + } + }; + + log::info!("...parsed `{:?}`", start_time); + + if start_time < reference_date() - Duration::hours(1) { + return self + .send_reply( + &message, + "โŒ You can not set activity time in the past.", + ) + .await; + } + + let mut planned: plannedactivities::ActiveModel = planned.into(); + let offset = start_time.offset().fix(); + planned.start = Set(start_time.with_timezone(&offset)); + + if planned.update(connection).await.is_err() { + return self + .send_reply(&message, "โŒ Failed to update start time.") + .await; + } + + self.send_reply(&message, "โœ… Start time updated.").await; + } + "details" => { + let description = args[2]; + let mut planned: plannedactivities::ActiveModel = planned.into(); + planned.details = Set(if description == "delete" { + Some(String::new()) + } else { + Some(description.to_string()) + }); + + if planned.update(connection).await.is_err() { + return self + .send_reply(&message, "โŒ Failed to update details.") + .await; + } + + self.send_reply(&message, "โœ… Details updated.").await; + } + "activity" => { + let activity = args[2]; + + let act = activityshortcuts::Entity::find() + .filter(activityshortcuts::Column::Name.eq(activity)) + .one(connection) + .await + .expect("โŒ Failed to load Activity shortcut"); + + if act.is_none() { + return self + .send_reply( + &message, + format!( + "โŒ Activity {} was not found. Use /activities for a list.", + activity + ), + ) + .await; + } + + let act = act.unwrap(); + let mut planned: plannedactivities::ActiveModel = planned.into(); + planned.activity_id = Set(act.link); + + if planned.update(connection).await.is_err() { + return self + .send_reply(&message, "โŒ Failed to update activity type.") + .await; + } + + self.send_reply(&message, "โœ… Activity type updated.").await; + } + _ => { + self.usage(&message).await; + } + } + } + } + } +} diff --git a/bot/src/commands/editguar_command.rs b/bot/src/commands/editguar_command.rs new file mode 100644 index 00000000..0d8e8648 --- /dev/null +++ b/bot/src/commands/editguar_command.rs @@ -0,0 +1,147 @@ +use { + crate::{ + actors::bot_actor::ActorUpdateMessage, + commands::{admin_check, guardian_lookup, match_command, validate_username}, + }, + entity::guardians, + kameo::message::Context, + sea_orm::{ActiveModelTrait, Set}, +}; + +command_actor!( + EditGuardianCommand, + "editguar", + "Edit information about registered guardians" +); + +impl Message for EditGuardianCommand { + type Reply = (); + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), args) = + match_command(message.update.text(), Self::prefix(), &self.bot_name) + { + if args.is_none() { + return self.usage(&message).await; + } + + // Split args in two or three: + // guardian id, + // subcommand, + // and optionally, parameters + let args = args.unwrap(); + let args: Vec<&str> = args.splitn(3, ' ').collect(); + + if args.is_empty() || args.len() == 2 { + return self.usage(&message).await; + } + + let name = args[0]; + + let connection = self.connection(); + + let guardian = if name == "my" { + let guardian = validate_username(&self.bot_ref, &message, connection).await; + if guardian.is_none() { + return; // TODO: You are not registered + } + guardian.unwrap() + } else { + let admin = admin_check(&self.bot_ref, &message, connection).await; + + if admin.is_none() { + return self.send_reply(&message, "โŒ You are not admin").await; + } + + let guardian = guardian_lookup(name, connection).await; + let guardian = match guardian { + Ok(Some(guardian)) => Some(guardian), + Ok(None) => { + self.send_reply(&message, format!("โŒ Guardian {} was not found.", &name)) + .await; + None + } + Err(_) => { + self.send_reply(&message, "โŒ Error querying guardian by name.") + .await; + None + } + }; + if guardian.is_none() { + return; + } + guardian.unwrap() + }; + + if args.len() == 1 { + let info = format!( + "{clan}{name} {email} {admin}", + clan = guardian + .psn_clan + .clone() + .map(|s| format!("[{}] ", s)) + .unwrap_or_default(), + name = guardian.telegram_name, + email = guardian.email.clone().unwrap_or("".into()), + admin = if guardian.is_superadmin { + "" + } else if guardian.is_admin { + "" + } else { + "" + }, + ); + return self.send_reply(&message, info).await; + } + + let command = args[1]; + let value = args[2]; + + match command { + "psn" => { + let mut guardian: guardians::ActiveModel = guardian.into(); + guardian.psn_name = Set(value.to_string()); + if guardian.update(connection).await.is_err() { + return self.send_reply(&message, "โŒ Failed to update PSN").await; + } + self.send_reply(&message, "โœ… PSN updated successfully") + .await; + } + "clan" => { + let clan_value = if value == "delete" { + None + } else { + Some(value.to_string()) + }; + let mut guardian: guardians::ActiveModel = guardian.into(); + guardian.psn_clan = Set(clan_value); + if guardian.update(connection).await.is_err() { + return self.send_reply(&message, "โŒ Failed to update clan").await; + } + self.send_reply(&message, "โœ… Updated guardian clan").await; + } + "email" => { + let email_value = if value == "delete" { + None + } else { + Some(value.to_string()) + }; + let mut guardian: guardians::ActiveModel = guardian.into(); + guardian.email = Set(email_value); + if guardian.update(connection).await.is_err() { + return self.send_reply(&message, "โŒ Failed to update email").await; + } + self.send_reply(&message, "โœ… Updated guardian email").await; + } + _ => { + self.send_reply(&message, "โ‰๏ธ Unknown information field") + .await; + } + } + } + } +} diff --git a/bot/src/commands/help_command.rs b/bot/src/commands/help_command.rs new file mode 100644 index 00000000..ab71d84b --- /dev/null +++ b/bot/src/commands/help_command.rs @@ -0,0 +1,21 @@ +use { + crate::{ + actors::bot_actor::{ActorUpdateMessage, ListCommands}, + commands::match_command, + }, + culpa::throws, + kameo::message::Context, +}; + +command_actor!(HelpCommand, "help", "List available commands"); + +impl Message for HelpCommand { + type Reply = anyhow::Result<()>; + + #[throws(anyhow::Error)] + async fn handle(&mut self, message: ActorUpdateMessage, _ctx: &mut Context) { + if let (Some(_), _) = match_command(message.update.text(), Self::prefix(), &self.bot_name) { + self.bot_ref.tell(ListCommands(message)).await?; + } + } +} diff --git a/bot/src/commands/join_command.rs b/bot/src/commands/join_command.rs new file mode 100644 index 00000000..7a9ebdf9 --- /dev/null +++ b/bot/src/commands/join_command.rs @@ -0,0 +1,114 @@ +use { + crate::{ + actors::bot_actor::ActorUpdateMessage, + commands::{decapitalize, match_command, validate_username}, + render_template_or_err, + }, + chrono::Duration, + culpa::throws, + entity::{plannedactivitymembers, prelude::*}, + kameo::message::Context, + libbot::datetime::{format_start_time, reference_date}, + sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}, +}; + +command_actor!(JoinCommand, "join", "Join existing activity from the list"); + +impl Message for JoinCommand { + type Reply = anyhow::Result<()>; + + #[throws(anyhow::Error)] + async fn handle(&mut self, message: ActorUpdateMessage, _ctx: &mut Context) { + if let (Some(_), activity_id) = + match_command(message.update.text(), Self::prefix(), &self.bot_name) + { + if activity_id.is_none() { + return self.usage(&message).await; + } + + let activity_id = activity_id.unwrap().parse::(); + if activity_id.is_err() { + return self.usage(&message).await; + } + + let activity_id = activity_id.unwrap(); + + let connection = self.connection(); + + if let Some(guardian) = validate_username(&self.bot_ref, &message, connection).await { + let found = PlannedActivities::find_by_id(activity_id) + .find_also_related(Activities) + .one(connection) + .await?; + + if found.is_none() { + return self + .send_reply( + &message, + format!("โŒ Activity {} was not found.", activity_id), + ) + .await; + } + + let (planned, activity) = found.unwrap(); + + let member = plannedactivitymembers::Entity::find() + .filter(plannedactivitymembers::Column::PlannedActivityId.eq(activity_id)) + .filter(plannedactivitymembers::Column::UserId.eq(guardian.id)) + .one(connection) + .await + .expect("โŒ Failed to find member"); + + if member.is_some() { + return self + .send_reply(&message, "โœ… You are already part of this group.") + .await; + } + + if planned.is_full(connection).await? { + return self + .send_reply(&message, "โŒ This activity group is full.") + .await; + } + + if planned.start < reference_date() - Duration::hours(1) { + return self + .send_reply(&message, "โŒ You can not join past activities.") + .await; + } + + let planned_activity_member = plannedactivitymembers::ActiveModel { + user_id: Set(guardian.id), + planned_activity_id: Set(planned.id), + added: Set(reference_date().into()), + ..Default::default() + }; + + if planned_activity_member.insert(connection).await.is_err() { + return self + .send_reply(&message, "๐Ÿ› Unexpected error saving group joiner") + .await; + } + + // join/joined template - TODO: format new member correctly (with icon etc) + let guar_name = guardian.to_string(); + let act_name = activity.expect("โŒ REASONS").format_name(); + let act_time = + decapitalize(&format_start_time(planned.start.into(), reference_date())); + let other_guars = planned.members_formatted_list(connection).await?; + let join_prompt = planned.join_prompt(connection).await?; + + let text = render_template_or_err!( + "join/joined", + ("guardian" => &guar_name), + ("activity_name" => &act_name), + ("activity_time" => &act_time), + ("other_guardians" => &other_guars), + ("join_prompt" => &join_prompt) + ); + + self.send_reply(&message, text).await; + } + } + } +} diff --git a/bot/src/commands/lfg_command.rs b/bot/src/commands/lfg_command.rs new file mode 100644 index 00000000..711ff781 --- /dev/null +++ b/bot/src/commands/lfg_command.rs @@ -0,0 +1,135 @@ +use { + crate::{ + actors::bot_actor::ActorUpdateMessage, + commands::{match_command, validate_username}, + render_template_or_err, + }, + entity::{activities, activityshortcuts, plannedactivities, plannedactivitymembers}, + kameo::message::Context, + libbot::datetime::{format_start_time, reference_date}, + sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}, +}; + +command_actor!(LfgCommand, "lfg", "Create a new Looking For Group event"); + +impl Message for LfgCommand { + type Reply = (); + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), args) = + match_command(message.update.text(), Self::prefix(), &self.bot_name) + { + log::info!("args are {:?}", args); + + if args.is_none() { + return self.usage(&message).await; + } + + // Split args in two: + // activity spec, + // and timespec + let args = args.unwrap(); + let args: Vec<&str> = args.splitn(2, ' ').collect(); + + if args.len() != 2 { + return self.usage(&message).await; + } + + let activity = args[0]; + let timespec = args[1]; + + log::info!("Adding activity `{}` at `{}`", &activity, ×pec); + + let connection = self.connection(); + + if let Some(guardian) = validate_username(&self.bot_ref, &message, connection).await { + let act = activityshortcuts::Entity::find() + .filter(activityshortcuts::Column::Name.eq(activity)) + .one(connection) + .await + .expect("โŒ Failed to load Activity shortcut"); + + if act.is_none() { + return self + .send_reply( + &message, + format!( + "โŒ Activity {} was not found. Use /activities to see the list.", + activity + ), + ) + .await; + } + // Parse input in MSK timezone... + let start_time = libbot::datetime::parse_time_spec(timespec); + // @todo Honor TELEGRAM_BOT_TIMEZONE envvar + + if start_time.is_err() { + return self + .send_reply(&message, format!("โŒ Failed to parse time {}", timespec)) + .await; + } + + // ...then convert back to UTC. + let start_time = start_time.unwrap(); //.and_utc(); + + let act = act.unwrap(); + + log::info!("...parsed `{:?}`", start_time); + + use chrono::Offset; + let offset = start_time.offset().fix(); + + let planned_activity = plannedactivities::ActiveModel { + author_id: Set(guardian.id), + activity_id: Set(act.link), + start: Set(start_time.with_timezone(&offset)), + ..Default::default() + }; + + // Note: Simplified without transaction for now + + let planned_activity = planned_activity + .insert(connection) + .await + .expect("โŒ Unexpected error saving LFG group"); + + let planned_activity_member = plannedactivitymembers::ActiveModel { + user_id: Set(guardian.id), + planned_activity_id: Set(planned_activity.id), + added: Set(reference_date().into()), + ..Default::default() + }; + + planned_activity_member + .insert(connection) + .await + .expect("โŒ Unexpected error saving LFG group creator"); + + let activity = activities::Entity::find_by_id(act.link) + .one(connection) + .await + .expect("โŒ Couldn't find linked activity") + .unwrap(); + + let start_time = format_start_time(start_time.to_utc(), reference_date()); + + self.send_reply( + &message, + render_template_or_err!( + "lfg/created", + ("guarName" => &guardian.to_string()), + ("groupName" => &activity.format_name()), + ("onTime" => &start_time), + ("actId" => &planned_activity.id) + ), + ) + .await; + } + } + } +} diff --git a/bot/src/commands/list_command.rs b/bot/src/commands/list_command.rs new file mode 100644 index 00000000..2a9eceb5 --- /dev/null +++ b/bot/src/commands/list_command.rs @@ -0,0 +1,40 @@ +use { + crate::{ + actors::bot_actor::{ActorUpdateMessage, Format}, + commands::{match_command, validate_username}, + render_template_or_err, + }, + entity::prelude::PlannedActivities, + futures::future::try_join_all, + kameo::message::Context, +}; + +command_actor!(ListCommand, "list", "List current events"); + +impl Message for ListCommand { + type Reply = anyhow::Result<()>; + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), _) = match_command(message.update.text(), Self::prefix(), &self.bot_name) { + let connection = self.connection(); + + if let Some(guardian) = validate_username(&self.bot_ref, &message, connection).await { + let events_data = PlannedActivities::upcoming_activities(connection).await; + let futures = events_data + .iter() + .map(|event| event.to_template(connection, Some(&guardian))); + let events_data = try_join_all(futures).await?; + + let output = render_template_or_err!("list/planned", ("events" => &events_data)); + + self.send_reply_with_format(&message, output, Format::Html) + .await; + } + } + Ok(()) + } +} diff --git a/bot/src/commands/manage_command.rs b/bot/src/commands/manage_command.rs new file mode 100644 index 00000000..033f4664 --- /dev/null +++ b/bot/src/commands/manage_command.rs @@ -0,0 +1,228 @@ +use { + crate::{ + actors::bot_actor::ActorUpdateMessage, + commands::{admin_check, guardian_lookup, match_command}, + render_template_or_err, + }, + entity::guardians, + kameo::message::Context, + sea_orm::{ActiveModelTrait, Set}, +}; + +command_actor!(ManageCommand, "manage", "Manage bot users (admin-only)"); + +impl Message for ManageCommand { + type Reply = (); + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), args) = + match_command(message.update.text(), Self::prefix(), &self.bot_name) + { + let connection = self.connection(); + + let admin = admin_check(&self.bot_ref, &message, connection).await; + + if admin.is_none() { + return self.send_reply(&message, "โŒ You are not admin").await; + } + + // let _admin = admin.unwrap(); + + if args.is_none() { + return self.usage(&message).await; + } + + // Split args in two: + // subcommand, + // and optional guardian id + let args = args.unwrap(); + let args: Vec<&str> = args.splitn(2, ' ').collect(); + + if args.is_empty() { + return self.usage(&message).await; + } + + let subcommand = args[0]; + let args = if args.len() > 1 { + Some(args[1].to_string()) + } else { + None + }; + + log::info!("{:?}", args); + + match subcommand { + "list-admins" => self.list_admins_subcommand(&message, connection).await, + "add-admin" => self.add_admin_subcommand(&message, args, connection).await, + "remove-admin" => { + self.remove_admin_subcommand(&message, args, connection) + .await + } + &_ => { + self.send_reply(&message, "โŒ Unknown management command") + .await; + } + } + } + } +} + +impl ManageCommand { + async fn list_admins_subcommand( + &self, + message: &ActorUpdateMessage, + connection: &DatabaseConnection, + ) { + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; + + let admins = guardians::Entity::find() + .filter(guardians::Column::IsAdmin.eq(true)) + .order_by_asc(guardians::Column::TelegramName) + .all(connection) + .await + .expect("โŒ Cannot execute SQL query"); + + if admins.is_empty() { + return self.send_reply(message, "โŒ No admins found").await; + } + + let admins: Vec = admins.into_iter().map(|adm| adm.format_name()).collect(); + + self.send_reply( + message, + render_template_or_err!("manage/admins", ("admins" => &admins)), + ) + .await; + } + + async fn add_admin_subcommand( + &self, + message: &ActorUpdateMessage, + args: Option, + connection: &DatabaseConnection, + ) { + let admin = admin_check(&self.bot_ref, message, connection).await; + + if admin.is_none() { + return self.send_reply(message, "โŒ You are not admin").await; + } + + let admin = admin.unwrap(); + + if !admin.is_superadmin { + return self.send_reply(message, "โŒ You are not superadmin").await; + } + + if args.is_none() { + return self + .send_reply(message, "โŒ Specify a guardian to promote to admin") + .await; + } + + let name = args.unwrap(); + + let guardian = guardian_lookup(&name, connection).await; + + match guardian { + Ok(Some(guardian)) => { + let tg_name = guardian.telegram_name.clone(); + + if guardian.is_admin { + return self + .send_reply(message, format!("โœ… @{tg_name} is already an admin")) + .await; + } + + let mut guardian: guardians::ActiveModel = guardian.into(); + guardian.is_admin = Set(true); + + if guardian.update(connection).await.is_err() { + return self.send_reply(message, "โŒ Error updating guardian").await; + } + + self.send_reply(message, format!("โœ… @{tg_name} is now an admin!")) + .await; + } + Ok(None) => { + self.send_reply(message, format!("โŒ Guardian {name} was not found.")) + .await; + } + Err(_) => { + self.send_reply(message, "โŒ Error querying guardian name.") + .await; + } + } + } + + async fn remove_admin_subcommand( + &self, + message: &ActorUpdateMessage, + args: Option, + connection: &DatabaseConnection, + ) { + let admin = admin_check(&self.bot_ref, message, connection).await; + + if admin.is_none() { + return self.send_reply(message, "โŒ You are not admin").await; + } + + let admin = admin.unwrap(); + + if !admin.is_superadmin { + return self.send_reply(message, "โŒ You are not superadmin").await; + } + + if args.is_none() { + return self + .send_reply(message, "โŒ Specify a guardian to demote from admins") + .await; + } + + let name = args.unwrap(); + + let guardian = guardian_lookup(&name, connection).await; + + match guardian { + Ok(Some(guardian)) => { + let tg_name = guardian.telegram_name.clone(); + + if !guardian.is_admin { + return self + .send_reply(message, format!("โœ… @{tg_name} is already not an admin")) + .await; + } + + if guardian.is_superadmin { + return self + .send_reply( + message, + format!("โŒ @{tg_name} is a superadmin, you can not demote."), + ) + .await; + } + + let mut guardian: guardians::ActiveModel = guardian.into(); + guardian.is_admin = Set(false); + + if guardian.update(connection).await.is_err() { + return self.send_reply(message, "โŒ Error updating guardian").await; + } + + self.send_reply(message, format!("โœ… @{tg_name} is not an admin anymore!")) + .await; + } + Ok(None) => { + self.send_reply(message, format!("โŒ Guardian {name} was not found.")) + .await; + } + Err(_) => { + self.send_reply(message, "โŒ Error querying guardian name.") + .await; + } + } + } +} diff --git a/src/commands/mod.rs b/bot/src/commands/mod.rs similarity index 56% rename from src/commands/mod.rs rename to bot/src/commands/mod.rs index 85d5ebea..6f12c5a9 100644 --- a/src/commands/mod.rs +++ b/bot/src/commands/mod.rs @@ -1,52 +1,107 @@ use { - crate::{ - bot_actor::{ActorUpdateMessage, BotActorMsg, Format, Notify, SendMessageReply}, - models::Guardian, - schema::guardians::dsl::*, - DbConnection, - }, - diesel::prelude::*, - riker::actors::{ActorRef, Tell}, + crate::actors::bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, + entity::guardians, + kameo::actor::ActorRef, + sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}, }; #[macro_export] macro_rules! command_actor { - ($name:ident, [ $($msgs:ident),* ]) => { - use $crate::{bot_actor::BotActorMsg, NamedActor, DbConnPool, BotConnection}; - use riker::actors::{ - actor, Actor, ActorFactoryArgs, ActorRef, BasicActorRef, Context, Sender, Receive, + ($name:ident, $prefix:literal, $help:literal) => { + use { + kameo::{actor::ActorRef, error::Infallible, message::*, Actor}, + sea_orm::DatabaseConnection, + $crate::BotCommand, }; - use paste::paste; #[derive(Clone)] - #[actor($($msgs)*)] pub struct $name { - bot_ref: ActorRef, + bot_ref: ActorRef<$crate::actors::bot_actor::BotActor>, bot_name: String, - connection_pool: DbConnPool, + connection_pool: DatabaseConnection, } - impl $name { - pub fn connection(&self) -> BotConnection { - self.connection_pool.get().unwrap() + impl BotCommand for $name { + fn prefix() -> &'static str { + concat!("/", $prefix) } - } - impl NamedActor for $name { - fn actor_name() -> String { std::stringify!($name).into() } + fn description() -> &'static str { + $help + } } - impl Actor for $name { - type Msg = paste! { [<$name Msg>] }; + impl $name { + pub fn new( + bot_ref: ActorRef<$crate::actors::bot_actor::BotActor>, + bot_name: String, + connection_pool: DatabaseConnection, + ) -> Self { + Self { + bot_ref, + bot_name, + connection_pool, + } + } + + pub fn connection(&self) -> &DatabaseConnection { + &self.connection_pool + } - fn recv(&mut self, ctx: &Context, msg: Self::Msg, sender: Sender) { - self.receive(ctx, msg, sender); + pub async fn usage(&self, message: &$crate::actors::bot_actor::ActorUpdateMessage) { + self.send_reply( + message, + $crate::render_template_or_err!(concat!($prefix, "/usage")), + ) + .await; + } + + #[allow(dead_code, reason = "help_command doesn't use those")] + async fn send_reply_with_format( + &self, + message: &$crate::actors::bot_actor::ActorUpdateMessage, + reply: S, + format: $crate::actors::bot_actor::Format, + ) where + S: Into, + { + let _ = self + .bot_ref + .tell($crate::actors::bot_actor::SendMessageReply( + reply.into(), + message.clone(), + format, + $crate::actors::bot_actor::Notify::Off, + )) + .await; + } + + #[allow(dead_code, reason = "help_command doesn't use those")] + async fn send_reply( + &self, + message: &$crate::actors::bot_actor::ActorUpdateMessage, + reply: S, + ) where + S: Into, + { + self.send_reply_with_format( + message, + reply, + $crate::actors::bot_actor::Format::Plain, + ) + .await; } } - impl ActorFactoryArgs<(ActorRef, String, DbConnPool)> for $name { - fn create_args((bot_ref, bot_name, connection_pool): (ActorRef, String, DbConnPool)) -> Self { - Self { bot_ref, bot_name, connection_pool } + impl Actor for $name { + type Args = Self; + type Error = Infallible; + + async fn on_start( + args: Self::Args, + _actor_ref: ActorRef, + ) -> Result { + Ok(args) } } }; @@ -91,103 +146,103 @@ pub fn decapitalize(s: &str) -> String { } /// Return a guardian record if message author is registered in Guardians table, `None` otherwise. -pub fn validate_username( - bot: &ActorRef, +pub async fn validate_username( + bot: &ActorRef, message: &ActorUpdateMessage, - connection: &DbConnection, -) -> Option { - let username = match message.update.from().as_ref().unwrap().username { + connection: &DatabaseConnection, +) -> Option { + let username = match message.update.from.as_ref().unwrap().username { None => { - bot.tell( - SendMessageReply( - "You have no telegram username, register your telegram account first.".into(), + let _ = bot + .tell(SendMessageReply( + "โŒ You have no telegram username, register your telegram account first." + .into(), message.clone(), Format::Plain, Notify::Off, - ), - None, - ); + )) + .await; return None; } Some(ref name) => name.clone(), }; - let db_user = guardians - .filter(telegram_name.eq(&username)) // @todo Fix with tg-id - .first::(connection) - .optional(); + let db_user = guardians::Entity::find() + .filter(guardians::Column::TelegramName.eq(&username)) // @todo Fix with tg-id + .one(connection) + .await; match db_user { Ok(Some(user)) => Some(user), Ok(None) => { - bot.tell( - SendMessageReply( - "You need to link your PSN account first: use /psn command".into(), + let _ = bot + .tell(SendMessageReply( + "โŒ You need to link your PSN account first: use /psn command".into(), message.clone(), Format::Plain, Notify::Off, - ), - None, - ); + )) + .await; None } Err(_) => { - bot.tell( - SendMessageReply( - "Error querying guardian info.".into(), + let _ = bot + .tell(SendMessageReply( + "โŒ Error querying guardian info.".into(), message.clone(), Format::Plain, Notify::Off, - ), - None, - ); + )) + .await; None } } } /// Return a guardian record if message author is an admin user, `None` otherwise. -pub fn admin_check( - bot: &ActorRef, +pub async fn admin_check( + bot: &ActorRef, message: &ActorUpdateMessage, - connection: &DbConnection, -) -> Option { - validate_username(bot, message, connection).filter(|g| g.is_admin) + connection: &DatabaseConnection, +) -> Option { + validate_username(bot, message, connection) + .await + .filter(|u| u.is_admin) } -pub fn guardian_lookup( +pub async fn guardian_lookup( name: &str, - connection: &DbConnection, -) -> Result, diesel::result::Error> { + connection: &DatabaseConnection, +) -> Result, sea_orm::DbErr> { if let Some(name) = name.strip_prefix('@') { - guardians - .filter(telegram_name.eq(name)) - .first::(connection) - .optional() + guardians::Entity::find() + .filter(guardians::Column::TelegramName.eq(name)) + .one(connection) + .await } else { - guardians - .filter(psn_name.ilike(&name)) - .first::(connection) - .optional() + guardians::Entity::find() + .filter(guardians::Column::PsnName.contains(name)) + .one(connection) + .await } - // @todo: lookup by integer id, positive + // @todo: lookup by integer id, positive (tg user id) } /// Match command in both variations (with bot name and without bot name). -/// @param msg Input message received from Telegram. +/// @param text Input message received from Telegram. /// @param command Command name with leading slash, if it's a root command. FIXME: Is it correct? /// @param bot_name Registered bot name. /// @returns A pair of matched command and remainder of the message text. /// (None, None) if command did not match, /// (command, and Some remaining text after command otherwise). -fn match_command( +pub fn match_command( text: Option<&str>, command: &str, bot_name: &str, ) -> (Option, Option) { // Take first token in the text - that must be the command, if any. // Split it by @ to see if we have a bot name attached - // If we do - it must match out bot name completely. + // If we do - it must match our bot name completely. // Strip trailing numeric digits from the left side - this might be part of the command argument, remember it. // The rest of the left side must match EXACTLY, not as a prefix. text.and_then(|input| { diff --git a/bot/src/commands/psn_command.rs b/bot/src/commands/psn_command.rs new file mode 100644 index 00000000..a0c0f036 --- /dev/null +++ b/bot/src/commands/psn_command.rs @@ -0,0 +1,121 @@ +use { + crate::{actors::bot_actor::ActorUpdateMessage, commands::match_command}, + entity::guardians, + kameo::message::Context, + sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}, +}; + +command_actor!(PsnCommand, "psn", "Link your telegram user to PSN"); + +impl Message for PsnCommand { + type Reply = (); + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), name) = + match_command(message.update.text(), Self::prefix(), &self.bot_name) + { + log::info!("PSN command"); + + if name.is_none() { + return self.usage(&message).await; + } + + let name = name.unwrap(); + + let from = match &message.update.from { + None => { + return self + .send_reply(&message, "Message has no sender info.") + .await; + } + Some(from) => from, + }; + + let username = match &from.username { + None => { + return self.usage(&message).await; + } + Some(name) => name, + }; + + let user_id = from.id; + + let connection = self.connection(); + + let db_user = guardians::Entity::find() + .filter(guardians::Column::TelegramId.eq(user_id.0 as i64)) + .one(connection) + .await; + + match db_user { + Ok(Some(user)) => { + let another_user = guardians::Entity::find() + .filter(guardians::Column::PsnName.contains(&name)) + .filter(guardians::Column::TelegramId.ne(user_id.0 as i64)) + .one(connection) + .await; + + match another_user { + Ok(Some(_)) => { + self.send_reply( + &message, + format!( + "โŒ The psn {psn} is already used by somebody else.", + psn = name + ), + ) + .await; + } + Ok(None) => { + let mut user: guardians::ActiveModel = user.into(); + user.telegram_name = Set(username.to_string()); + user.psn_name = Set(name.to_string()); + + if user.update(connection).await.is_err() { + self.send_reply(&message, "โŒ Failed to update PSN name.") + .await; + } else { + self.send_reply( + &message, + format!("โœ… Your PSN name updated to {}.", name), + ) + .await; + } + } + Err(_) => { + self.send_reply(&message, "โŒ Error querying guardian PSN.") + .await; + } + } + } + Ok(None) => { + let guardian = guardians::ActiveModel { + telegram_name: Set(username.to_string()), + telegram_id: Set(user_id.0 as i64), + psn_name: Set(name.to_string()), + ..Default::default() + }; + + if guardian.insert(connection).await.is_err() { + self.send_reply(&message, "โŒ Error saving guardian info.") + .await; + } else { + self.send_reply( + &message, + format!("โœ… Your PSN name is now set to {name}."), + ) + .await; + } + } + Err(_) => { + self.send_reply(&message, "โŒ Error querying guardian name.") + .await; + } + }; + } + } +} diff --git a/bot/src/commands/uptime_command.rs b/bot/src/commands/uptime_command.rs new file mode 100644 index 00000000..1be3c4be --- /dev/null +++ b/bot/src/commands/uptime_command.rs @@ -0,0 +1,47 @@ +#[cfg(target_os = "linux")] +use procfs::process::Process; +use { + crate::{actors::bot_actor::ActorUpdateMessage, commands::match_command}, + kameo::message::Context, +}; + +command_actor!(UptimeCommand, "uptime", "Show bot uptime and statistics"); + +#[cfg(target_os = "linux")] +fn get_process_info() -> String { + if let Ok(process) = Process::myself() { + use {crate::render_template_or_err, thousands::Separable}; + let stat = process.stat().unwrap(); + let page_size = procfs::page_size(); + render_template_or_err!("uptime/procinfo", + ("thn" => &stat.num_threads), + ("vmb" => &stat.vsize.separate_with_commas()), + ("vmp" => &(stat.vsize / page_size).separate_with_commas()), + ("rmp" => &stat.rss.separate_with_commas()), + ("rmb" => &(stat.rss * page_size).separate_with_commas()) + ) + } else { + "- Couldn't access process information".to_string() + } +} + +#[cfg(not(target_os = "linux"))] +fn get_process_info() -> String { + "- Process info only available on Linux hosts.".to_string() +} + +impl Message for UptimeCommand { + type Reply = (); + + async fn handle( + &mut self, + msg: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), _) = match_command(msg.update.text(), Self::prefix(), &self.bot_name) { + let uptime = libbot::datetime::format_uptime(); + let message = format!("- โฐ Started {uptime}\n{}", get_process_info()); + self.send_reply(&msg, message).await; + } + } +} diff --git a/bot/src/commands/whois_command.rs b/bot/src/commands/whois_command.rs new file mode 100644 index 00000000..395ce817 --- /dev/null +++ b/bot/src/commands/whois_command.rs @@ -0,0 +1,62 @@ +use { + crate::{ + actors::bot_actor::ActorUpdateMessage, + commands::{guardian_lookup, match_command, validate_username}, + }, + kameo::message::Context, +}; + +command_actor!(WhoisCommand, "whois", "Query telegram or PSN id"); + +impl Message for WhoisCommand { + type Reply = (); + + async fn handle( + &mut self, + message: ActorUpdateMessage, + _ctx: &mut Context, + ) -> Self::Reply { + if let (Some(_), name) = + match_command(message.update.text(), Self::prefix(), &self.bot_name) + { + if name.is_none() { + return self.usage(&message).await; + } + + let name = name.unwrap(); + + let connection = self.connection(); + + if validate_username(&self.bot_ref, &message, connection) + .await + .is_none() + { + return; // TODO: say something? + } + + let guardian = guardian_lookup(&name, connection).await; + + match guardian { + Ok(Some(guardian)) => { + self.send_reply( + &message, + format!( + "โœ… Guardian @{telegram_name} PSN {psn_name}", + telegram_name = guardian.telegram_name, + psn_name = guardian.psn_name + ), + ) + .await; + } + Ok(None) => { + self.send_reply(&message, format!("โŒ Guardian {name} was not found.")) + .await; + } + Err(_) => { + self.send_reply(&message, "โŒ Error querying guardian name.") + .await; + } + } + } + } +} diff --git a/src/lib.rs b/bot/src/lib.rs similarity index 63% rename from src/lib.rs rename to bot/src/lib.rs index ce86c31c..c0ea1852 100644 --- a/src/lib.rs +++ b/bot/src/lib.rs @@ -1,23 +1,15 @@ -// #![feature(nll)] // features from edition-2018 -// #![feature(type_alias_enum_variants)] // #![allow(proc_macro_derive_resolution_fallback)] // see https://github.com/rust-lang/rust/issues/50504 #![warn(unused_imports)] // during development #![feature(type_ascription)] -#![expect(non_local_definitions)] // Old diesel macros -#[macro_use] -extern crate diesel; -#[macro_use] -extern crate diesel_derives_extra; +use { + culpa::throws, + sea_orm::{DatabaseConnection, DbErr}, + std::{error::Error, fmt::Write}, +}; -use {diesel::pg::PgConnection, diesel_logger::LoggingConnection, r2d2::Pool}; - -pub mod bot_actor; +pub mod actors; pub mod commands; -pub mod datetime; -pub mod models; -pub mod schema; -pub mod services; static TEMPLATE_FILES: std::sync::LazyLock> = std::sync::LazyLock::new(|| include_dir::include_dir!("$CARGO_MANIFEST_DIR/templates")); @@ -36,42 +28,74 @@ pub(crate) static TEMPLATES: std::sync::LazyLock = std::sync::LazyLo tera }); -#[allow( - clippy::crate_in_macro_def, - reason = "We refer to this specific TEMPLATES instance in this specific crate" -)] +pub fn error_chain_to_string(err: &dyn Error) -> String { + let mut result = format!("Error: {}", err); + let mut current = err.source(); + while let Some(source) = current { + write!(&mut result, "\n- caused by: {}", source).unwrap(); + current = source.source(); + } + result +} + #[macro_export] macro_rules! render_template { ($template:expr) => { { - crate::TEMPLATES.render($template, &tera::Context::new()) - .map_err(|e| format!("Failed to render template '{}': {}", $template, e)) + $crate::TEMPLATES.render($template, &tera::Context::new()) + .map_err(|e| format!("{}", $crate::error_chain_to_string(&e))) } }; - ($template:expr, $(($key:expr,$value:expr)),+) => { + ($template:expr, $(($key:expr => $value:expr)),+) => { { let mut context = tera::Context::new(); $( context.insert($key, $value); )* - crate::TEMPLATES.render($template, &context) - .map_err(|e| format!("Failed to render template '{}': {}", $template, e)) + $crate::TEMPLATES.render($template, &context) + .map_err(|e| format!("{}", $crate::error_chain_to_string(&e))) } }; } -// TODO: only BotConnection should be public -pub type DbConnection = LoggingConnection; -pub type DbConnPool = Pool>; -pub type BotConnection = r2d2::PooledConnection>; +/// Like render_template, but also render the error text if any error happens during template rendering. +#[macro_export] +macro_rules! render_template_or_err { + ($template:expr) => { + { + let res = $crate::TEMPLATES.render($template, &tera::Context::new()); + if let Ok(item) = res { + item + } else { + format!("๐Ÿ› {}", $crate::error_chain_to_string(&res.unwrap_err())) + } + } + }; + ($template:expr, $(($key:expr => $value:expr)),+) => { + { + let mut context = tera::Context::new(); + $( + context.insert($key, $value); + )* + let res = $crate::TEMPLATES.render($template, &context); + if let Ok(item) = res { + item + } else { + format!("๐Ÿ› {}", $crate::error_chain_to_string(&res.unwrap_err())) + } + } + }; +} -pub trait NamedActor { - fn actor_name() -> String; +/// Establish a database connection using the entity crate +#[throws(DbErr)] +pub async fn establish_db_connection() -> DatabaseConnection { + entity::establish_db_connection().await? } pub trait BotCommand { /// Print command usage instructions. - // fn usage(&self, bot: &BotMenu, message: &UpdateWithCx, Message>); + // fn usage(&self, bot: &BotMenu, message: &UpdateWithCx, Message>); /// Return command prefix to match. /// To support sub-commands the prefix for root commands should start with '/'. fn prefix() -> &'static str; @@ -79,20 +103,6 @@ pub trait BotCommand { fn description() -> &'static str; } -/// Establish a pool of connections with DB. -pub fn establish_db_connection() -> DbConnPool { - dotenv::dotenv().ok(); - - let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - let manager = diesel::r2d2::ConnectionManager::new(database_url.clone()); - - r2d2::Pool::builder() - .min_idle(Some(1)) - .max_size(15) - .build(manager) - .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) -} - // https://chaoslibrary.blot.im/rust-cloning-a-trait-object/ // // trait BotCommandClone { @@ -113,15 +123,15 @@ pub fn establish_db_connection() -> DbConnPool { // self.clone_box() // } // } - +/* #[cfg(test)] mod tests { - // use super::*; + use super::*; // Command is prefix of another command. - // struct PrefixCommand; + struct PrefixCommand; - // struct PrefixTwoCommand; + struct PrefixTwoCommand; // impl PrefixCommand { // pub fn new() -> Box { @@ -129,15 +139,15 @@ mod tests { // } // } - // impl BotCommand for PrefixCommand { - // fn prefix() -> &'static str { - // "/prefix" - // } + impl BotCommand for PrefixCommand { + fn prefix() -> &'static str { + "/prefix" + } - // fn description() -> &'static str { - // "Test" - // } - // } + fn description() -> &'static str { + "Test" + } + } // impl PrefixTwoCommand { // pub fn new() -> Box { @@ -145,15 +155,15 @@ mod tests { // } // } - // impl BotCommand for PrefixTwoCommand { - // fn prefix() -> &'static str { - // "/prefixtwo" - // } + impl BotCommand for PrefixTwoCommand { + fn prefix() -> &'static str { + "/prefixtwo" + } - // fn description() -> &'static str { - // "Test two" - // } - // } + fn description() -> &'static str { + "Test two" + } + } // #[test] // fn test_command_insertion_order1() { @@ -209,3 +219,4 @@ mod tests { // tokio::run(retry); // } } +*/ diff --git a/bot/templates/activities/list.tera b/bot/templates/activities/list.tera new file mode 100644 index 00000000..9824efd8 --- /dev/null +++ b/bot/templates/activities/list.tera @@ -0,0 +1,11 @@ +Activities: use a short name: + +{%- set_global prevgame = "" -%} +{% for game in games -%} +{% if prevgame != game.game -%} +{% set_global prevgame = game.game %} + +*** {{ game.game }}: +{% endif %} +{{game.shortcut}} {{game.activity}} +{%- endfor %} diff --git a/bot/templates/activities/usage.tera b/bot/templates/activities/usage.tera new file mode 100644 index 00000000..1e83f782 --- /dev/null +++ b/bot/templates/activities/usage.tera @@ -0,0 +1,29 @@ +Activities command help: + +/activities + Lists all available activities shortcuts. + +๐Ÿ”‘ Admin-only mode: + +/activities ids + Lists IDs of all activities. +/activities add KV + Create new activity from KV pairs (see below). +/activities edit ID KV + Modify activity with given ID by updating all given KVs. +/activities addsc ID shortcut + Add activity shortcut for activity ID. +/activities delete ID + Remove activity if it doesn't have any activities planned. + +KV pairs are space-separated pairs of key=value elements +String arguments may be in quotes, but this is optional. + +Supported KV pairs for add/edit commands: + +name=activity name (e.g. Crucible) +mode=activity mode (e.g. Iron Banner) +min_fireteam_size=n +max_fireteam_size=n +min_light=n +min_level=n diff --git a/bot/templates/cancel/disbanded.tera b/bot/templates/cancel/disbanded.tera new file mode 100644 index 00000000..194e6611 --- /dev/null +++ b/bot/templates/cancel/disbanded.tera @@ -0,0 +1 @@ +This fireteam is disbanded and can no longer be joined. diff --git a/bot/templates/cancel/left.tera b/bot/templates/cancel/left.tera new file mode 100644 index 00000000..93520e96 --- /dev/null +++ b/bot/templates/cancel/left.tera @@ -0,0 +1,2 @@ +{{guardian_name}} has left {{activity_name}} group {{activity_time}} +{{suffix}} diff --git a/bot/templates/cancel/usage.tera b/bot/templates/cancel/usage.tera new file mode 100644 index 00000000..5f23c2fc --- /dev/null +++ b/bot/templates/cancel/usage.tera @@ -0,0 +1,4 @@ +Cancel command help: + +/cancel ActivityID + Leave planned activity by its number. diff --git a/bot/templates/edit/usage.tera b/bot/templates/edit/usage.tera new file mode 100644 index 00000000..63ee3fae --- /dev/null +++ b/bot/templates/edit/usage.tera @@ -0,0 +1,13 @@ +Edit command help: + +/edit time + Change scheduled time for activity. Time format examples: + \"tomorrow at 21:00\" or \"Friday at 9 pm\" or \"21:00\" + +/edit details + Change details/description for activity. + Use 'delete' as description text to remove details. + +/edit activity + Change type of activity, list of shortcuts + is available from output of `/activities` command. diff --git a/templates/editguar/usage.tera b/bot/templates/editguar/usage.tera similarity index 100% rename from templates/editguar/usage.tera rename to bot/templates/editguar/usage.tera diff --git a/bot/templates/en/README.md b/bot/templates/en/README.md new file mode 100644 index 00000000..4f710026 --- /dev/null +++ b/bot/templates/en/README.md @@ -0,0 +1 @@ +Template files for english version. diff --git a/bot/templates/join/joined.tera b/bot/templates/join/joined.tera new file mode 100644 index 00000000..d6fb5c14 --- /dev/null +++ b/bot/templates/join/joined.tera @@ -0,0 +1,3 @@ +{{guardian}} has joined {{activity_name}} group {{activity_time}} +{{other_guardians}} are going +{{join_prompt}}{# join link and a few ifs? #} diff --git a/bot/templates/join/usage.tera b/bot/templates/join/usage.tera new file mode 100644 index 00000000..dc0b6fe8 --- /dev/null +++ b/bot/templates/join/usage.tera @@ -0,0 +1,6 @@ +Join planned activity by its number + +Activity IDs are available from the output of `/list` command. + +`/join ActivityID` + Join planned activity by its number. diff --git a/bot/templates/lfg/created.tera b/bot/templates/lfg/created.tera new file mode 100644 index 00000000..3663185e --- /dev/null +++ b/bot/templates/lfg/created.tera @@ -0,0 +1,2 @@ +{{guarName}} is looking for {{groupName}} group {{onTime}} +Enter `/edit{{actId}} details ` to specify more details about the event. diff --git a/bot/templates/lfg/usage.tera b/bot/templates/lfg/usage.tera new file mode 100644 index 00000000..a9685aed --- /dev/null +++ b/bot/templates/lfg/usage.tera @@ -0,0 +1,8 @@ +LFG usage: /lfg + +For a list of activity codes see `/activities` +Start date can be specified as absolute date YYYY-MM-DD HH:MM or +as an open text "in 3 hours" or "next sunday". + +Example: /lfg kf 2018-09-10 23:00 +Times are in Moscow (MSK) timezone. diff --git a/templates/list/event.tera b/bot/templates/list/event.tera similarity index 85% rename from templates/list/event.tera rename to bot/templates/list/event.tera index 08013d6f..835d8572 100644 --- a/templates/list/event.tera +++ b/bot/templates/list/event.tera @@ -3,7 +3,7 @@ {{ event.details }} {% endif %} {% for member in event.members -%} -{{ member.psn_name }} (t.me/{{ member.telegram_name }}) +{% include "list/member" %} {% endfor %} โฐ {{ event.time }} {#- join_prompt -#} @@ -12,7 +12,7 @@ This activity fireteam is full. {% else %} Enter `{{ event.join_link }}` to join this group. Up to {{ event.count }} more can join. {% endif -%} -{#- end join_prompt -#} +{#- leave prompt -#} {% if event.fireteam_joined -%} Enter `{{ event.leave_link }}` to leave this group. {% endif -%} diff --git a/bot/templates/list/join.tera b/bot/templates/list/join.tera new file mode 100644 index 00000000..2ea23184 --- /dev/null +++ b/bot/templates/list/join.tera @@ -0,0 +1,5 @@ +{%- if event.fireteam_full %} +This activity fireteam is full. +{% else %} +Enter `{{ event.join_link }}` to join this group. Up to {{ event.count }} more can join. +{% endif -%} diff --git a/bot/templates/list/leave.tera b/bot/templates/list/leave.tera new file mode 100644 index 00000000..7a16ee2e --- /dev/null +++ b/bot/templates/list/leave.tera @@ -0,0 +1,3 @@ +{% if event.fireteam_joined -%} +Enter `{{ event.leave_link }}` to leave this group. +{% endif -%} diff --git a/bot/templates/list/member.tera b/bot/templates/list/member.tera new file mode 100644 index 00000000..2bb43112 --- /dev/null +++ b/bot/templates/list/member.tera @@ -0,0 +1 @@ +{{ member.icon }} {{ member.psn_name }} (t.me/{{ member.telegram_name }}) diff --git a/templates/list/planned.tera b/bot/templates/list/planned.tera similarity index 55% rename from templates/list/planned.tera rename to bot/templates/list/planned.tera index a9a4081f..a7eb2e17 100644 --- a/templates/list/planned.tera +++ b/bot/templates/list/planned.tera @@ -2,6 +2,9 @@ {% for event in events -%} {% include "list/event" %} +{%- if not loop.last %} +๐‘—Œ {#- Siddham Section Mark With Rays And Dotted Crescents #} +{% endif %} {% else -%} No activities planned, add something with `/lfg` -{%- endfor -%} +{% endfor %} diff --git a/bot/templates/manage/admins.tera b/bot/templates/manage/admins.tera new file mode 100644 index 00000000..15f5a106 --- /dev/null +++ b/bot/templates/manage/admins.tera @@ -0,0 +1,5 @@ +Existing admins: + +{% for admin in admins %} +{{admin}} +{% endfor -%} diff --git a/bot/templates/manage/usage.tera b/bot/templates/manage/usage.tera new file mode 100644 index 00000000..c08a13a9 --- /dev/null +++ b/bot/templates/manage/usage.tera @@ -0,0 +1,10 @@ +Manage command help: + +/manage list-admins + List currently registered admins. + +/manage add-admin + Grant admin rights to guardian. + +/manage remove-admin + Remove admin rights from guardian. diff --git a/bot/templates/psn/usage.tera b/bot/templates/psn/usage.tera new file mode 100644 index 00000000..e31ceec5 --- /dev/null +++ b/bot/templates/psn/usage.tera @@ -0,0 +1,4 @@ +PSN command help: + +/psn + Link your Telegram account to your PSN account. diff --git a/bot/templates/ru/README.md b/bot/templates/ru/README.md new file mode 100644 index 00000000..d8f20b1d --- /dev/null +++ b/bot/templates/ru/README.md @@ -0,0 +1 @@ +ะจะฐะฑะปะพะฝะฝั‹ะต ั„ะฐะนะปั‹ ะดะปั ั€ัƒััะบะพะน ะฒะตั€ัะธะธ. diff --git a/bot/templates/uptime/procinfo.tera b/bot/templates/uptime/procinfo.tera new file mode 100644 index 00000000..0b2af7ca --- /dev/null +++ b/bot/templates/uptime/procinfo.tera @@ -0,0 +1,3 @@ +- ๐Ÿงต {{thn}} threads +- ๐Ÿ“ƒ {{vmb}} bytes ({{vmp}} pages) virtual memory +- ๐Ÿ“ƒ {{rmb}} bytes ({{rmp}} pages) resident memory diff --git a/bot/templates/uptime/usage.tera b/bot/templates/uptime/usage.tera new file mode 100644 index 00000000..e060d527 --- /dev/null +++ b/bot/templates/uptime/usage.tera @@ -0,0 +1 @@ +Show bot uptime and statistics diff --git a/bot/templates/whois/usage.tera b/bot/templates/whois/usage.tera new file mode 100644 index 00000000..3148d410 --- /dev/null +++ b/bot/templates/whois/usage.tera @@ -0,0 +1 @@ +To query user provide his @TelegramId (starting with @) or PsnId (without the @) diff --git a/diesel.toml b/diesel.toml deleted file mode 100644 index afcfb3b4..00000000 --- a/diesel.toml +++ /dev/null @@ -1,3 +0,0 @@ -[print_schema] -file = "src/schema.rs" -patch_file = "src/schema.patch" diff --git a/entity/Cargo.toml b/entity/Cargo.toml new file mode 100644 index 00000000..8d4e770d --- /dev/null +++ b/entity/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "entity" +version = "0.4.0" +edition = "2021" +publish = false + +[dependencies] +anyhow.workspace = true +chrono.workspace = true +culpa.workspace = true +dotenv.workspace = true +futures.workspace = true +libbot.workspace = true +regex.workspace = true +sea-orm.workspace = true +serde.workspace = true +tokio.workspace = true diff --git a/entity/src/activities.rs b/entity/src/activities.rs new file mode 100644 index 00000000..5c790783 --- /dev/null +++ b/entity/src/activities.rs @@ -0,0 +1,89 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.1 +//------------------------------------------------------------------------------------------------- +// Activity +//------------------------------------------------------------------------------------------------- + +use sea_orm::entity::prelude::*; + +// Old diesel schema for reference: +// table! { +// activities (id) { +// id -> Int4, +// name -> Text, +// mode -> Nullable, +// min_fireteam_size -> Int4, +// max_fireteam_size -> Int4, +// min_light -> Nullable, +// min_level -> Nullable, +// } +// } + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "activities")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text")] + pub name: String, + #[sea_orm(column_type = "Text", nullable)] + pub mode: Option, + pub min_fireteam_size: i32, + pub max_fireteam_size: i32, + pub min_light: Option, + pub min_level: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::activityshortcuts::Entity")] + ActivityShortcuts, + #[sea_orm(has_many = "super::plannedactivities::Entity")] + PlannedActivities, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ActivityShortcuts.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::PlannedActivities.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Model { + pub fn format_name(&self) -> String { + format!("{} {}", self.name, self.mode.clone().unwrap_or_default()) + } +} + +#[cfg(test)] +mod tests { + use { + crate::establish_db_connection, + anyhow::Result, + dotenv::dotenv, + sea_orm::{EntityTrait, PaginatorTrait}, + }; + + #[tokio::test] + #[ignore] + async fn test_activities() -> Result<()> { + dotenv().ok(); + let db = establish_db_connection().await?; + + let mut results = super::Entity::find().paginate(&db, 10); + + while let Some(activities) = results.fetch_and_next().await? { + for act in activities { + println!("{}", act.format_name()); + } + } + + Ok(()) + } +} diff --git a/entity/src/activityshortcuts.rs b/entity/src/activityshortcuts.rs new file mode 100644 index 00000000..160e9d8c --- /dev/null +++ b/entity/src/activityshortcuts.rs @@ -0,0 +1,61 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.1 +//------------------------------------------------------------------------------------------------- +// ActivityShortcut +//------------------------------------------------------------------------------------------------- + +use sea_orm::entity::prelude::*; + +// Old diesel schema for reference: +// table! { +// activityshortcuts (id) { +// id -> Int4, +// name -> Text, +// game -> Text, +// link -> Int4, +// } +// } + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "activity_shortcuts")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text", unique)] + pub name: String, + #[sea_orm(column_type = "Text")] + pub game: String, + pub link: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::activities::Entity", + from = "Column::Link", + to = "super::activities::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Activities, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Activities.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +// impl Entity { +// pub fn find_one_by_name( +// connection: &DatabaseConnection, +// act_name: &str, +// ) -> diesel::result::QueryResult> { + +// ::table() +// .filter(name.eq(act_name)) +// .get_result::(connection) +// .optional() +// } +// } diff --git a/entity/src/alerts.rs b/entity/src/alerts.rs new file mode 100644 index 00000000..35f2c047 --- /dev/null +++ b/entity/src/alerts.rs @@ -0,0 +1,173 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.1 +//------------------------------------------------------------------------------------------------- +// Alert +//------------------------------------------------------------------------------------------------- + +use { + chrono::Duration, + sea_orm::entity::prelude::*, + std::{fmt, sync::LazyLock}, +}; + +// Old diesel schema for reference: +// table! { +// alerts (id) { +// id -> Int4, +// guid -> Text, +// title -> Text, +// #[sql_name = "type"] +// kind -> Text, +// startdate -> Timestamptz, +// expirydate -> Nullable, +// faction -> Nullable, +// flavor -> Nullable, +// } +// } + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "alerts")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text", unique)] + pub guid: String, + #[sea_orm(column_type = "Text")] + pub title: String, + #[sea_orm(column_type = "Text", column_name = "type")] + pub kind: String, + #[sea_orm(column_name = "startdate")] + pub start_date: ChronoDateTimeWithTimeZone, + #[sea_orm(column_name = "expirydate")] + pub expiry_date: Option, + #[sea_orm(column_type = "Text", nullable)] + pub faction: Option, + #[sea_orm(column_type = "Text", nullable)] + pub flavor: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +impl fmt::Display for Model { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} {} {}", + self.type_icon(), + self.reward_icon(), + self.title + ) + } +} + +impl Model { + pub fn is_important(&self) -> bool { + self.is_forma() + || self.is_nitain() + || self.is_orokin_reactor() + || self + .expiry_date + .map(|exp| exp - self.start_date >= Duration::minutes(90)) + .unwrap_or(false) + } + + pub fn type_icon(&self) -> String { + match self.kind.as_str() { + "Alert" => "โœŠ".into(), + "Invasion" => "๐Ÿ›".into(), + "Outbreak" => "โ›“".into(), + _ => format!("โ‰๏ธ {}", self.kind), + } + } + + pub fn reward_icon(&self) -> String { + if self.is_forma() { + "โš–" + } else if self.is_nitain() { + "โœจ" + } else if self.is_orokin_reactor() { + "๐Ÿฎ" + } else if self.is_endo() { + "๐Ÿ”ฎ" + } else if self.is_blueprint() { + "๐Ÿ—ฟ" + } else if self.is_resource() { + "๐Ÿ”‹" + } else if self.is_mod() { + "โš™" + } else if self.is_aura() { + "โ„๏ธ" + } else if self.is_credits() { + "๐Ÿ’ฐ" + } else { + "" + } + .into() + } + + pub fn is_blueprint(&self) -> bool { + self.title.contains("(Blueprint)") + } + + pub fn is_resource(&self) -> bool { + self.title.contains("(Resource)") + } + + pub fn is_mod(&self) -> bool { + self.title.contains("(Mod)") + } + + pub fn is_aura(&self) -> bool { + self.title.contains("(Aura)") + } + + pub fn is_credits(&self) -> bool { + static CREDITS: LazyLock = + LazyLock::new(|| regex::Regex::new(r"^\d+cr ").unwrap()); + CREDITS.is_match(&self.title) + } + + pub fn is_forma(&self) -> bool { + self.title.contains("Forma") + } + + pub fn is_nitain(&self) -> bool { + self.title.contains("Nitain Extract") + } + + pub fn is_orokin_reactor(&self) -> bool { + self.title.contains("Orokin Reactor") + } + + pub fn is_endo(&self) -> bool { + self.title.contains("ENDO") + } +} + +#[cfg(test)] +mod tests { + use { + crate::establish_db_connection, + anyhow::Result, + dotenv::dotenv, + sea_orm::{EntityTrait, QuerySelect}, + }; + + #[tokio::test] + #[ignore] + async fn test_alerts() -> Result<()> { + dotenv().ok(); + let db = establish_db_connection().await?; + + let results = super::Entity::find().limit(5).all(&db).await?; + + println!("Displaying {} alerts", results.len()); + for alrt in results { + println!("{}", alrt.title); + } + + Ok(()) + } +} diff --git a/entity/src/guardians.rs b/entity/src/guardians.rs new file mode 100644 index 00000000..c2f20cb9 --- /dev/null +++ b/entity/src/guardians.rs @@ -0,0 +1,116 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.1 +//------------------------------------------------------------------------------------------------- +// Guardian +//------------------------------------------------------------------------------------------------- + +use {sea_orm::entity::prelude::*, std::fmt}; + +// Old diesel schema for reference: +// table! { +// guardians (id) { +// id -> Int4, +// telegram_name -> Text, +// telegram_id -> Int8, +// psn_name -> Text, +// email -> Nullable, +// psn_clan -> Nullable, +// created_at -> Timestamptz, +// updated_at -> Timestamptz, +// deleted_at -> Nullable, +// tokens -> Nullable, +// pending_activation_code -> Nullable, +// is_admin -> Bool, +// is_superadmin -> Bool, +// } +// } + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "guardians")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text", unique)] + pub telegram_name: String, + #[sea_orm(unique)] + pub telegram_id: i64, + #[sea_orm(column_type = "Text")] + pub psn_name: String, + #[sea_orm(column_type = "Text", nullable)] + pub email: Option, + #[sea_orm(column_type = "Text", nullable)] + pub psn_clan: Option, + pub created_at: ChronoDateTimeWithTimeZone, + pub updated_at: ChronoDateTimeWithTimeZone, + pub deleted_at: Option, + #[sea_orm(column_type = "JsonBinary", nullable)] + pub tokens: Option, + #[sea_orm(column_type = "Text", nullable)] + pub pending_activation_code: Option, + pub is_admin: bool, + pub is_superadmin: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::plannedactivities::Entity")] + PlannedActivities, + #[sea_orm(has_many = "super::plannedactivitymembers::Entity")] + PlannedActivityMembers, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::PlannedActivities.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::PlannedActivityMembers.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl fmt::Display for Model { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.format_name()) + } +} + +impl Model { + pub fn format_name(&self) -> String { + format!("{} (t.me/{})", self.psn_name, self.telegram_name) + } + + pub fn names(&self) -> (String, String) { + (self.telegram_name.clone(), self.psn_name.clone()) + } +} + +#[cfg(test)] +mod tests { + use { + crate::establish_db_connection, + anyhow::Result, + dotenv::dotenv, + sea_orm::{EntityTrait, PaginatorTrait}, + }; + + #[tokio::test] + #[ignore] + async fn test_guardians() -> Result<()> { + dotenv().ok(); + let db = establish_db_connection().await?; + + let mut results = super::Entity::find().paginate(&db, 10); + + while let Some(guardians) = results.fetch_and_next().await? { + for guar in guardians { + println!("{guar}"); + } + } + + Ok(()) + } +} diff --git a/entity/src/lib.rs b/entity/src/lib.rs new file mode 100644 index 00000000..4e32dc9f --- /dev/null +++ b/entity/src/lib.rs @@ -0,0 +1,41 @@ +#![feature(duration_constructors_lite)] + +use { + culpa::throws, + sea_orm::{Database, DatabaseConnection, DbErr}, +}; + +pub mod prelude; + +pub mod activities; +pub mod activityshortcuts; +pub mod alerts; +pub mod guardians; +pub mod plannedactivities; +pub mod plannedactivitymembers; + +// Old diesel schema for reference: +// +// joinable!(activityshortcuts -> activities (link)); +// joinable!(plannedactivities -> activities (activity_id)); +// joinable!(plannedactivities -> guardians (author_id)); +// joinable!(plannedactivitymembers -> guardians (user_id)); +// joinable!(plannedactivitymembers -> plannedactivities (planned_activity_id)); +// +// allow_tables_to_appear_in_same_query!( +// activities, +// activityshortcuts, +// alerts, +// guardians, +// plannedactivities, +// plannedactivitymembers, +// ); + +/// Establish a pool of connections with DB. +#[throws(DbErr)] +pub async fn establish_db_connection() -> DatabaseConnection { + dotenv::dotenv().ok(); + + let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + Database::connect(database_url).await? +} diff --git a/entity/src/plannedactivities.rs b/entity/src/plannedactivities.rs new file mode 100644 index 00000000..eccbc07a --- /dev/null +++ b/entity/src/plannedactivities.rs @@ -0,0 +1,314 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.1 +//------------------------------------------------------------------------------------------------- +// PlannedActivity +//------------------------------------------------------------------------------------------------- + +use { + crate::{ + guardians, + plannedactivitymembers::{self, ActivityMemberTemplate}, + }, + chrono::Duration, + culpa::throws, + libbot::datetime::{format_start_time, reference_date}, + sea_orm::{entity::prelude::*, QueryOrder}, +}; + +// Old diesel schema for reference: +// table! { +// plannedactivities (id) { +// id -> Int4, +// author_id -> Int4, +// activity_id -> Int4, +// details -> Nullable, +// start -> Timestamptz, +// } +// } + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "planned_activities")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub author_id: i32, // refs Guardians + pub activity_id: i32, // refs Activities + #[sea_orm(column_type = "Text", nullable)] + pub details: Option, + pub start: ChronoDateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::activities::Entity", + from = "Column::ActivityId", + to = "super::activities::Column::Id", + on_update = "Cascade", + on_delete = "Restrict" + )] + Activities, + #[sea_orm( + belongs_to = "super::guardians::Entity", + from = "Column::AuthorId", + to = "super::guardians::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Guardians, + #[sea_orm(has_many = "super::plannedactivitymembers::Entity")] + PlannedActivityMembers, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Activities.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Guardians.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::PlannedActivityMembers.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +// Output information +#[derive(serde::Serialize)] +pub struct PlannedActivityTemplate { + pub id: i32, + pub name: String, + pub details: String, + pub members: Vec, + pub count: usize, + pub time: String, + pub fireteam_full: bool, + pub fireteam_joined: bool, + pub join_link: String, + pub leave_link: String, +} + +impl Entity { + pub async fn upcoming_activities(connection: &DatabaseConnection) -> Vec { + Self::find() + .filter(Column::Start.gt(reference_date() - Duration::minutes(60))) + .all(connection) + .await + .unwrap_or_default() + } + + pub async fn upcoming_activities_alert(connection: &DatabaseConnection) -> Option> { + // log::info!("reminder check at {}", reference_date()); + + let reference = reference_date(); + + let upcoming_events = Self::find() + .filter(Column::Start.gt(reference)) + .all(connection) + .await + .unwrap_or_default() + .into_iter() + .filter(|event| { + let event_start: chrono::DateTime = event.start.into(); + if event_start > reference { + let diff = event_start - reference; + matches!(diff.num_minutes(), 60 | 15 | 0) + } else { + false + } + }) + .collect::>(); + + if upcoming_events.is_empty() { + return None; + } + + Some(upcoming_events) + } +} + +impl Model { + #[throws(sea_orm::DbErr)] + pub async fn to_template( + &self, + connection: &DatabaseConnection, + guardian: Option<&guardians::Model>, + ) -> PlannedActivityTemplate { + let activity = self.activity(connection).await?.unwrap(); // use Relation + + let members = self.members(connection).await?; + let mut memvec = Vec::new(); + for x in members { + memvec.push(x.to_template(connection).await?); + } + let members = memvec; + + let count = activity.max_fireteam_size as usize - members.len(); + + PlannedActivityTemplate { + id: self.id, + name: activity.format_name(), + details: self.format_details(), + members, + count, + time: format_start_time(self.start.into(), reference_date()), + fireteam_full: count == 0, + fireteam_joined: self.find_member(connection, guardian).await?.is_some(), + join_link: self.join_link(), + leave_link: self.cancel_link(), + } + } + + #[throws(sea_orm::DbErr)] + pub async fn author(&self, connection: &DatabaseConnection) -> Option { + self.find_related(crate::guardians::Entity) + .one(connection) + .await? + } + + #[throws(sea_orm::DbErr)] + pub async fn activity( + &self, + connection: &DatabaseConnection, + ) -> Option { + self.find_related(crate::activities::Entity) + .one(connection) + .await? + } + + #[throws(sea_orm::DbErr)] + pub async fn members( + &self, + connection: &DatabaseConnection, + ) -> Vec { + self.find_related(crate::plannedactivitymembers::Entity) + .order_by_asc(crate::plannedactivitymembers::Column::Added) + .all(connection) + .await? + } + + #[throws(sea_orm::DbErr)] + pub async fn members_count(&self, connection: &DatabaseConnection) -> usize { + self.find_related(crate::plannedactivitymembers::Entity) + .order_by_asc(crate::plannedactivitymembers::Column::Added) + .count(connection) + .await? as usize + } + + pub fn join_link(&self) -> String { + format!("/join{}", self.id) + } + + pub fn cancel_link(&self) -> String { + format!("/cancel{}", self.id) + } + + #[throws(sea_orm::DbErr)] + pub async fn join_prompt(&self, connection: &DatabaseConnection) -> String { + if self.is_full(connection).await? { + "This activity fireteam is full.".into() + } else { + let count = self.activity(connection).await?.unwrap().max_fireteam_size as usize + - self.members_count(connection).await?; + format!( + "Enter `{joinLink}` to join this group. Up to {count} more can join.", + joinLink = self.join_link(), + count = count + ) + } + } + + #[throws(sea_orm::DbErr)] + pub async fn is_full(&self, connection: &DatabaseConnection) -> bool { + self.members_count(connection).await? + >= self.activity(connection).await?.unwrap().max_fireteam_size as usize + } + + #[throws(sea_orm::DbErr)] + pub async fn requires_more_members(&self, connection: &DatabaseConnection) -> bool { + self.members_count(connection).await? + < self.activity(connection).await?.unwrap().min_fireteam_size as usize + } + + pub fn format_details(&self) -> String { + self.details.clone().map(|s| s + "\n").unwrap_or_default() + } + + #[throws(sea_orm::DbErr)] + pub async fn members_formatted(&self, connection: &DatabaseConnection, joiner: &str) -> String { + use futures::future::try_join_all; + + let members = self.members(connection).await?; + let futures = members.into_iter().map(|guardian| { + crate::plannedactivitymembers::Model::format_member_name(connection, guardian.user_id) + }); + let names: Vec = try_join_all(futures).await?; + names.join(joiner) + } + + #[throws(sea_orm::DbErr)] + pub async fn members_formatted_list(&self, connection: &DatabaseConnection) -> String { + self.members_formatted(connection, ", ").await? + } + + #[throws(sea_orm::DbErr)] + pub async fn members_formatted_column(&self, connection: &DatabaseConnection) -> String { + self.members_formatted(connection, "\n").await? + } + + // This should be on Entity? + #[throws(sea_orm::DbErr)] + pub async fn find_member( + &self, + connection: &DatabaseConnection, + guardian: Option<&guardians::Model>, + ) -> Option { + if let Some(g) = guardian { + return plannedactivitymembers::Entity::find() + .filter(plannedactivitymembers::Column::UserId.eq(g.id)) + .filter(plannedactivitymembers::Column::PlannedActivityId.eq(self.id)) + .one(connection) + .await?; + } + None + } +} + +// #[cfg(test)] +// mod tests { +// use { +// crate::establish_db_connection, +// anyhow::Result, +// dotenv::dotenv, +// sea_orm::{EntityTrait, PaginatorTrait}, +// }; +// +// #[tokio::test] +// #[ignore] +// async fn test_planned_activities() -> Result<()> { +// dotenv().ok(); +// let db = establish_db_connection().await; +// +// // TODO: Fix this test to use SeaORM syntax +// // let guar = guardians +// // .find(1) +// // .first::(&connection) +// // .expect("Guardian with id 1 not found"); +// // let results = PlannedActivity::belonging_to(&guar) +// // .load::(&connection) +// // .expect("Error loading activities"); +// +// // println!("Displaying {} planned activities", results.len()); +// // for act in results { +// // println!("{}", act.to_string(&connection, Some(&guar))); +// // } +// +// Ok(()) +// } +// } diff --git a/entity/src/plannedactivitymembers.rs b/entity/src/plannedactivitymembers.rs new file mode 100644 index 00000000..cb71fbb4 --- /dev/null +++ b/entity/src/plannedactivitymembers.rs @@ -0,0 +1,125 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.1 +//------------------------------------------------------------------------------------------------- +// PlannedActivityMember +//------------------------------------------------------------------------------------------------- + +use { + culpa::throws, + sea_orm::entity::prelude::*, + serde::{Deserialize, Serialize}, + std::sync::LazyLock, +}; + +// Old diesel schema for reference: +// table! { +// plannedactivitymembers (id) { +// id -> Int4, +// planned_activity_id -> Int4, +// user_id -> Int4, +// added -> Timestamptz, +// } +// } + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "planned_activity_members")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub planned_activity_id: i32, + pub user_id: i32, + pub added: ChronoDateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::guardians::Entity", + from = "Column::UserId", + to = "super::guardians::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Guardians, + #[sea_orm( + belongs_to = "super::plannedactivities::Entity", + from = "Column::PlannedActivityId", + to = "super::plannedactivities::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + PlannedActivities, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Guardians.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::PlannedActivities.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Serialize, Deserialize)] +pub struct ActivityMemberTemplate { + pub psn_name: String, + pub telegram_name: String, + pub icon: String, +} + +impl Model { + #[throws(DbErr)] + pub async fn format_name(&self, connection: &DatabaseConnection) -> String { + Self::format_member_name(connection, self.user_id).await? + } + + #[throws(DbErr)] + pub async fn format_member_name(connection: &DatabaseConnection, user_id: i32) -> String { + let guardian = super::guardians::Entity::find_by_id(user_id) + .one(connection) + .await? + .ok_or(DbErr::RecordNotFound("Guardian not found".to_string()))?; + + guardian.format_name() + } + + #[throws(DbErr)] + pub async fn to_template(&self, connection: &DatabaseConnection) -> ActivityMemberTemplate { + let guardian = super::guardians::Entity::find_by_id(self.user_id) + .one(connection) + .await? + .ok_or(DbErr::RecordNotFound("Guardian not found".to_string()))?; + + let (telegram_name, psn_name) = guardian.names(); + ActivityMemberTemplate { + psn_name, + telegram_name, + icon: self.icon(), + } + } + + pub fn icon(&self) -> String { + static ICON_POOL: LazyLock> = LazyLock::new(|| { + vec![ + "๐Ÿ’‚๐Ÿป", + "๐Ÿ•ต๐Ÿผ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿญ", + "๐Ÿง‘โ€๐Ÿ’ป", + "๐Ÿง‘๐Ÿผโ€๐Ÿš’", + "๐Ÿง‘๐Ÿพโ€๐Ÿš€", + "๐Ÿฅท๐Ÿพ", + "๐Ÿฅท๐Ÿป", + "๐Ÿง™๐Ÿฝ", + "๐Ÿง๐Ÿผ", + "๐ŸงŒ", + "๐Ÿง›๐Ÿผ", + "๐ŸงŸ", + ] + }); + ICON_POOL[self.user_id.unsigned_abs() as usize % ICON_POOL.len()].into() + } +} diff --git a/entity/src/prelude.rs b/entity/src/prelude.rs new file mode 100644 index 00000000..9a35c74f --- /dev/null +++ b/entity/src/prelude.rs @@ -0,0 +1,8 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.1 + +pub use super::{ + activities::Entity as Activities, activityshortcuts::Entity as ActivityShortcuts, + alerts::Entity as Alerts, guardians::Entity as Guardians, + plannedactivities::Entity as PlannedActivities, + plannedactivitymembers::Entity as PlannedActivityMembers, +}; diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 00000000..6a754d18 --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,17 @@ +[package] +authors = ["Berkus Decker "] +edition = "2021" +name = "libbot" +version = "0.4.0" +publish = false + +[lib] +name = "libbot" + +[dependencies] +anyhow.workspace = true +chrono.workspace = true +chrono-tz.workspace = true +culpa.workspace = true +# entity.workspace = true +two_timer.workspace = true diff --git a/src/datetime/mod.rs b/lib/src/datetime.rs similarity index 92% rename from src/datetime/mod.rs rename to lib/src/datetime.rs index 0c19d2a8..27cc07c1 100644 --- a/src/datetime/mod.rs +++ b/lib/src/datetime.rs @@ -1,22 +1,41 @@ use { + anyhow::bail, chrono::{prelude::*, DateTime, Duration, TimeZone, Utc}, chrono_tz::{Europe::Moscow, Tz}, - diesel::{helper_types::AsExprOf, sql_types::Timestamptz}, + culpa::throws, std::fmt::Write, }; -// Diesel farts, see issues/1752 -pub fn nowtz() -> AsExprOf { - use diesel::{dsl::now, IntoSql}; - now.into_sql::() -} - // All internal date representation and storage is in UTC. -// MSK time used only to parse input time and to display times. +// MSK time zone is used only to parse input time and to display times. pub type BotDateTime = chrono::DateTime; pub type BotTime = chrono::NaiveTime; // UTC +#[throws(anyhow::Error)] +pub fn parse_time_spec(timespec: impl AsRef) -> DateTime { + let now = Local::now().with_timezone(&Moscow); + // Parse input in MSK timezone... + // @todo Honor TELEGRAM_BOT_TIMEZONE envvar + let start_time = match two_timer::parse( + timespec.as_ref(), + two_timer::Config::new(now).default_to_past(false), + ) { + Ok((start, _end, _found)) => start, //.and_utc(), + Err(_) => { + bail!("โŒ Failed to parse time {}", timespec.as_ref()) + } + }; + + // ...then convert back to UTC. + // let start_time = start_time.unwrap().0; //.and_utc(); + + // use chrono::Offset; + // let offset = start_time.offset().fix(); + + start_time +} + fn time_diff_string(duration: Duration) -> String { let times = vec![ (Duration::days(365), "year"), diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 00000000..f96448a3 --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,2 @@ +pub mod datetime; +pub mod services; diff --git a/src/services/destiny_schedule.rs b/lib/src/services/destiny_schedule.rs similarity index 81% rename from src/services/destiny_schedule.rs rename to lib/src/services/destiny_schedule.rs index 0edfc3c9..4f0b41b2 100644 --- a/src/services/destiny_schedule.rs +++ b/lib/src/services/destiny_schedule.rs @@ -1,10 +1,6 @@ use { - crate::{ - bot_actor::{BotActorMsg, Format, Notify, SendMessage}, - datetime::{reference_date, BotDateTime}, - }, + crate::datetime::{reference_date, BotDateTime}, chrono::{DateTime, Duration, TimeZone, Utc}, - riker::{actor::Tell, actors::ActorRef}, std::sync::LazyLock, }; // use plurals::{Lang, Plural}; @@ -40,14 +36,6 @@ fn raid_week_number(now: BotDateTime) -> i64 { // plural: "weeks", // }; -// 1. Daily resets at 20:00 MSK (17:00 UTC) every day -pub fn daily_reset(bot: ActorRef, chat_id: teloxide::types::ChatId) { - bot.tell( - SendMessage("โšก๏ธ Daily reset".into(), chat_id, Format::Plain, Notify::Off), - None, - ); -} - pub fn dreaming_city_cycle() -> String { let curses: [&'static str; 3] = ["Weak Curse", "Growing Curse", "Strongest Curse"]; let urls: [&'static str; 3] = [ @@ -97,23 +85,6 @@ pub fn ascendant_challenge_cycle() -> String { ) } -// 2. Weekly (main) resets at 20:00 msk every Tue -// 6. On main reset: change in Dreaming City curse -// dreaming city on 3-week schedule -// 7. On main reset: change in Dreaming City Ascendant Challenges -// dreaming city challenges on 6-week schedule -pub fn major_weekly_reset(bot: ActorRef, chat_id: teloxide::types::ChatId) { - let msg = format!( - "โšก๏ธ Weekly reset:\n\n{d1week}\n\n{d2week}", - d1week = this_week_in_d1(), - d2week = this_week_in_d2(), - ); - bot.tell( - SendMessage(msg, chat_id, Format::Markdown, Notify::Off), - None, - ); -} - pub fn this_week_in_d1() -> String { format!("This week in Destiny 1:\n\n{}", raid_cycle()) } diff --git a/lib/src/services/mod.rs b/lib/src/services/mod.rs new file mode 100644 index 00000000..f600498a --- /dev/null +++ b/lib/src/services/mod.rs @@ -0,0 +1 @@ +pub mod destiny_schedule; diff --git a/migration/Cargo.toml b/migration/Cargo.toml new file mode 100644 index 00000000..d248a363 --- /dev/null +++ b/migration/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "migration" +version = "0.4.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +sea-orm-migration.workspace = true +tokio.workspace = true diff --git a/migration/README.md b/migration/README.md new file mode 100644 index 00000000..3b438d89 --- /dev/null +++ b/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/migration/init.sql b/migration/init.sql new file mode 100644 index 00000000..d25192ff --- /dev/null +++ b/migration/init.sql @@ -0,0 +1,2 @@ +CREATE ROLE aeglbot WITH LOGIN PASSWORD 'whatever'; +CREATE DATABASE aeglbot OWNER aeglbot; diff --git a/migration/src/lib.rs b/migration/src/lib.rs new file mode 100644 index 00000000..680888b3 --- /dev/null +++ b/migration/src/lib.rs @@ -0,0 +1,34 @@ +pub use sea_orm_migration::prelude::*; + +mod m20180508_101204_create_alerts; +mod m20180508_101216_create_guardians; +mod m20180508_101223_create_activities; +mod m20180508_101240_create_planned_activities; +mod m20180508_101326_populate_activities; +mod m20180905_090102_populate_activities; +mod m20180921_110336_add_admins; +mod m20180922_104727_add_superadmins; +mod tables; + +pub use tables::*; + +pub struct Migrator; + +impl MigratorTrait for Migrator { + fn migration_table_name() -> DynIden { + "__seaql_migrations".into_iden() + } + + fn migrations() -> Vec> { + vec![ + Box::new(m20180508_101204_create_alerts::Migration), + Box::new(m20180508_101216_create_guardians::Migration), + Box::new(m20180508_101223_create_activities::Migration), + Box::new(m20180508_101240_create_planned_activities::Migration), + Box::new(m20180508_101326_populate_activities::Migration), + Box::new(m20180905_090102_populate_activities::Migration), + Box::new(m20180921_110336_add_admins::Migration), + Box::new(m20180922_104727_add_superadmins::Migration), + ] + } +} diff --git a/migration/src/m20180508_101204_create_alerts.rs b/migration/src/m20180508_101204_create_alerts.rs new file mode 100644 index 00000000..50745285 --- /dev/null +++ b/migration/src/m20180508_101204_create_alerts.rs @@ -0,0 +1,35 @@ +use { + crate::Alerts, + sea_orm_migration::{prelude::*, schema::*}, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(Alerts::Table) // create table if not exists alerts ( + .col(pk_auto(Alerts::Id)) // id serial primary key not null, + .col(string(Alerts::Guid)) // guid text not null unique, + .col(string(Alerts::Title)) // title text not null, + .col(string(Alerts::Type)) // type text not null, + .col(timestamp_with_time_zone(Alerts::StartDate)) // startdate timestamp with time zone not null, + .col(timestamp_with_time_zone_null(Alerts::ExpiryDate)) // expirydate timestamp with time zone, + .col(string_null(Alerts::Faction)) // faction text, + .col(string_null(Alerts::Flavor)) // flavor text + .to_owned(), // ); + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Alerts::Table).to_owned()) + .await + } +} diff --git a/migration/src/m20180508_101216_create_guardians.rs b/migration/src/m20180508_101216_create_guardians.rs new file mode 100644 index 00000000..d81b4295 --- /dev/null +++ b/migration/src/m20180508_101216_create_guardians.rs @@ -0,0 +1,47 @@ +use { + crate::Guardians, + sea_orm_migration::{prelude::*, schema::*}, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(Guardians::Table) // create table guardians ( + .col(pk_auto(Guardians::Id)) // id serial primary key not null, + .col(string_uniq(Guardians::TelegramName)) // telegram_name text not null unique, + .col(big_integer_uniq(Guardians::TelegramId)) // telegram_id bigint not null unique, + .col(string(Guardians::PsnName)) // psn_name text not null, + .col(string_null(Guardians::Email)) // email text, + .col(string_null(Guardians::PsnClan)) // psn_clan text, + .col( + timestamp_with_time_zone(Guardians::CreatedAt) // created_at timestamp with time zone not null default now(), + .default(Expr::current_timestamp()), + ) + .col( + timestamp_with_time_zone(Guardians::UpdatedAt) // updated_at timestamp with time zone not null default now(), + .default(Expr::current_timestamp()), + ) + .col( + timestamp_with_time_zone_null(Guardians::DeletedAt) // deleted_at timestamp with time zone default null, + .default(Expr::value("null")), + ) + .col(json_binary_null(Guardians::Tokens)) // tokens jsonb, + .col(string_null(Guardians::PendingActivationCode)) // pending_activation_code text + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Guardians::Table).to_owned()) + .await + } +} diff --git a/migration/src/m20180508_101223_create_activities.rs b/migration/src/m20180508_101223_create_activities.rs new file mode 100644 index 00000000..310b72b3 --- /dev/null +++ b/migration/src/m20180508_101223_create_activities.rs @@ -0,0 +1,75 @@ +use { + crate::{Activities, ActivityShortcuts}, + sea_orm_migration::{prelude::*, schema::*}, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(Activities::Table) // create table activities ( + .col(pk_auto(Activities::Id)) // id serial primary key not null, + .col(string(Activities::Name)) // name text not null, + .col(string_null(Activities::Mode)) // mode text, + .col(integer(Activities::MinFireteamSize)) // min_fireteam_size integer not null, + .col(integer(Activities::MaxFireteamSize)) // max_fireteam_size integer not null, + .col(integer_null(Activities::MinLight)) // min_light integer, + .col(integer_null(Activities::MinLevel)) // min_level integer + .to_owned(), + ) + .await?; + // create index activities_name_idx on activities(name); + manager + .create_index( + Index::create() + .if_not_exists() + .name("activities_name_idx") + .table(Activities::Table) + .col(Activities::Name) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .if_not_exists() + .table(ActivityShortcuts::Table) // create table activityshortcuts ( + .col(pk_auto(ActivityShortcuts::Id)) // id serial primary key not null, + .col(string_uniq(ActivityShortcuts::Name)) // name text not null unique, + .col(string(ActivityShortcuts::Game)) // game text not null, + .col(integer(ActivityShortcuts::Link)) // link integer not null references activities(id) on delete restrict + .foreign_key( + ForeignKey::create() + .name("activityshortcuts_link_fkey") + .from(ActivityShortcuts::Table, ActivityShortcuts::Link) + .to(Activities::Table, Activities::Id) + .on_delete(ForeignKeyAction::Cascade) // Drop all shortcuts if activity is dropped. + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // drop index activities_name_idx; + manager + .drop_index(Index::drop().name("activities_name_idx").to_owned()) + .await?; + // drop table activityshortcuts; + manager + .drop_table(Table::drop().table(ActivityShortcuts::Table).to_owned()) + .await?; + // drop table activities; + manager + .drop_table(Table::drop().table(Activities::Table).to_owned()) + .await + } +} diff --git a/migration/src/m20180508_101240_create_planned_activities.rs b/migration/src/m20180508_101240_create_planned_activities.rs new file mode 100644 index 00000000..6b30fee7 --- /dev/null +++ b/migration/src/m20180508_101240_create_planned_activities.rs @@ -0,0 +1,101 @@ +use { + crate::{Activities, Guardians, PlannedActivities, PlannedActivityMembers}, + sea_orm_migration::{prelude::*, schema::*}, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(PlannedActivities::Table) // create table plannedactivities ( + .col(pk_auto(PlannedActivities::Id)) // id serial primary key not null, + .col(string(PlannedActivities::AuthorId)) // author_id integer not null references guardians(id), + .col(integer(PlannedActivities::ActivityId)) // activity_id integer not null references activities(id) on delete restrict on update cascade, + .col(string_null(PlannedActivities::Details)) // details text, + .col(timestamp_with_time_zone(PlannedActivities::Start)) // start timestamp with time zone not null + .foreign_key( + ForeignKey::create() + .name("plannedactivities_author_id_fkey") + .from(PlannedActivities::Table, PlannedActivities::AuthorId) + .to(Guardians::Table, Guardians::Id), + ) + // -- Disallow activity drop if plannedactivities exist. + .foreign_key( + ForeignKey::create() + .name("plannedactivities_activity_id_fkey") + .from(PlannedActivities::Table, PlannedActivities::ActivityId) + .to(Activities::Table, Activities::Id) + .on_delete(ForeignKeyAction::Restrict) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .if_not_exists() + .table(PlannedActivityMembers::Table) // create table plannedactivitymembers ( + .col(pk_auto(PlannedActivityMembers::Id)) // id serial primary key not null, + .col(integer(PlannedActivityMembers::PlannedActivityId)) // planned_activity_id integer not null references plannedactivities(id), + .col(integer(PlannedActivityMembers::UserId)) // user_id integer not null references guardians(id), + .col( + timestamp_with_time_zone(PlannedActivityMembers::Added) // added timestamp with time zone not null default now(), + .default(Expr::current_timestamp()), + ) + .foreign_key( + ForeignKey::create() + .name("plannedactivitymembers_planned_activity_id_fkey") + .from( + PlannedActivityMembers::Table, + PlannedActivityMembers::PlannedActivityId, + ) + .to(PlannedActivities::Table, PlannedActivities::Id) + .on_delete(ForeignKeyAction::Cascade) // Drop all members if planned activity is dropped. + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("plannedactivitymembers_user_id_fkey") + .from( + PlannedActivityMembers::Table, + PlannedActivityMembers::UserId, + ) + .to(Guardians::Table, Guardians::Id) + .on_delete(ForeignKeyAction::Cascade) // Drop member if guardian is dropped. + .on_update(ForeignKeyAction::Cascade), + ) + .index( + Index::create() + .unique() // unique (planned_activity_id, user_id) + .name("plannedactivitymembers_planned_activity_id_user_id_key") + .col(PlannedActivityMembers::PlannedActivityId) + .col(PlannedActivityMembers::UserId), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // drop table plannedactivitymembers; + // drop table plannedactivities; + manager + .drop_table( + Table::drop() + .table(PlannedActivityMembers::Table) + .to_owned(), + ) + .await?; + manager + .drop_table(Table::drop().table(PlannedActivities::Table).to_owned()) + .await + } +} diff --git a/migration/src/m20180508_101326_populate_activities.rs b/migration/src/m20180508_101326_populate_activities.rs new file mode 100644 index 00000000..2c8071e7 --- /dev/null +++ b/migration/src/m20180508_101326_populate_activities.rs @@ -0,0 +1,173 @@ +use { + crate::{Activities, ActivityShortcuts}, + sea_orm_migration::{prelude::*, sea_orm::TransactionTrait}, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // @fixme: this already runs in a transaction, no need for a new one... + let transaction = manager.get_connection().begin().await?; + let builder = transaction.get_database_backend(); + + // INSERT INTO activities (id, name, mode, min_fireteam_size, max_fireteam_size, min_light, min_level) + // VALUES + #[rustfmt::skip] + let activities = vec![ + (1, "Vault of Glass", "normal", 1, 6, Some(200), Some(25)), + (2, "Vault of Glass", "hard", 1, 6, Some(280), Some(30)), + (3, "Crota's End", "normal", 1, 6, Some(200), Some(30)), + (4, "Crota's End", "hard", 1, 6, Some(280), Some(33)), + (5, "King's Fall", "normal", 1, 6, Some(290), Some(35)), + (6, "King's Fall", "hard", 1, 6, Some(320), Some(40)), + (7, "Wrath of the Machine", "normal", 1, 6, Some(360), Some(40)), + (8, "Wrath of the Machine", "hard", 1, 6, Some(380), Some(40)), + (9, "Vanguard", "Patrols", 1, 3, None, None), + (10, "Vanguard", "Archon's Forge", 1, 3, None, None), + (11, "Vanguard", "Court of Oryx", 1, 3, None, None), + (12, "Vanguard", "any", 1, 3, None, None), + (13, "Crucible", "Private Matches", 1, 12, None, None), + (14, "Crucible", "Trials of Osiris", 3, 3, Some(370), Some(40)), + (15, "Crucible", "Iron Banner", 1, 6, Some(350), Some(40)), + (16, "Crucible", "6v6", 1, 6, None, None), + (17, "Crucible", "3v3", 1, 3, None, None), + (19, "Crucible", "Private Tournament", 1, 12, None, None), + (20, "Vanguard", "Challenge of Elders", 1, 3, Some(320), None), + (21, "Vanguard", "Prison of Elders", 1, 3, None, None), + (22, "Vanguard", "Nightfall", 1, 3, Some(380), None), + (18, "Crucible", "any", 1, 6, None, None), + (23, "Dolmen", "any", 1, 12, None, Some(1)), + (25, "Dungeon", "any", 3, 12, None, Some(8)), + (26, "Questing", "any", 1, 12, None, Some(1)), + (27, "Cyrodiil", "pvp", 1, 12, None, Some(10)), + (24, "Delve", "any", 1, 4, None, Some(1)), + (28, "Alienation", "coop", 1, 4, None, Some(1)), + (29, "Crucible", "4v4", 1, 4, None, None), + (30, "Titanfall 2", "coop", 1, 4, None, None), + (31, "Titanfall 2", "pvp", 1, 6, None, None), + (32, "Leviathan", "normal", 1, 6, Some(100), Some(15)), + (33, "Crucible", "Trials of the Nine", 4, 4, Some(250), Some(20)), + (34, "Warframe", "pve", 1, 4, None, None), + (35, "Warframe", "pvp", 1, 4, None, None), + (36, "Warframe", "Index", 1, 4, None, None), + (37, "Warframe", "Raid (obsolete)", 4, 8, None, None), + (38, "Leviathan", "prestige", 1, 6, Some(300), Some(20)), + (39, "Leviathan, Eater of Worlds", "normal", 1, 6, Some(300), Some(20)), + (40, "Leviathan, Eater of Worlds", "prestige", 1, 6, Some(315), Some(25)), + ]; + for act in activities { + let insert = Query::insert() + .into_table(Activities::Table) + .columns([ + Activities::Id, + Activities::Name, + Activities::Mode, + Activities::MinFireteamSize, + Activities::MaxFireteamSize, + Activities::MinLight, + Activities::MinLevel, + ]) + .values_panic([ + act.0.into(), + act.1.into(), + act.2.into(), + act.3.into(), + act.4.into(), + act.5.into(), //.unwrap_or(Expr::value("null")).into(), + act.6.into(), // unwrap_or(Expr::value("null")).into(), + ]) + .to_owned(); + + let insert = builder.build(&insert); + transaction.execute(insert).await?; + } + + // INSERT INTO activityshortcuts (id, name, game, link) + // VALUES + #[rustfmt::skip] + let shortcuts = vec![ + (1, "kf", "Destiny", 6), + (2, "kfh", "Destiny", 6), + (3, "kfn", "Destiny", 5), + (4, "cr", "Destiny", 4), + (5, "crh", "Destiny", 4), + (6, "crn", "Destiny", 3), + (7, "vog", "Destiny", 2), + (8, "vogh", "Destiny", 2), + (9, "vogn", "Destiny", 1), + (10, "wotm", "Destiny", 7), + (11, "wotmh", "Destiny", 8), + (12, "wotmn", "Destiny", 7), + (13, "pvp", "Destiny", 18), + (14, "3v3", "Destiny", 17), + (15, "6v6", "Destiny", 16), + (16, "ib", "Destiny", 15), + (17, "too", "Destiny", 14), + (18, "pvt", "Destiny", 13), + (19, "trn", "Destiny", 19), + (20, "pve", "Destiny", 12), + (21, "patrol", "Destiny", 9), + (22, "coo", "Destiny", 11), + (23, "forge", "Destiny", 10), + (24, "poe", "Destiny", 21), + (25, "coe", "Destiny", 20), + (26, "nf", "Destiny", 22), + (27, "dolmen", "TESO", 23), + (28, "delve", "TESO", 24), + (29, "dung", "TESO", 25), + (30, "quest", "TESO", 26), + (31, "cyro", "TESO", 27), + (32, "cyrod", "TESO", 27), + (33, "alien", "Alienation", 28), + (34, "pvp2", "Destiny 2", 29), + (35, "tf2coop", "Titanfall 2", 30), + (36, "tf2pvp", "Titanfall 2", 31), + (37, "levin", "Destiny 2", 32), + (38, "to9", "Destiny 2", 33), + (39, "wfpve", "Warframe", 34), + (40, "wfpvp", "Warframe", 35), + (41, "wfindex", "Warframe", 36), + (42, "wfraid", "Warframe", 37), + (43, "levip", "Destiny 2", 38), + (44, "eaten", "Destiny 2", 39), + (45, "eatep", "Destiny 2", 40), + ]; + for shr in shortcuts { + let insert = Query::insert() + .into_table(ActivityShortcuts::Table) + .columns([ + ActivityShortcuts::Id, + ActivityShortcuts::Name, + ActivityShortcuts::Game, + ActivityShortcuts::Link, + ]) + .values_panic([shr.0.into(), shr.1.into(), shr.2.into(), shr.3.into()]) + .to_owned(); + + let insert = builder.build(&insert); + transaction.execute(insert).await?; + } + + transaction.commit().await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let transaction = manager.get_connection().begin().await?; + + transaction + .execute_unprepared("DELETE FROM activityshortcuts WHERE id BETWEEN 1 AND 45") + .await?; + transaction + .execute_unprepared("DELETE FROM activities WHERE id BETWEEN 1 AND 40") + .await?; + + transaction.commit().await?; + + Ok(()) + } +} diff --git a/migration/src/m20180905_090102_populate_activities.rs b/migration/src/m20180905_090102_populate_activities.rs new file mode 100644 index 00000000..73afedb1 --- /dev/null +++ b/migration/src/m20180905_090102_populate_activities.rs @@ -0,0 +1,115 @@ +use { + crate::{Activities, ActivityShortcuts}, + sea_orm_migration::{ + prelude::*, + sea_orm::{TransactionTrait, Value}, + }, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // @fixme: this already runs in a transaction, no need for a new one... + let transaction = manager.get_connection().begin().await?; + let builder = transaction.get_database_backend(); + + // INSERT INTO activities (id, name, mode, min_fireteam_size, max_fireteam_size, min_light, min_level) + // VALUES + #[rustfmt::skip] + let activities = vec![ + (41, "Vanguard", "Escalation Protocol", 1, 9, Some(350), None), + (42, "Leviathan, Spire of Stars", "normal", 6, 6, Some(370), Some(30)), + (43, "Leviathan, Spire of Stars", "prestige", 6, 6, Some(385), Some(30)), + (44, "King's Fall", "weekly", 6, 6, Some(390), Some(40)), + (45, "Crota's End", "weekly", 6, 6, Some(390), Some(40)), + (46, "Vault of Glass", "weekly", 6, 6, Some(390), Some(40)), + (47, "Wrath of the Machine", "weekly", 6, 6, Some(390), Some(40)), + (48, "Last Wish", "normal", 6, 6, Some(450), Some(40)), + (49, "Last Wish", "prestige", 6, 6, Some(500), Some(40)), + (50, "Gambit", "pve/pvp", 1, 4, Some(400), Some(30)), + ]; + for act in activities { + let insert = Query::insert() + .into_table(Activities::Table) + .columns([ + Activities::Id, + Activities::Name, + Activities::Mode, + Activities::MinFireteamSize, + Activities::MaxFireteamSize, + Activities::MinLight, + Activities::MinLevel, + ]) + .values_panic([ + act.0.into(), + act.1.into(), + act.2.into(), + act.3.into(), + act.4.into(), + act.5 + .map(|v| v.into()) + .unwrap_or(Expr::value(Value::Int(None))), + act.6 + .map(|v| v.into()) + .unwrap_or(Expr::value(Value::Int(None))), + ]) + .to_owned(); + + let insert = builder.build(&insert); + transaction.execute(insert).await?; + } + + // INSERT INTO activityshortcuts (id, name, game, link) + // VALUES + #[rustfmt::skip] + let shortcuts = vec![ + (46, "escal8", "Destiny 2", 41), + (47, "spiren", "Destiny 2", 42), + (48, "spirep", "Destiny 2", 43), + (49, "kfw", "Destiny", 44), + (50, "crw", "Destiny", 45), + (51, "vogw", "Destiny", 46), + (52, "wotmw", "Destiny", 47), + (53, "lastwn", "Destiny 2", 48), + (54, "lastwp", "Destiny 2", 49), + (55, "gambit", "Destiny 2", 50), + ]; + for shr in shortcuts { + let insert = Query::insert() + .into_table(ActivityShortcuts::Table) + .columns([ + ActivityShortcuts::Id, + ActivityShortcuts::Name, + ActivityShortcuts::Game, + ActivityShortcuts::Link, + ]) + .values_panic([shr.0.into(), shr.1.into(), shr.2.into(), shr.3.into()]) + .to_owned(); + + let insert = builder.build(&insert); + transaction.execute(insert).await?; + } + + transaction.commit().await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let transaction = manager.get_connection().begin().await?; + + transaction + .execute_unprepared("DELETE FROM activityshortcuts WHERE id BETWEEN 46 AND 55") + .await?; + transaction + .execute_unprepared("DELETE FROM activities WHERE id BETWEEN 41 AND 50") + .await?; + + transaction.commit().await?; + + Ok(()) + } +} diff --git a/migration/src/m20180921_110336_add_admins.rs b/migration/src/m20180921_110336_add_admins.rs new file mode 100644 index 00000000..4563b1ef --- /dev/null +++ b/migration/src/m20180921_110336_add_admins.rs @@ -0,0 +1,42 @@ +use { + crate::Guardians, + sea_orm_migration::{prelude::*, schema::*}, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // alter table guardians add column is_admin boolean not null default false; + manager + .alter_table( + Table::alter() + .table(Guardians::Table) + .add_column(boolean(Guardians::IsAdmin).default(Expr::value(false))) + .to_owned(), + ) + .await?; + // update guardians set is_admin = true where telegram_name = 'berkus'; + // entity::Guardians::update_one(); + let update = Query::update() + .table(Guardians::Table) + .value(Guardians::IsAdmin, true) + .and_where(Expr::col(Guardians::TelegramName).eq("berkus")) + .to_owned(); + manager.exec_stmt(update).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // alter table guardians drop column is_admin; + manager + .alter_table( + Table::alter() + .table(Guardians::Table) + .drop_column(Guardians::IsAdmin) + .to_owned(), + ) + .await + } +} diff --git a/migration/src/m20180922_104727_add_superadmins.rs b/migration/src/m20180922_104727_add_superadmins.rs new file mode 100644 index 00000000..271cf446 --- /dev/null +++ b/migration/src/m20180922_104727_add_superadmins.rs @@ -0,0 +1,42 @@ +use { + crate::Guardians, + sea_orm_migration::{prelude::*, schema::*}, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // alter table guardians add column is_superadmin boolean not null default false; + manager + .alter_table( + Table::alter() + .table(Guardians::Table) + .add_column(boolean(Guardians::IsSuperadmin).default(Expr::value(false))) + .to_owned(), + ) + .await?; + // update guardians set is_superadmin = true where telegram_name = 'berkus'; + let update = Query::update() + .table(Guardians::Table) + .value(Guardians::IsSuperadmin, true) + .and_where(Expr::col(Guardians::TelegramName).eq("berkus")) + .to_owned(); + + manager.exec_stmt(update).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // alter table guardians drop column is_superadmin; + manager + .alter_table( + Table::alter() + .table(Guardians::Table) + .drop_column(Guardians::IsSuperadmin) + .to_owned(), + ) + .await + } +} diff --git a/migration/src/m20180926_165439_add_foreign_keys.rs b/migration/src/m20180926_165439_add_foreign_keys.rs new file mode 100644 index 00000000..69667be6 --- /dev/null +++ b/migration/src/m20180926_165439_add_foreign_keys.rs @@ -0,0 +1,75 @@ +use { + crate::{Activities, ActivityShortcuts, Guardians, PlannedActivities, PlannedActivityMembers}, + sea_orm_migration::{prelude::*, schema::*}, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Replace the sample below with your own migration scripts + todo!(); + + // -- Links between activities and activityshortcuts + // -- Drop all shortcuts if activity is dropped. + // alter table activityshortcuts drop constraint if exists activityshortcuts_link_fkey; + // alter table activityshortcuts add constraint activityshortcuts_link_fkey + // foreign key (link) references activities(id) on delete cascade on update cascade; + + // -- Links between activities and plannedactivities + // -- Disallow activity drop if plannedactivities exist. + // alter table plannedactivities drop constraint if exists plannedactivities_activity_id_fkey; + // alter table plannedactivities add constraint plannedactivities_activity_id_fkey + // foreign key (activity_id) references activities(id) on delete restrict on update cascade; + + // -- Links between plannedactivitymembers and plannedactivities + // -- Drop all members if planned activity is dropped. + // alter table plannedactivitymembers drop constraint if exists plannedactivitymembers_planned_activity_id_fkey; + // alter table plannedactivitymembers add constraint plannedactivitymembers_planned_activity_id_fkey + // foreign key (planned_activity_id) references plannedactivities(id) on delete cascade on update cascade; + + // -- Drop member if guardian is dropped. + // alter table plannedactivitymembers drop constraint if exists plannedactivitymembers_user_id_fkey; + // alter table plannedactivitymembers add constraint plannedactivitymembers_user_id_fkey + // foreign key (user_id) references guardians(id) on delete cascade on update cascade; + + manager + .create_table( + Table::create() + .table(Post::Table) + .if_not_exists() + .col(pk_auto(Post::Id)) + .col(string(Post::Title)) + .col(string(Post::Text)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Replace the sample below with your own migration scripts + todo!(); + + // alter table activityshortcuts drop constraint if exists activityshortcuts_link_fkey; + // alter table activityshortcuts add constraint activityshortcuts_link_fkey + // foreign key (link) references activities(id) on delete restrict; + + // alter table plannedactivities drop constraint if exists plannedactivities_activity_id_fkey; + // alter table plannedactivities add constraint plannedactivities_activity_id_fkey + // foreign key (activity_id) references activities(id); + + // alter table plannedactivitymembers drop constraint if exists plannedactivitymembers_planned_activity_id_fkey; + // alter table plannedactivitymembers add constraint plannedactivitymembers_planned_activity_id_fkey + // foreign key (planned_activity_id) references plannedactivities(id); + + // alter table plannedactivitymembers drop constraint if exists plannedactivitymembers_user_id_fkey; + // alter table plannedactivitymembers add constraint plannedactivitymembers_user_id_fkey + // foreign key (user_id) references guardians(id); + + manager + .drop_table(Table::drop().table(Post::Table).to_owned()) + .await + } +} diff --git a/migration/src/main.rs b/migration/src/main.rs new file mode 100644 index 00000000..f054deaf --- /dev/null +++ b/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[tokio::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/migration/src/tables.rs b/migration/src/tables.rs new file mode 100644 index 00000000..576150e8 --- /dev/null +++ b/migration/src/tables.rs @@ -0,0 +1,72 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveIden)] +pub enum Alerts { + Table, + Id, + Guid, + Title, + Type, + StartDate, + ExpiryDate, + Faction, + Flavor, +} + +#[derive(DeriveIden)] +pub enum Activities { + Table, + Id, + Name, + Mode, + MinFireteamSize, + MaxFireteamSize, + MinLight, + MinLevel, +} + +#[derive(DeriveIden)] +pub enum ActivityShortcuts { + Table, + Id, + Name, + Game, + Link, +} + +#[derive(DeriveIden)] +pub enum PlannedActivities { + Table, + Id, + AuthorId, + ActivityId, + Details, + Start, +} + +#[derive(DeriveIden)] +pub enum PlannedActivityMembers { + Table, + Id, + PlannedActivityId, + UserId, + Added, +} + +#[derive(DeriveIden)] +pub enum Guardians { + Table, + Id, + TelegramName, + TelegramId, + PsnName, + Email, + PsnClan, + CreatedAt, + UpdatedAt, + DeletedAt, + Tokens, + PendingActivationCode, + IsAdmin, + IsSuperadmin, +} diff --git a/migrations/.gitkeep b/migrations/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql deleted file mode 100644 index a9f52609..00000000 --- a/migrations/00000000000000_diesel_initial_setup/down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - -DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); -DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql deleted file mode 100644 index d68895b1..00000000 --- a/migrations/00000000000000_diesel_initial_setup/up.sql +++ /dev/null @@ -1,36 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - - - - --- Sets up a trigger for the given table to automatically set a column called --- `updated_at` whenever the row is modified (unless `updated_at` was included --- in the modified columns) --- --- # Example --- --- ```sql --- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); --- --- SELECT diesel_manage_updated_at('users'); --- ``` -CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ -BEGIN - EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s - FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); -END; -$$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ -BEGIN - IF ( - NEW IS DISTINCT FROM OLD AND - NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at - ) THEN - NEW.updated_at := current_timestamp; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/migrations/2018-05-08-101204_create_alerts/down.sql b/migrations/2018-05-08-101204_create_alerts/down.sql deleted file mode 100644 index 84810116..00000000 --- a/migrations/2018-05-08-101204_create_alerts/down.sql +++ /dev/null @@ -1 +0,0 @@ -drop table alerts; diff --git a/migrations/2018-05-08-101204_create_alerts/up.sql b/migrations/2018-05-08-101204_create_alerts/up.sql deleted file mode 100644 index b5b57088..00000000 --- a/migrations/2018-05-08-101204_create_alerts/up.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table alerts ( - id serial primary key not null, - guid text not null unique, - title text not null, - type text not null, - startdate timestamp without time zone not null, - expirydate timestamp without time zone, - faction text, - flavor text -); diff --git a/migrations/2018-05-08-101216_create_guardians/down.sql b/migrations/2018-05-08-101216_create_guardians/down.sql deleted file mode 100644 index cf137a7b..00000000 --- a/migrations/2018-05-08-101216_create_guardians/down.sql +++ /dev/null @@ -1 +0,0 @@ -drop table guardians; diff --git a/migrations/2018-05-08-101216_create_guardians/up.sql b/migrations/2018-05-08-101216_create_guardians/up.sql deleted file mode 100644 index bdb7126f..00000000 --- a/migrations/2018-05-08-101216_create_guardians/up.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table guardians ( - id serial primary key not null, - telegram_name text not null unique, - telegram_id bigint not null unique, - psn_name text not null, - email text, - psn_clan text, - created_at timestamp without time zone not null default now(), - updated_at timestamp without time zone not null default now(), - deleted_at timestamp without time zone default null, - tokens jsonb, - pending_activation_code text -); diff --git a/migrations/2018-05-08-101223_create_activities/down.sql b/migrations/2018-05-08-101223_create_activities/down.sql deleted file mode 100644 index 23885e15..00000000 --- a/migrations/2018-05-08-101223_create_activities/down.sql +++ /dev/null @@ -1,3 +0,0 @@ -drop index activities_name_idx; -drop table activityshortcuts; -drop table activities; diff --git a/migrations/2018-05-08-101223_create_activities/up.sql b/migrations/2018-05-08-101223_create_activities/up.sql deleted file mode 100644 index 8efdf8f6..00000000 --- a/migrations/2018-05-08-101223_create_activities/up.sql +++ /dev/null @@ -1,18 +0,0 @@ -create table activities ( - id serial primary key not null, - name text not null, - mode text, - min_fireteam_size integer not null, - max_fireteam_size integer not null, - min_light integer, - min_level integer -); - -create index activities_name_idx on activities(name); - -create table activityshortcuts ( - id serial primary key not null, - name text not null unique, - game text not null, - link integer not null references activities(id) on delete restrict -); diff --git a/migrations/2018-05-08-101240_create_planned_activities/down.sql b/migrations/2018-05-08-101240_create_planned_activities/down.sql deleted file mode 100644 index 22e5a8de..00000000 --- a/migrations/2018-05-08-101240_create_planned_activities/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -drop table plannedactivitymembers; -drop table plannedactivities; diff --git a/migrations/2018-05-08-101240_create_planned_activities/up.sql b/migrations/2018-05-08-101240_create_planned_activities/up.sql deleted file mode 100644 index 44110798..00000000 --- a/migrations/2018-05-08-101240_create_planned_activities/up.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table plannedactivities ( - id serial primary key not null, - author_id integer not null references guardians(id), - activity_id integer not null references activities(id), - details text, - start timestamp without time zone not null -); - -create table plannedactivitymembers ( - id serial primary key not null, - planned_activity_id integer not null references plannedactivities(id), - user_id integer not null references guardians(id), - added timestamp without time zone not null default now(), - unique (planned_activity_id, user_id) -); diff --git a/migrations/2018-05-08-101245_create_reminders/down.sql b/migrations/2018-05-08-101245_create_reminders/down.sql deleted file mode 100644 index 4d68dbf1..00000000 --- a/migrations/2018-05-08-101245_create_reminders/down.sql +++ /dev/null @@ -1 +0,0 @@ -drop table plannedactivityreminders; diff --git a/migrations/2018-05-08-101245_create_reminders/up.sql b/migrations/2018-05-08-101245_create_reminders/up.sql deleted file mode 100644 index 0c550d08..00000000 --- a/migrations/2018-05-08-101245_create_reminders/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -create table plannedactivityreminders ( - id serial primary key not null, - planned_activity_id integer not null references plannedactivities(id), - user_id integer not null references guardians(id), - remind timestamp without time zone not null -); diff --git a/migrations/2018-05-08-101326_populate_activities/down.sql b/migrations/2018-05-08-101326_populate_activities/down.sql deleted file mode 100644 index f5c34e59..00000000 --- a/migrations/2018-05-08-101326_populate_activities/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -delete from activityshortcuts where id between 1 and 45; -delete from activities where id between 1 and 40; diff --git a/migrations/2018-05-08-101326_populate_activities/up.sql b/migrations/2018-05-08-101326_populate_activities/up.sql deleted file mode 100644 index 1f5d6b20..00000000 --- a/migrations/2018-05-08-101326_populate_activities/up.sql +++ /dev/null @@ -1,90 +0,0 @@ -INSERT INTO activities (id, name, mode, min_fireteam_size, max_fireteam_size, min_light, min_level) -VALUES -(1 , 'Vault of Glass', 'normal', 1, 6, 200, 25), -(2 , 'Vault of Glass', 'hard', 1, 6, 280, 30), -(3 , 'Crota''s End', 'normal', 1, 6, 200, 30), -(4 , 'Crota''s End', 'hard', 1, 6, 280, 33), -(5 , 'King''s Fall', 'normal', 1, 6, 290, 35), -(6 , 'King''s Fall', 'hard', 1, 6, 320, 40), -(7 , 'Wrath of the Machine', 'normal', 1, 6, 360, 40), -(8 , 'Wrath of the Machine', 'hard', 1, 6, 380, 40), -(9 , 'Vanguard', 'Patrols', 1, 3, null, null), -(10, 'Vanguard', 'Archon''s Forge', 1, 3, null, null), -(11, 'Vanguard', 'Court of Oryx', 1, 3, null, null), -(12, 'Vanguard', 'any', 1, 3, null, null), -(13, 'Crucible', 'Private Matches', 1, 12, null, null), -(14, 'Crucible', 'Trials of Osiris', 3, 3, 370, 40), -(15, 'Crucible', 'Iron Banner', 1, 6, 350, 40), -(16, 'Crucible', '6v6', 1, 6, null, null), -(17, 'Crucible', '3v3', 1, 3, null, null), -(19, 'Crucible', 'Private Tournament', 1, 12, null, null), -(20, 'Vanguard', 'Challenge of Elders', 1, 3, 320, null), -(21, 'Vanguard', 'Prison of Elders', 1, 3, null, null), -(22, 'Vanguard', 'Nightfall', 1, 3, 380, null), -(18, 'Crucible', 'any', 1, 6, null, null), -(23, 'Dolmen' , 'any', 1, 12, null, 1), -(25, 'Dungeon', 'any', 3, 12, null, 8), -(26, 'Questing', 'any', 1, 12, null, 1), -(27, 'Cyrodiil', 'pvp', 1, 12, null, 10), -(24, 'Delve', 'any', 1, 4, null, 1), -(28, 'Alienation', 'coop', 1, 4, null, 1), -(29, 'Crucible', '4v4', 1, 4, null, null), -(30, 'Titanfall 2', 'coop', 1, 4, null, null), -(31, 'Titanfall 2', 'pvp', 1, 6, null, null), -(32, 'Leviathan', 'normal', 1, 6, 100, 15), -(33, 'Crucible', 'Trials of the Nine', 4, 4, 250, 20), -(34, 'Warframe', 'pve', 1, 4, null, null), -(35, 'Warframe', 'pvp', 1, 4, null, null), -(36, 'Warframe', 'Index', 1, 4, null, null), -(37, 'Warframe', 'Raid (obsolete)', 4, 8, null, null), -(38, 'Leviathan', 'prestige', 1, 6, 300, 20), -(39, 'Leviathan, Eater of Worlds', 'normal', 1, 6, 300, 20), -(40, 'Leviathan, Eater of Worlds', 'prestige', 1, 6, 315, 25); - -INSERT INTO activityshortcuts (id, name, game, link) -VALUES -(1, 'kf', 'Destiny', 6), -(2, 'kfh', 'Destiny', 6), -(3, 'kfn', 'Destiny', 5), -(4, 'cr', 'Destiny', 4), -(5, 'crh', 'Destiny', 4), -(6, 'crn', 'Destiny', 3), -(7, 'vog', 'Destiny', 2), -(8, 'vogh', 'Destiny', 2), -(9, 'vogn', 'Destiny', 1), -(10, 'wotm', 'Destiny', 7), -(11, 'wotmh', 'Destiny', 8), -(12, 'wotmn', 'Destiny', 7), -(13, 'pvp', 'Destiny', 18), -(14, '3v3', 'Destiny', 17), -(15, '6v6', 'Destiny', 16), -(16, 'ib', 'Destiny', 15), -(17, 'too', 'Destiny', 14), -(18, 'pvt', 'Destiny', 13), -(19, 'trn', 'Destiny', 19), -(20, 'pve', 'Destiny', 12), -(21, 'patrol', 'Destiny', 9), -(22, 'coo', 'Destiny', 11), -(23, 'forge', 'Destiny', 10), -(24, 'poe', 'Destiny', 21), -(25, 'coe', 'Destiny', 20), -(26, 'nf', 'Destiny', 22), -(27, 'dolmen', 'TESO', 23), -(28, 'delve', 'TESO', 24), -(29, 'dung', 'TESO', 25), -(30, 'quest', 'TESO', 26), -(31, 'cyro', 'TESO', 27), -(32, 'cyrod', 'TESO', 27), -(33, 'alien', 'Alienation', 28), -(34, 'pvp2', 'Destiny 2', 29), -(35, 'tf2coop', 'Titanfall 2', 30), -(36, 'tf2pvp', 'Titanfall 2', 31), -(37, 'levin', 'Destiny 2', 32), -(38, 'to9', 'Destiny 2', 33), -(39, 'wfpve', 'Warframe', 34), -(40, 'wfpvp', 'Warframe', 35), -(41, 'wfindex', 'Warframe', 36), -(42, 'wfraid', 'Warframe', 37), -(43, 'levip', 'Destiny 2', 38), -(44, 'eaten', 'Destiny 2', 39), -(45, 'eatep', 'Destiny 2', 40); diff --git a/migrations/2018-09-05-090102_populate_activities/down.sql b/migrations/2018-09-05-090102_populate_activities/down.sql deleted file mode 100644 index 749f452c..00000000 --- a/migrations/2018-09-05-090102_populate_activities/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -delete from activityshortcuts where id between 46 and 55; -delete from activities where id between 41 and 50; diff --git a/migrations/2018-09-05-090102_populate_activities/up.sql b/migrations/2018-09-05-090102_populate_activities/up.sql deleted file mode 100644 index 3d402efc..00000000 --- a/migrations/2018-09-05-090102_populate_activities/up.sql +++ /dev/null @@ -1,25 +0,0 @@ -INSERT INTO activities (id, name, mode, min_fireteam_size, max_fireteam_size, min_light, min_level) -VALUES -(41, 'Vanguard', 'Escalation Protocol', 1, 9, 350, null), -(42, 'Leviathan, Spire of Stars', 'normal', 6, 6, 370, 30), -(43, 'Leviathan, Spire of Stars', 'prestige', 6, 6, 385, 30), -(44, 'King''s Fall', 'weekly', 6, 6, 390, 40), -(45, 'Crota''s End', 'weekly', 6, 6, 390, 40), -(46, 'Vault of Glass', 'weekly', 6, 6, 390, 40), -(47, 'Wrath of the Machine', 'weekly', 6, 6, 390, 40), -(48, 'Last Wish', 'normal', 6, 6, 450, 40), -(49, 'Last Wish', 'prestige', 6, 6, 500, 40), -(50, 'Gambit', 'pve/pvp', 1, 4, 400, 30); - -INSERT INTO activityshortcuts (id, name, game, link) -VALUES -(46, 'escal8', 'Destiny 2', 41), -(47, 'spiren', 'Destiny 2', 42), -(48, 'spirep', 'Destiny 2', 43), -(49, 'kfw' , 'Destiny' , 44), -(50, 'crw' , 'Destiny' , 45), -(51, 'vogw' , 'Destiny' , 46), -(52, 'wotmw' , 'Destiny' , 47), -(53, 'lastwn', 'Destiny 2', 48), -(54, 'lastwp', 'Destiny 2', 49), -(55, 'gambit', 'Destiny 2', 50); diff --git a/migrations/2018-09-05-091147_no-reminders-table/down.sql b/migrations/2018-09-05-091147_no-reminders-table/down.sql deleted file mode 100644 index 0c550d08..00000000 --- a/migrations/2018-09-05-091147_no-reminders-table/down.sql +++ /dev/null @@ -1,6 +0,0 @@ -create table plannedactivityreminders ( - id serial primary key not null, - planned_activity_id integer not null references plannedactivities(id), - user_id integer not null references guardians(id), - remind timestamp without time zone not null -); diff --git a/migrations/2018-09-05-091147_no-reminders-table/up.sql b/migrations/2018-09-05-091147_no-reminders-table/up.sql deleted file mode 100644 index 4d68dbf1..00000000 --- a/migrations/2018-09-05-091147_no-reminders-table/up.sql +++ /dev/null @@ -1 +0,0 @@ -drop table plannedactivityreminders; diff --git a/migrations/2018-09-05-133606_switch_to_timestamptz/down.sql b/migrations/2018-09-05-133606_switch_to_timestamptz/down.sql deleted file mode 100644 index a4889d11..00000000 --- a/migrations/2018-09-05-133606_switch_to_timestamptz/down.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE alerts ALTER COLUMN startdate TYPE timestamp USING startdate AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE alerts ALTER COLUMN expirydate TYPE timestamp USING expirydate AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE guardians ALTER COLUMN created_at TYPE timestamp USING created_at AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE guardians ALTER COLUMN updated_at TYPE timestamp USING updated_at AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE guardians ALTER COLUMN deleted_at TYPE timestamp USING deleted_at AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE plannedactivities ALTER COLUMN start TYPE timestamp USING start AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE plannedactivitymembers ALTER COLUMN added TYPE timestamp USING added AT TIME ZONE 'Europe/Moscow'; diff --git a/migrations/2018-09-05-133606_switch_to_timestamptz/up.sql b/migrations/2018-09-05-133606_switch_to_timestamptz/up.sql deleted file mode 100644 index 9baa2b58..00000000 --- a/migrations/2018-09-05-133606_switch_to_timestamptz/up.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE alerts ALTER COLUMN startdate TYPE timestamp with time zone USING startdate AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE alerts ALTER COLUMN expirydate TYPE timestamp with time zone USING expirydate AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE guardians ALTER COLUMN created_at TYPE timestamp with time zone USING created_at AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE guardians ALTER COLUMN updated_at TYPE timestamp with time zone USING updated_at AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE guardians ALTER COLUMN deleted_at TYPE timestamp with time zone USING deleted_at AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE plannedactivities ALTER COLUMN start TYPE timestamp with time zone USING start AT TIME ZONE 'Europe/Moscow'; -ALTER TABLE plannedactivitymembers ALTER COLUMN added TYPE timestamp with time zone USING added AT TIME ZONE 'Europe/Moscow'; diff --git a/migrations/2018-09-21-110336_add_admins/down.sql b/migrations/2018-09-21-110336_add_admins/down.sql deleted file mode 100644 index 946dba4a..00000000 --- a/migrations/2018-09-21-110336_add_admins/down.sql +++ /dev/null @@ -1 +0,0 @@ -alter table guardians drop column is_admin; diff --git a/migrations/2018-09-21-110336_add_admins/up.sql b/migrations/2018-09-21-110336_add_admins/up.sql deleted file mode 100644 index 50874cea..00000000 --- a/migrations/2018-09-21-110336_add_admins/up.sql +++ /dev/null @@ -1,3 +0,0 @@ -alter table guardians add column is_admin boolean not null default false; - -update guardians set is_admin = true where telegram_name = 'berkus'; diff --git a/migrations/2018-09-22-104727_add_superadmins/down.sql b/migrations/2018-09-22-104727_add_superadmins/down.sql deleted file mode 100644 index a2552e29..00000000 --- a/migrations/2018-09-22-104727_add_superadmins/down.sql +++ /dev/null @@ -1 +0,0 @@ -alter table guardians drop column is_superadmin; diff --git a/migrations/2018-09-22-104727_add_superadmins/up.sql b/migrations/2018-09-22-104727_add_superadmins/up.sql deleted file mode 100644 index eb3f467b..00000000 --- a/migrations/2018-09-22-104727_add_superadmins/up.sql +++ /dev/null @@ -1,3 +0,0 @@ -alter table guardians add column is_superadmin boolean not null default false; - -update guardians set is_superadmin = true where telegram_name = 'berkus'; diff --git a/migrations/2018-09-26-165439_add_foreign_keys/down.sql b/migrations/2018-09-26-165439_add_foreign_keys/down.sql deleted file mode 100644 index c4303e64..00000000 --- a/migrations/2018-09-26-165439_add_foreign_keys/down.sql +++ /dev/null @@ -1,15 +0,0 @@ -alter table activityshortcuts drop constraint if exists activityshortcuts_link_fkey; -alter table activityshortcuts add constraint activityshortcuts_link_fkey -foreign key (link) references activities(id) on delete restrict; - -alter table plannedactivities drop constraint if exists plannedactivities_activity_id_fkey; -alter table plannedactivities add constraint plannedactivities_activity_id_fkey -foreign key (activity_id) references activities(id); - -alter table plannedactivitymembers drop constraint if exists plannedactivitymembers_planned_activity_id_fkey; -alter table plannedactivitymembers add constraint plannedactivitymembers_planned_activity_id_fkey -foreign key (planned_activity_id) references plannedactivities(id); - -alter table plannedactivitymembers drop constraint if exists plannedactivitymembers_user_id_fkey; -alter table plannedactivitymembers add constraint plannedactivitymembers_user_id_fkey -foreign key (user_id) references guardians(id); diff --git a/migrations/2018-09-26-165439_add_foreign_keys/up.sql b/migrations/2018-09-26-165439_add_foreign_keys/up.sql deleted file mode 100644 index f90ecd37..00000000 --- a/migrations/2018-09-26-165439_add_foreign_keys/up.sql +++ /dev/null @@ -1,22 +0,0 @@ --- Links between activities and activityshortcuts --- Drop all shortcuts if activity is dropped. -alter table activityshortcuts drop constraint if exists activityshortcuts_link_fkey; -alter table activityshortcuts add constraint activityshortcuts_link_fkey -foreign key (link) references activities(id) on delete cascade on update cascade; - --- Links between activities and plannedactivities --- Disallow activity drop if plannedactivities exist. -alter table plannedactivities drop constraint if exists plannedactivities_activity_id_fkey; -alter table plannedactivities add constraint plannedactivities_activity_id_fkey -foreign key (activity_id) references activities(id) on delete restrict on update cascade; - --- Links between plannedactivitymembers and plannedactivities --- Drop all members if planned activity is dropped. -alter table plannedactivitymembers drop constraint if exists plannedactivitymembers_planned_activity_id_fkey; -alter table plannedactivitymembers add constraint plannedactivitymembers_planned_activity_id_fkey -foreign key (planned_activity_id) references plannedactivities(id) on delete cascade on update cascade; - --- Drop member if guardian is dropped. -alter table plannedactivitymembers drop constraint if exists plannedactivitymembers_user_id_fkey; -alter table plannedactivitymembers add constraint plannedactivitymembers_user_id_fkey -foreign key (user_id) references guardians(id) on delete cascade on update cascade; diff --git a/src/bot_actor.rs b/src/bot_actor.rs deleted file mode 100644 index 36817254..00000000 --- a/src/bot_actor.rs +++ /dev/null @@ -1,266 +0,0 @@ -use { - crate::{ - commands::*, - establish_db_connection, - services::reminder_actor::{ - ReminderActor, ScheduleNextDay, ScheduleNextMinute, ScheduleNextWeek, - }, - BotCommand, DbConnPool, NamedActor, - }, - riker::actors::{ - actor, Actor, ActorFactoryArgs, ActorRefFactory, BasicActorRef, ChannelRef, Context, - Receive, Sender, Subscribe, Tell, - }, - std::fmt::Formatter, - teloxide::{ - prelude::*, - types::{ChatId, ParseMode}, - }, -}; - -#[derive(Clone)] -#[actor(SendMessage, SendMessageReply, ListCommands)] -pub struct BotActor { - pub bot: Bot, - bot_name: String, - lfg_chat_id: i64, - update_channel: ChannelRef, - connection_pool: DbConnPool, - commands_list: Vec<(String, String)>, -} - -unsafe impl Send for BotActor {} - -impl std::fmt::Debug for BotActor { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "BotActor") - } -} - -pub type UpdateMessage = UpdateWithCx; -pub type ActorUpdateMessage = ActorUpdateWithCx; - -// Manually derived version of UpdateWithCx<_, _> -#[derive(Debug, Clone)] -pub struct ActorUpdateWithCx { - pub requester: R, - pub update: Upd, -} - -impl From for ActorUpdateMessage { - fn from(m: UpdateMessage) -> Self { - Self { - requester: m.requester, - update: m.update, - } - } -} - -impl BotActor { - // Public API - - pub fn new( - name: &str, - bot: Bot, - chan: ChannelRef, - lfg_chat_id: i64, - ) -> Self { - BotActor { - bot, - bot_name: name.to_string(), - lfg_chat_id, - update_channel: chan, - connection_pool: establish_db_connection(), - commands_list: vec![], - } - } - - pub fn list_commands(&self) -> Vec<(String, String)> { - self.commands_list.clone() - } - - // Internal helpers - - // fn handle_error(error: anyhow::Error) -> RetryPolicy { - // // count errors - // log::error!("handle_error"); - // match error.downcast_ref::() { - // Some(te) => { - // log::error!("Telegram error: {}, retrying connection.", te); - // RetryPolicy::WaitRetry(Duration::from_secs(30)) - // } - // None => { - // log::error!("handle_error didn't match, real error {:?}", error); - // //handle_error didnt match, real error Io(Custom { kind: Other, error: StringError("failed to lookup address information: nodename nor servname provided, or not known") }) - // RetryPolicy::ForwardError(error) - // } - // } - // } -} - -impl Actor for BotActor { - type Msg = BotActorMsg; - - /// Register all bot commands and subscribe them to the system notification channel. - fn pre_start(&mut self, ctx: &Context) { - macro_rules! new_command { - ($T:ident) => { - let cmd = ctx - .actor_of_args::<$T, _>(&$T::actor_name(), (ctx.myself().clone(), self.bot_name.clone(), self.connection_pool.clone())) - .unwrap(); // FIXME: panics in pre_start do not cause actor restart, so this is faulty! - self.commands_list.push(($T::prefix().into(), $T::description().into())); - self.update_channel.tell( - Subscribe { - actor: Box::new(cmd), - topic: "raw-commands".into(), - }, - None, - ); - } - } - - new_command!(ActivitiesCommand); - new_command!(CancelCommand); - new_command!(ChatidCommand); - new_command!(D1weekCommand); - new_command!(D2weekCommand); - new_command!(EditCommand); - new_command!(EditGuardianCommand); - new_command!(HelpCommand); - new_command!(JoinCommand); - new_command!(LfgCommand); - new_command!(ListCommand); - new_command!(ManageCommand); - new_command!(PsnCommand); - new_command!(UptimeCommand); - new_command!(WhoisCommand); - - // Create reminder tasks actor - let reminders = ctx - .actor_of_args::( - "reminders", - (ctx.myself(), self.lfg_chat_id, self.connection_pool.clone()), - ) - .unwrap(); - // Schedule first run, the actor handler will reschedule. - reminders.tell(ScheduleNextMinute, None); - reminders.tell(ScheduleNextDay, None); - reminders.tell(ScheduleNextWeek, None); - } - - fn recv(&mut self, ctx: &Context, msg: Self::Msg, sender: Sender) { - self.receive(ctx, msg, sender); - } -} - -impl ActorFactoryArgs<(String, Bot, ChannelRef, i64)> for BotActor { - fn create_args( - (bot_name, bot, chan, lfg_chat): (String, Bot, ChannelRef, i64), - ) -> Self { - Self::new(&bot_name, bot, chan, lfg_chat) - } -} - -#[derive(Clone, Debug)] -pub enum Format { - Plain, - Markdown, - Html, -} - -#[derive(Clone, Debug)] -pub enum Notify { - Off, - On, -} - -#[derive(Clone, Debug)] -pub struct SendMessage(pub String, pub ChatId, pub Format, pub Notify); - -#[derive(Clone, Debug)] -pub struct SendMessageReply(pub String, pub ActorUpdateMessage, pub Format, pub Notify); - -#[derive(Clone, Debug)] -pub struct ListCommands(pub ActorUpdateMessage); - -impl Receive for BotActor { - type Msg = BotActorMsg; - - fn receive(&mut self, _ctx: &Context, msg: SendMessage, _sender: Sender) { - log::debug!("SendMessage: {}", &msg.0); - let resp = self - .bot - .send_message(msg.1, msg.0) - .disable_notification(match msg.3 { - Notify::On => false, - Notify::Off => true, - }) - .disable_web_page_preview(true); - - let resp = match msg.2 { - Format::Html => resp.parse_mode(ParseMode::Html), - Format::Markdown => resp.parse_mode(ParseMode::MarkdownV2), - Format::Plain => resp, - }; - - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - - rt.block_on(resp.send()).unwrap(); - } -} - -impl Receive for BotActor { - type Msg = BotActorMsg; - - fn receive(&mut self, _ctx: &Context, msg: SendMessageReply, _sender: Sender) { - log::debug!("SendMessageReply: {}", &msg.0); - let message = msg.1; - - let fut = self - .bot - .send_message(message.update.chat_id(), msg.0) - .reply_to_message_id(message.update.id) - .disable_notification(match msg.3 { - Notify::On => false, - Notify::Off => true, - }) - .disable_web_page_preview(true); - - let fut = match msg.2 { - Format::Html => fut.parse_mode(ParseMode::Html), - Format::Markdown => fut.parse_mode(ParseMode::MarkdownV2), - Format::Plain => fut, - }; - - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - - rt.block_on(fut.send()).unwrap(); - } -} - -impl Receive for BotActor { - type Msg = BotActorMsg; - - fn receive(&mut self, ctx: &Context, msg: ListCommands, _sender: Sender) { - log::debug!("ListCommands"); - let message = msg.0; - - let mut sorted_cmds = self.list_commands(); - sorted_cmds.sort_by_key(|v| v.0.clone()); - let reply = sorted_cmds.into_iter().fold( - "Help ๐Ÿš‘\nThese are the registered commands for this Bot:\n\n".into(), - |acc, pair| format!("{}{} โ€” {}\n\n", acc, pair.0, pair.1), - ); - - ctx.myself.tell( - SendMessageReply(reply, message, Format::Html, Notify::Off), - None, - ); - } -} diff --git a/src/commands/activities_command.rs b/src/commands/activities_command.rs deleted file mode 100644 index 36c8eeba..00000000 --- a/src/commands/activities_command.rs +++ /dev/null @@ -1,491 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::{admin_check, match_command}, - models::{Activity, ActivityShortcut, NewActivity, NewActivityShortcut}, - BotCommand, - }, - diesel::{self, prelude::*}, - diesel_derives_traits::{Model, NewModel}, - itertools::Itertools, - riker::actors::Tell, - std::collections::HashMap, -}; - -command_actor!(ActivitiesCommand, [ActorUpdateMessage]); - -impl ActivitiesCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), Format::Plain, Notify::Off), - None, - ); - } - - fn usage(&self, message: &ActorUpdateMessage) { - self.send_reply( - message, - "Activities command help: - -/activities - Lists all available activities shortcuts. - -Admin-only mode: - -/activities ids - Lists IDs of all activities. -/activities add KV - Create new activity from KV pairs (see below). -/activities edit ID KV - Modify activity with given ID by updating all given KVs. -/activities addsc ID shortcut - Add activity shortcut for activity ID. -/activities delete ID - Remove activity if it doesn't have any activities planned. - -KV pairs are space-separated pairs of key=value elements -String arguments may be in quotes, but this is optional. - -Supported KV pairs for add/edit commands: - -name=activity name (e.g. Crucible) -mode=activity mode (e.g. Iron Banner) -min_fireteam_size=n -max_fireteam_size=n -min_light=n -min_level=n ", - ); - } -} - -impl BotCommand for ActivitiesCommand { - fn prefix() -> &'static str { - "/activities" - } - - fn description() -> &'static str { - "List available activity shortcuts" - } -} - -// Need to find a way to partially implement the Actor trait here, esp to set up sub-command actors -// impl Actor for ActivitiesCommand { -// // Create subcommand actors somewhere here... -// -// fn pre_start(&mut self, ctx: &Context) { -// todo!() -// } -// -// fn post_start(&mut self, ctx: &Context) { -// todo!() -// } -// } - -impl Receive for ActivitiesCommand { - type Msg = ActivitiesCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), args) = - match_command(message.update.text(), Self::prefix(), &self.bot_name) - { - let connection = self.connection(); - - if args.is_none() { - use crate::schema::{ - activities::dsl::{activities, id}, - activityshortcuts::dsl::{activityshortcuts, game, name}, - }; - - // Just /activities - let games = activityshortcuts - .select(game) - .distinct() - .order(game.asc()) - .load::(&connection) - .expect("Failed to load activity shortcuts"); - - let mut text = "Activities: use a short name:\n".to_owned(); - - for game_name in games { - text += &format!("*** {0}:\n", game_name); - let shortcuts = activityshortcuts - .filter(game.eq(game_name)) - .order(name.asc()) - .load::(&connection) - .expect("TEMP loading @FIXME"); - for shortcut in shortcuts { - let link_name = activities - .filter(id.eq(shortcut.link)) - .first::(&connection) - .expect("Failed to load activity"); - - text += &format!( - "{name}\t{link}\n", - name = shortcut.name, - link = link_name.format_name(), - ); - } - text += "\n"; - } - - return self.bot_ref.tell( - SendMessageReply(text, message, Format::Html, Notify::Off), - None, - ); - } - - // some args - pass to a subcommand - - let args = args.unwrap(); - let args: Vec<&str> = args.splitn(2, ' ').collect(); - - if args.is_empty() { - return self.usage(&message); - } - - let admin = admin_check(&self.bot_ref, &message, &connection); - if admin.is_none() { - return self.send_reply(&message, "You are not admin"); - } - - // split into subcommands: - match args[0] { - "ids" => { - use crate::schema::activities::dsl::{activities, id, mode, name}; - - let games = activities - .select((id, name, mode)) - .order(id.asc()) - .load::<(i32, String, Option)>(&connection) - .expect("Failed to load activities"); - - let mut text = "Activities:\n\n".to_string(); - for (id_, name_, mode_) in games { - text += &format!("{}. {} {}\n", id_, name_, mode_.unwrap_or("".into())); - } - self.send_reply(&message, text); - } - "add" => { - if args.len() < 2 { - self.send_reply(&message, "Syntax: /activities add KV"); - return self.usage(&message); - } - - let argmap = parse_kv_args(args[1]); - if argmap.is_none() { - return self - .send_reply(&message, "Invalid activity specification, see help."); - } - let mut argmap = argmap.unwrap(); - let name = argmap.remove("name"); - if name.is_none() { - return self.send_reply(&message, "Must specify activity name, see help."); - } - - let min_fireteam_size = argmap.remove("min_fireteam_size"); - if min_fireteam_size.is_none() { - return self - .send_reply(&message, "Must specify min_fireteam_size, see help."); - } - let min_fireteam_size = min_fireteam_size.unwrap().parse::(); - if min_fireteam_size.is_err() { - return self.send_reply(&message, "min_fireteam_size must be a number"); - } - let min_fireteam_size = min_fireteam_size.unwrap(); - - let max_fireteam_size = argmap.remove("max_fireteam_size"); - if max_fireteam_size.is_none() { - return self - .send_reply(&message, "Must specify max_fireteam_size, see help."); - } - let max_fireteam_size = max_fireteam_size.unwrap().parse::(); - if max_fireteam_size.is_err() { - return self.send_reply(&message, "max_fireteam_size must be a number"); - } - let max_fireteam_size = max_fireteam_size.unwrap(); - - // check no duplicates -- ? - let mut act = NewActivity { - name: name.unwrap().into(), - mode: None, - min_fireteam_size, - max_fireteam_size, - min_level: None, - min_light: None, - }; - - for (key, val) in argmap { - match key { - "min_light" => { - let val = val.parse::(); - if val.is_err() { - return self.send_reply(&message, "min_light must be a number"); - } - act.min_light = Some(val.unwrap()) - } - "min_level" => { - let val = val.parse::(); - if val.is_err() { - return self.send_reply(&message, "min_level must be a number"); - } - act.min_level = Some(val.unwrap()) - } - "mode" => act.mode = Some(val.into()), - _ => { - return self - .send_reply(&message, format!("Unknown field name {}", key)); - } - } - } - - match act.save(&connection) { - Ok(act) => self - .send_reply(&message, format!("Activity {} added.", act.format_name())), - Err(e) => { - self.send_reply(&message, format!("Error creating activity. {:?}", e)) - } - } - } - "addsc" => { - if args.len() < 2 { - return self.send_reply( - &message, - "Syntax: /activities addsc ActivityID ShortcutName Game name", - ); - } - - let args: Vec<&str> = args[1].splitn(3, ' ').collect(); - if args.len() != 3 { - return self.send_reply( - &message, - "To add a shortcut specify activity ID, shortcut name and then the game name", - ); - } - - let link = args[0].parse::(); - if link.is_err() { - return self.send_reply(&message, "ActivityID must be a number"); - } - let link = link.unwrap(); - let name = args[1].to_string(); - let game = args[2].to_string(); - - let act = Activity::find_one(&connection, &link).expect("Failed to run SQL"); - - if act.is_none() { - return self - .send_reply(&message, format!("Activity {} was not found.", link)); - } - - let shortcut = NewActivityShortcut { name, game, link }; - - if shortcut.save(&connection).is_err() { - return self.send_reply(&message, "Error creating shortcut"); - } - - self.send_reply(&message, "Shortcut added"); - } - "edit" => { - if args.len() < 2 { - self.send_reply(&message, "Syntax: /activities edit ID KV"); - return self.usage(&message); - } - - let args: Vec<&str> = args[1].splitn(2, ' ').collect(); - if args.len() != 2 { - return self.send_reply( - &message, - "To edit first specify Activity ID and then key=value pairs", - ); - } - - let id = args[0].parse::(); - if id.is_err() { - return self.send_reply(&message, "ActivityID must be a number"); - } - let id = id.unwrap(); - - let act = Activity::find_one(&connection, &id).expect("Failed to run SQL"); - - if act.is_none() { - return self - .send_reply(&message, format!("Activity {} was not found.", id)); - } - let mut act = act.unwrap(); - - let argmap = parse_kv_args(args[1]); - if argmap.is_none() { - return self - .send_reply(&message, "Invalid activity specification, see help."); - } - let argmap = argmap.unwrap(); - - for (key, val) in argmap { - match key { - "name" => act.name = val.into(), - "min_fireteam_size" => { - let val = val.parse::(); - if val.is_err() { - return self.send_reply( - &message, - "min_fireteam_size must be a number", - ); - } - act.min_fireteam_size = val.unwrap() - } - "max_fireteam_size" => { - let val = val.parse::(); - if val.is_err() { - return self.send_reply( - &message, - "max_fireteam_size must be a number", - ); - } - act.max_fireteam_size = val.unwrap() - } - "min_light" => { - let val = val.parse::(); - if val.is_err() { - return self.send_reply(&message, "min_light must be a number"); - } - act.min_light = Some(val.unwrap()) - } - "min_level" => { - let val = val.parse::(); - if val.is_err() { - return self.send_reply(&message, "min_level must be a number"); - } - act.min_level = Some(val.unwrap()) - } - "mode" => act.mode = Some(val.into()), - _ => { - return self - .send_reply(&message, format!("Unknown field name {}", key)); - } - } - } - - match act.save(&connection) { - Ok(act) => self.send_reply( - &message, - format!("Activity {} updated.", act.format_name()), - ), - Err(e) => { - self.send_reply(&message, format!("Error updating activity. {:?}", e)) - } - } - } - "delete" => { - if args.len() < 2 { - self.send_reply(&message, "Syntax: /activities delete ID"); - return self.usage(&message); - } - - let id = args[1].parse::(); - if id.is_err() { - return self.send_reply(&message, "ActivityID must be a number"); - } - let id = id.unwrap(); - - let act = Activity::find_one(&connection, &id).expect("Failed to run SQL"); - - if act.is_none() { - return self - .send_reply(&message, format!("Activity {} was not found.", id)); - } - - let act = act.unwrap(); - - let name = act.format_name(); - - match act.destroy(&connection) { - Ok(_) => self.send_reply(&message, format!("Activity {} deleted.", name)), - Err(e) => { - self.send_reply(&message, format!("Error deleting activity. {:?}", e)) - } - } - } - _ => { - self.send_reply(&message, "Unknown activities operation"); - self.usage(&message); - } - } - } - } -} - -fn parse_kv_args(args: &str) -> Option> { - fn final_collect(args: Vec<&str>) -> HashMap<&str, &str> { - args.into_iter() - .tuples() - .map(|(k, v)| (k, v.trim_matches('"'))) - .collect::>() - } - - let fragments: Vec<&str> = args.split('=').collect(); - - log::trace!("{:?}", fragments); - - match fragments.len() { - x if x < 2 => None, - 2 => // only single parameter - Some(final_collect(fragments)), - _ => { - // ['max_fireteam_size', '1', 'name', '6', 'mode', '"Last Wish, Enhance"'] - let subfrags = fragments[1..fragments.len() - 1] - .iter() - .flat_map(|x: &&str| { - x.rsplitn(2, ' ') - .collect::>() - .into_iter() - .rev() - .collect::>() - }) - .collect::>(); - - log::trace!("{:?}", subfrags); - - let mut final_ = vec![fragments[0]]; - final_.extend(subfrags); - final_.extend(vec![fragments[fragments.len() - 1]]); - - log::trace!("Final {:?}", final_); - - let the_map = final_collect(final_); - - log::trace!(".. as map {:?}", the_map); - - Some(the_map) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_split_algorithm() { - // min_fireteam_size=1 max_fireteam_size=6 name="Last Wish, Enhanced" mode="prestige" - let args = - r#"min_fireteam_size=1 max_fireteam_size=6 name="Last Wish, Enhanced" mode="prestige""#; - let result = parse_kv_args(args); - assert!(result.is_some()); - let result = result.unwrap(); - assert_eq!(result.len(), 4); - - let args = r#"name="Last Wish, Enhanced""#; - let result = parse_kv_args(args); - assert!(result.is_some()); - let mut result = result.unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result.remove("name"), Some("Last Wish, Enhanced")); - - let args = r#"whatever else"#; - let result = parse_kv_args(args); - assert!(result.is_none()); - } -} diff --git a/src/commands/cancel_command.rs b/src/commands/cancel_command.rs deleted file mode 100644 index 1cb60d37..00000000 --- a/src/commands/cancel_command.rs +++ /dev/null @@ -1,123 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::{decapitalize, match_command, validate_username}, - datetime::{format_start_time, reference_date}, - models::PlannedActivity, - BotCommand, - }, - chrono::Duration, - diesel_derives_traits::Model, - riker::actors::Tell, -}; - -command_actor!(CancelCommand, [ActorUpdateMessage]); - -impl CancelCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), Format::Plain, Notify::Off), - None, - ); - } - - fn usage(&self, message: &ActorUpdateMessage) { - self.send_reply( - message, - "To leave a fireteam provide fireteam id -Fireteam IDs are available from output of /list command.", - ); - } -} - -impl BotCommand for CancelCommand { - fn prefix() -> &'static str { - "/cancel" - } - - fn description() -> &'static str { - "Leave joined activity" - } -} - -impl Receive for CancelCommand { - type Msg = CancelCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), activity_id) = - match_command(message.update.text(), Self::prefix(), &self.bot_name) - { - if activity_id.is_none() { - return self.usage(&message); - } - - let activity_id = activity_id.unwrap().parse::(); - if activity_id.is_err() { - return self.usage(&message); - } - - let activity_id = activity_id.unwrap(); - let connection = self.connection(); - - if let Some(guardian) = validate_username(&self.bot_ref, &message, &connection) { - let planned = PlannedActivity::find_one(&connection, &activity_id) - .expect("Failed to run SQL"); - - if planned.is_none() { - return self - .send_reply(&message, format!("Activity {} was not found.", activity_id)); - } - - let planned = planned.unwrap(); - - let member = planned.find_member(&connection, Some(&guardian)); - - if member.is_none() { - return self.send_reply(&message, "You are not part of this group."); - } - - if planned.start < reference_date() - Duration::hours(1) { - return self.send_reply(&message, "You can not leave past activities."); - } - - let member = member.unwrap(); - - if member.destroy(&connection).is_err() { - return self.send_reply(&message, "Failed to remove group member"); - } - - let act_name = planned.activity(&connection).format_name(); - let act_time = decapitalize(&format_start_time(planned.start, reference_date())); - - let suffix = if planned.members(&connection).is_empty() { - if planned.destroy(&connection).is_err() { - return self.send_reply(&message, "Failed to remove planned activity"); - } - "This fireteam is disbanded and can no longer be joined.".into() - } else { - format!( - "{} are going -{}", - planned.members_formatted_list(&connection), - planned.join_prompt(&connection) - ) - }; - - self.send_reply( - &message, - format!( - "{guarName} has left {actName} group {actTime} -{suffix}", - guarName = guardian.format_name(), - actName = act_name, - actTime = act_time, - suffix = suffix - ), - ); - } - } - } -} diff --git a/src/commands/chatid_command.rs b/src/commands/chatid_command.rs deleted file mode 100644 index 389106cc..00000000 --- a/src/commands/chatid_command.rs +++ /dev/null @@ -1,38 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::match_command, - BotCommand, - }, - riker::actors::Tell, -}; - -command_actor!(ChatidCommand, [ActorUpdateMessage]); - -impl BotCommand for ChatidCommand { - fn prefix() -> &'static str { - "/chatid" - } - - fn description() -> &'static str { - "Figure out the numeric chat ID" - } -} - -impl Receive for ChatidCommand { - type Msg = ChatidCommandMsg; - - fn receive(&mut self, _ctx: &Context, msg: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), _) = match_command(msg.update.text(), Self::prefix(), &self.bot_name) { - self.bot_ref.tell( - SendMessageReply( - format!("ChatId: {}", msg.update.chat_id()), - msg, - Format::Plain, - Notify::Off, - ), - None, - ); - } - } -} diff --git a/src/commands/d2week_command.rs b/src/commands/d2week_command.rs deleted file mode 100644 index 1364f80d..00000000 --- a/src/commands/d2week_command.rs +++ /dev/null @@ -1,34 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::match_command, - services::this_week_in_d2, - BotCommand, - }, - riker::actors::Tell, -}; - -command_actor!(D2weekCommand, [ActorUpdateMessage]); - -impl BotCommand for D2weekCommand { - fn prefix() -> &'static str { - "/d2week" - } - - fn description() -> &'static str { - "Show current Destiny 2 week" - } -} - -impl Receive for D2weekCommand { - type Msg = D2weekCommandMsg; - - fn receive(&mut self, _ctx: &Context, msg: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), _) = match_command(msg.update.text(), Self::prefix(), &self.bot_name) { - self.bot_ref.tell( - SendMessageReply(this_week_in_d2(), msg, Format::Markdown, Notify::Off), - None, - ); - } - } -} diff --git a/src/commands/dweek_command.rs b/src/commands/dweek_command.rs deleted file mode 100644 index a9217e90..00000000 --- a/src/commands/dweek_command.rs +++ /dev/null @@ -1,34 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::match_command, - services::this_week_in_d1, - BotCommand, - }, - riker::actors::Tell, -}; - -command_actor!(D1weekCommand, [ActorUpdateMessage]); - -impl BotCommand for D1weekCommand { - fn prefix() -> &'static str { - "/dweek" - } - - fn description() -> &'static str { - "Show current Destiny 1 week" - } -} - -impl Receive for D1weekCommand { - type Msg = D1weekCommandMsg; - - fn receive(&mut self, _ctx: &Context, msg: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), _) = match_command(msg.update.text(), Self::prefix(), &self.bot_name) { - self.bot_ref.tell( - SendMessageReply(this_week_in_d1(), msg, Format::Markdown, Notify::Off), - None, - ); - } - } -} diff --git a/src/commands/edit_command.rs b/src/commands/edit_command.rs deleted file mode 100644 index 03c99e70..00000000 --- a/src/commands/edit_command.rs +++ /dev/null @@ -1,183 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::{match_command, validate_username}, - datetime::reference_date, - models::{ActivityShortcut, PlannedActivity}, - BotCommand, - }, - chrono::{prelude::*, Duration}, - chrono_english::{parse_date_string, Dialect}, - chrono_tz::Europe::Moscow, - diesel_derives_traits::Model, - riker::actors::Tell, -}; - -command_actor!(EditCommand, [ActorUpdateMessage]); - -impl EditCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), Format::Plain, Notify::Off), - None, - ); - } - - fn usage(&self, message: &ActorUpdateMessage) { - self.send_reply( - message, - "Usage: - -ActivityIDs are available from output of /list command. - -/edit ActivityID time - Change activity time to a new one. Accepted time spec - is the same as in /lfg command. - -/edit ActivityID details - Replaces old /details command. - To update activity details enter text, - to delete details use `delete` instead of text. - -/edit ActivityID activity - Change type of activity, list of shortcuts - is available from output of /activities command", - ); - } -} - -impl BotCommand for EditCommand { - fn prefix() -> &'static str { - "/edit" - } - - fn description() -> &'static str { - "Edit existing activity" - } -} - -impl Receive for EditCommand { - type Msg = EditCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), args) = - match_command(message.update.text(), Self::prefix(), &self.bot_name) - { - let connection = self.connection(); - - if args.is_none() { - return self.usage(&message); - } - let args = args.unwrap(); - - let args: Vec<_> = args.splitn(3, ' ').collect(); - if args.len() != 3 { - return self.usage(&message); - } - - if validate_username(&self.bot_ref, &message, &connection).is_some() { - let id = args[0].parse::(); - if id.is_err() { - return self.send_reply(&message, "ActivityID must be a number"); - } - let id = id.unwrap(); - - let planned = - PlannedActivity::find_one(&connection, &id).expect("Failed to run SQL"); - - if planned.is_none() { - return self.send_reply(&message, format!("Activity {} was not found.", id)); - } - let mut planned = planned.unwrap(); - - if planned.start < reference_date() - Duration::hours(1) { - return self.send_reply(&message, "You can not edit past activities."); - } - - match args[1] { - "time" => { - let timespec = args[2]; - let start_time = parse_date_string( - timespec, - Local::now().with_timezone(&Moscow), - Dialect::Uk, - ); - // @todo Honor TELEGRAM_BOT_TIMEZONE envvar - - if start_time.is_err() { - return self.send_reply( - &message, - format!("Failed to parse time {}", timespec), - ); - } - - // ...then convert back to UTC. - let start_time = start_time.unwrap().with_timezone(&Utc); - - log::info!("...parsed `{:?}`", start_time); - - if planned.start < reference_date() - Duration::hours(1) { - return self.send_reply( - &message, - "You can not set activity time in the past.", - ); - } - - planned.start = start_time; - - if planned.save(&connection).is_err() { - return self.send_reply(&message, "Failed to update start time."); - } - - self.send_reply(&message, "Start time updated."); - } - "details" => { - let description = args[2]; - planned.details = if description == "delete" { - Some(String::new()) - } else { - Some(description.to_string()) - }; - if planned.save(&connection).is_err() { - return self.send_reply(&message, "Failed to update details."); - } - - self.send_reply(&message, "Details updated."); - } - "activity" => { - let activity = args[2]; - - let act = ActivityShortcut::find_one_by_name(&connection, activity) - .expect("Failed to load Activity shortcut"); - - if act.is_none() { - self.send_reply( - &message, - format!( - "Activity {} was not found. Use /activities for a list.", - activity - ), - ); - } - - let act = act.unwrap(); - - planned.activity_id = act.link; - - if planned.save(&connection).is_err() { - return self.send_reply(&message, "Failed to update activity."); - } - - self.send_reply(&message, "Activity updated."); - } - x => { - self.send_reply(&message, format!("Unknown attribute {}", x)); - } - } - } - } - } -} diff --git a/src/commands/editguar_command.rs b/src/commands/editguar_command.rs deleted file mode 100644 index 257bf1df..00000000 --- a/src/commands/editguar_command.rs +++ /dev/null @@ -1,158 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::{admin_check, guardian_lookup, match_command, validate_username}, - render_template, BotCommand, - }, - riker::actors::Tell, -}; - -command_actor!(EditGuardianCommand, [ActorUpdateMessage]); - -impl EditGuardianCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), Format::Plain, Notify::Off), - None, - ); - } - - fn usage(&self, message: &ActorUpdateMessage) { - self.send_reply( - message, - render_template!("editguar/usage").expect("Failed to render editguar usage template"), - ); - } -} - -impl BotCommand for EditGuardianCommand { - fn prefix() -> &'static str { - "/editguar" - } - - fn description() -> &'static str { - "Edit information about registered guardians" - } -} - -impl Receive for EditGuardianCommand { - type Msg = EditGuardianCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), args) = - match_command(message.update.text(), Self::prefix(), &self.bot_name) - { - let connection = self.connection(); - - if args.is_none() { - return self.usage(&message); - } - - // Split args in two or three: - // guardian id, - // subcommand, - // and optionally, parameters - let args = args.unwrap(); - let args: Vec<&str> = args.splitn(3, ' ').collect(); - - if args.is_empty() || args.len() == 2 { - return self.usage(&message); - } - - let name = args[0]; - - let guardian = if name == "my" { - let guardian = validate_username(&self.bot_ref, &message, &connection); - if guardian.is_none() { - return; - } - guardian.unwrap() - } else { - let admin = admin_check(&self.bot_ref, &message, &connection); - - if admin.is_none() { - return self.send_reply(&message, "You are not admin"); - } - - let guardian = guardian_lookup(name, &connection); - let guardian = match guardian { - Ok(Some(guardian)) => Some(guardian), - Ok(None) => { - self.send_reply(&message, format!("Guardian {} was not found.", &name)); - None - } - Err(_) => { - self.send_reply(&message, "Error querying guardian by name."); - None - } - }; - if guardian.is_none() { - return; - } - guardian.unwrap() - }; - - if args.len() == 1 { - let info = format!( - "{clan}{name} {email} {admin}", - clan = guardian - .psn_clan - .clone() - .map(|s| format!("[{}] ", s)) - .unwrap_or("".into()), - name = guardian.format_name(), - email = guardian.email.clone().unwrap_or("".into()), - admin = if guardian.is_superadmin { - "" - } else if guardian.is_admin { - "" - } else { - "" - }, - ); - return self.send_reply(&message, info); - } - - let command = args[1]; - let value = args[2]; - - let mut guardian = guardian; - - use diesel_derives_traits::Model; - - match command { - "psn" => { - guardian.psn_name = value.into(); - guardian.save(&connection).expect("Failed to update PSN"); - self.send_reply(&message, "Updated guardian PSN"); - } - "clan" => { - let value = if value == "delete" { - None - } else { - Some(value.into()) - }; - guardian.psn_clan = value; - guardian.save(&connection).expect("Failed to update clan"); - self.send_reply(&message, "Updated guardian clan"); - } - "email" => { - let value = if value == "delete" { - None - } else { - Some(value.into()) - }; - guardian.email = value; - guardian.save(&connection).expect("Failed to update email"); - self.send_reply(&message, "Updated guardian email"); - } - _ => { - self.send_reply(&message, "Unknown information field"); - } - } - } - } -} diff --git a/src/commands/help_command.rs b/src/commands/help_command.rs deleted file mode 100644 index 7043f8dc..00000000 --- a/src/commands/help_command.rs +++ /dev/null @@ -1,30 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, ListCommands}, - commands::match_command, - BotCommand, - }, - riker::actors::Tell, -}; - -command_actor!(HelpCommand, [ActorUpdateMessage]); - -impl BotCommand for HelpCommand { - fn prefix() -> &'static str { - "/help" - } - - fn description() -> &'static str { - "List available commands" - } -} - -impl Receive for HelpCommand { - type Msg = HelpCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), _) = match_command(message.update.text(), Self::prefix(), &self.bot_name) { - self.bot_ref.tell(ListCommands(message), None); - } - } -} diff --git a/src/commands/join_command.rs b/src/commands/join_command.rs deleted file mode 100644 index 041ee6bf..00000000 --- a/src/commands/join_command.rs +++ /dev/null @@ -1,121 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::{decapitalize, match_command, validate_username}, - datetime::{format_start_time, reference_date}, - models::{NewPlannedActivityMember, PlannedActivity}, - render_template, BotCommand, - }, - chrono::Duration, - diesel_derives_traits::{Model, NewModel}, - riker::actors::Tell, -}; - -command_actor!(JoinCommand, [ActorUpdateMessage]); - -impl JoinCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), Format::Plain, Notify::Off), - None, - ); - } - - fn usage(&self, message: &ActorUpdateMessage) { - self.send_reply( - message, - render_template!("join/usage").expect("Failed to render join usage template"), - ); - } -} - -impl BotCommand for JoinCommand { - fn prefix() -> &'static str { - "/join" - } - - fn description() -> &'static str { - "Join existing activity from the list" - } -} - -impl Receive for JoinCommand { - type Msg = JoinCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), activity_id) = - match_command(message.update.text(), Self::prefix(), &self.bot_name) - { - if activity_id.is_none() { - return self.usage(&message); - } - - let activity_id = activity_id.unwrap().parse::(); - if activity_id.is_err() { - return self.usage(&message); - } - - let activity_id = activity_id.unwrap(); - let connection = self.connection(); - - if let Some(guardian) = validate_username(&self.bot_ref, &message, &connection) { - let planned = PlannedActivity::find_one(&connection, &activity_id) - .expect("Failed to run SQL"); - - if planned.is_none() { - return self - .send_reply(&message, format!("Activity {} was not found.", activity_id)); - } - - let planned = planned.unwrap(); - - let member = planned.find_member(&connection, Some(&guardian)); - - if member.is_some() { - return self.send_reply(&message, "You are already part of this group."); - } - - if planned.is_full(&connection) { - return self.send_reply(&message, "This activity group is full."); - } - - if planned.start < reference_date() - Duration::hours(1) { - return self.send_reply(&message, "You can not join past activities."); - } - - let planned_activity_member = NewPlannedActivityMember { - user_id: guardian.id, - planned_activity_id: planned.id, - added: reference_date(), - }; - - planned_activity_member - .save(&connection) - .expect("Unexpected error saving group joiner"); - - // join/joined template - - let guar_name = guardian.to_string(); - let act_name = planned.activity(&connection).format_name(); - let act_time = decapitalize(&format_start_time(planned.start, reference_date())); - let other_guars = planned.members_formatted_list(&connection); - let join_prompt = planned.join_prompt(&connection); - - let text = render_template!( - "join/joined", - ("guarName", &guar_name), - ("actName", &act_name), - ("actTime", &act_time), - ("otherGuars", &other_guars), - ("joinPrompt", &join_prompt) - ) - .expect("Failed to render join joined template"); - - self.send_reply(&message, text); - } - } - } -} diff --git a/src/commands/lfg_command.rs b/src/commands/lfg_command.rs deleted file mode 100644 index 4a178997..00000000 --- a/src/commands/lfg_command.rs +++ /dev/null @@ -1,164 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::{match_command, validate_username}, - datetime::{format_start_time, reference_date}, - models::{Activity, ActivityShortcut, NewPlannedActivity, NewPlannedActivityMember}, - BotCommand, - }, - chrono::prelude::*, - chrono_english::{parse_date_string, Dialect}, - chrono_tz::Europe::Moscow, - diesel::{self, prelude::*}, - diesel_derives_traits::{Model, NewModel}, - riker::actors::Tell, -}; - -command_actor!(LfgCommand, [ActorUpdateMessage]); - -impl LfgCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S, format: Format) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), format, Notify::Off), - None, - ); - } - - fn usage(&self, message: &ActorUpdateMessage) { - self.send_reply( - message, - "LFG usage: /lfg activity YYYY-MM-DD HH:MM -For a list of activity codes: /activities -Example: /lfg kf 2018-09-10 23:00 -Times are in Moscow (MSK) timezone.", - Format::Html, - ); - } -} - -impl BotCommand for LfgCommand { - fn prefix() -> &'static str { - "/lfg" - } - - fn description() -> &'static str { - "Create a new Looking For Group event" - } -} - -impl Receive for LfgCommand { - type Msg = LfgCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), args) = - match_command(message.update.text(), Self::prefix(), &self.bot_name) - { - log::info!("args are {:?}", args); - - if args.is_none() { - return self.usage(&message); - } - - // Split args in two: - // activity spec, - // and timespec - let args = args.unwrap(); - let args: Vec<&str> = args.splitn(2, ' ').collect(); - - if args.len() < 2 { - return self.usage(&message); - } - - let activity = args[0]; - let timespec = args[1]; - let connection = self.connection(); - - log::info!("Adding activity `{}` at `{}`", &activity, ×pec); - - if let Some(guardian) = validate_username(&self.bot_ref, &message, &connection) { - let act = ActivityShortcut::find_one_by_name(&connection, activity) - .expect("Failed to load Activity shortcut"); - - if act.is_none() { - return self.send_reply( - &message, - format!( - "Activity {} was not found. Use /activities to see the list.", - activity - ), - Format::Plain, - ); - } - // Parse input in MSK timezone... - let start_time = - parse_date_string(timespec, Local::now().with_timezone(&Moscow), Dialect::Uk); - // @todo Honor TELEGRAM_BOT_TIMEZONE envvar - - if start_time.is_err() { - return self.send_reply( - &message, - format!("Failed to parse time {}", timespec), - Format::Plain, - ); - } - - // ...then convert back to UTC. - let start_time = start_time.unwrap().with_timezone(&Utc); - - let act = act.unwrap(); - - log::info!("...parsed `{:?}`", start_time); - - let planned_activity = NewPlannedActivity { - author_id: guardian.id, - activity_id: act.link, - start: start_time, - }; - - use diesel::result::Error; - - connection - .transaction::<_, Error, _>(|| { - let planned_activity = planned_activity - .save(&connection) - .expect("Unexpected error saving LFG group"); - - let planned_activity_member = NewPlannedActivityMember { - user_id: guardian.id, - planned_activity_id: planned_activity.id, - added: reference_date(), - }; - - planned_activity_member - .save(&connection) - .expect("Unexpected error saving LFG group creator"); - - let activity = Activity::find_one(&connection, &act.link) - .expect("Couldn't find linked activity") - .unwrap(); - - self.send_reply( - &message, - format!( - "{guarName} is looking for {groupName} group {onTime} -{joinPrompt} -Enter `/edit{actId} details ` to specify more details about the event.", - guarName = guardian, - groupName = activity.format_name(), - onTime = format_start_time(start_time, reference_date()), - joinPrompt = planned_activity.join_prompt(&connection), - actId = planned_activity.id - ), - Format::Plain, - ); - - Ok(()) - }) - .expect("never happens, but please implement error handling"); - } - } - } -} diff --git a/src/commands/list_command.rs b/src/commands/list_command.rs deleted file mode 100644 index 895a05f2..00000000 --- a/src/commands/list_command.rs +++ /dev/null @@ -1,57 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::{match_command, validate_username}, - models::PlannedActivity, - render_template, BotCommand, - }, - riker::actors::Tell, -}; - -command_actor!(ListCommand, [ActorUpdateMessage]); - -impl ListCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S, format: Format) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), format, Notify::Off), - None, - ); - } -} - -impl BotCommand for ListCommand { - fn prefix() -> &'static str { - "/list" - } - - fn description() -> &'static str { - "List current events" - } -} - -impl Receive for ListCommand { - type Msg = ListCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), _) = match_command(message.update.text(), Self::prefix(), &self.bot_name) { - let connection = self.connection(); - - if let Some(guardian) = validate_username(&self.bot_ref, &message, &connection) { - // let count = self.activity(connection).max_fireteam_size as usize - // - self.members_count(connection); - let upcoming_events: Vec<_> = PlannedActivity::upcoming_activities(&connection) - .iter() - .map(|s| s.to_template(Some(&guardian), &connection)) - .collect(); - - let output = render_template!("list/planned", ("events", &upcoming_events)) - .expect("Rendering failed"); - - self.send_reply(&message, output, Format::Html); - } - } - } -} diff --git a/src/commands/manage_command.rs b/src/commands/manage_command.rs deleted file mode 100644 index c21144f4..00000000 --- a/src/commands/manage_command.rs +++ /dev/null @@ -1,281 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::{admin_check, guardian_lookup, match_command}, - BotCommand, - }, - riker::actors::Tell, -}; - -// #[derive(Clone)] -// struct ListAdminsSubcommand; -// -// #[derive(Clone)] -// struct AddAdminSubcommand; -// -// #[derive(Clone)] -// struct RemoveAdminSubcommand; - -command_actor!(ManageCommand, [ActorUpdateMessage]); - -impl ManageCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), Format::Plain, Notify::Off), - None, - ); - } - - fn usage(&self, message: &ActorUpdateMessage) { - self.send_reply( - message, - "Manage admins: -/manage list-admins - List existing admins -/manage add-admin - Add existing guardian as an admin -/manage remove-admin - Remove admin rights from guardian", - ); - } -} - -impl BotCommand for ManageCommand { - fn prefix() -> &'static str { - "/manage" - } - - fn description() -> &'static str { - "Manage bot users (admin-only)" - } -} - -impl Receive for ManageCommand { - type Msg = ManageCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), args) = - match_command(message.update.text(), Self::prefix(), &self.bot_name) - { - let connection = self.connection(); - let admin = admin_check(&self.bot_ref, &message, &connection); - - if admin.is_none() { - return self.send_reply(&message, "You are not admin"); - } - - // let _admin = admin.unwrap(); - - if args.is_none() { - return self.usage(&message); - } - - // Split args in two: - // subcommand, - // and optional guardian id - let args = args.unwrap(); - let args: Vec<&str> = args.splitn(2, ' ').collect(); - - if args.is_empty() { - return self.usage(&message); - } - - let subcommand = args[0]; - let args = if args.len() > 1 { - Some(args[1].to_string()) - } else { - None - }; - - log::info!("{:?}", args); - - match subcommand { - "list-admins" => self.list_admins_subcommand(&message), - "add-admin" => self.add_admin_subcommand(&message, args), - "remove-admin" => self.remove_admin_subcommand(&message, args), - &_ => { - self.send_reply(&message, "Unknown management command"); - } - } - - // Need to invent some sort of match string format for matching subcommands - // Some are `/command subcommand [args]`, some are `/command arg subcommand args` etc. - // Can encode this string in prefix() for subcommands and make them match, maybe even directly? - // i.e. add subcommands together with master command to the general list of commands (need to sort properly too) - // if match_subcommand(message, ListAdminsSubcommand) { - // return ListAdminsSubcommand::execute(); - // } else if match_subcommand(message, AddAdminSubcommand) { - // return AddAdminSubcommand::execute(); - // } - } - } -} - -// command_ctor!(ListAdminsSubcommand); -// -// impl BotCommand for ListAdminsSubcommand { -// fn prefix(&self) -> &'static str { -// "list-admins" -// } -// -// fn description(&self) -> &'static str { -// "List bot admins (admin-only)" -// } -impl ManageCommand { - fn list_admins_subcommand(&self, message: &ActorUpdateMessage) { - use crate::{models::Guardian, schema::guardians::dsl::*}; - use diesel::prelude::*; - - let connection = self.connection(); - - let admins = guardians - .filter(is_admin.eq(true)) - .order(telegram_name.asc()) - .load::(&connection) - .expect("Cannot execute SQL query"); - - if admins.is_empty() { - return self.send_reply(message, "No admins found"); - } - - let text = admins - .iter() - .fold("Existing admins:\n\n".to_owned(), |acc, admin| { - acc + &format!("{}\n", admin) - }); - - self.send_reply(message, text); - } -} - -// command_ctor!(AddAdminSubcommand); -// -// impl BotCommand for AddAdminSubcommand { -// fn prefix(&self) -> &'static str { -// "add-admin" -// } -// -// fn description(&self) -> &'static str { -// "Add bot admin (admin-only)" -// } -impl ManageCommand { - fn add_admin_subcommand(&self, message: &ActorUpdateMessage, args: Option) { - let connection = self.connection(); - let admin = admin_check(&self.bot_ref, message, &connection); - - if admin.is_none() { - return self.send_reply(message, "You are not admin"); - } - - let admin = admin.unwrap(); - - if !admin.is_superadmin { - return self.send_reply(message, "You are not superadmin"); - } - - if args.is_none() { - return self.send_reply(message, "Specify a guardian to promote to admin"); - } - - let name = args.unwrap(); - - let guardian = guardian_lookup(&name, &connection); - - match guardian { - Ok(Some(mut guardian)) => { - let tg_name = guardian.telegram_name.clone(); - - if guardian.is_admin { - return self.send_reply(message, format!("@{} is already an admin", &tg_name)); - } - - use diesel_derives_traits::Model; - - guardian.is_admin = true; - guardian - .save(&connection) // @todo handle DbError - .expect("Cannot execute SQL query"); - - self.send_reply(message, format!("@{} is now an admin!", &tg_name)); - } - Ok(None) => { - self.send_reply(message, format!("Guardian {} was not found.", &name)); - } - Err(_) => { - self.send_reply(message, "Error querying guardian name."); - } - } - } -} - -// command_ctor!(RemoveAdminSubcommand); -// -// impl BotCommand for RemoveAdminSubcommand { -// fn prefix(&self) -> &'static str { -// "remove-admin" -// } -// -// fn description(&self) -> &'static str { -// "Remove bot admin (admin-only)" -// } - -impl ManageCommand { - fn remove_admin_subcommand(&self, message: &ActorUpdateMessage, args: Option) { - let connection = self.connection(); - let admin = admin_check(&self.bot_ref, message, &connection); - - if admin.is_none() { - return self.send_reply(message, "You are not admin"); - } - - let admin = admin.unwrap(); - - if !admin.is_superadmin { - return self.send_reply(message, "You are not superadmin"); - } - - if args.is_none() { - return self.send_reply(message, "Specify a guardian to demote from admins"); - } - - let name = args.unwrap(); - - let guardian = guardian_lookup(&name, &connection); - - match guardian { - Ok(Some(mut guardian)) => { - let tg_name = guardian.telegram_name.clone(); - - if !guardian.is_admin { - return self - .send_reply(message, format!("@{} is already not an admin", &tg_name)); - } - - if guardian.is_superadmin { - return self.send_reply( - message, - format!("@{} is a superadmin, you can not demote.", &tg_name), - ); - } - - use diesel_derives_traits::Model; - - guardian.is_admin = false; - guardian - .save(&connection) // @todo handle DbError - .expect("Cannot execute SQL query"); - - self.send_reply(message, format!("@{} is not an admin anymore!", &tg_name)); - } - Ok(None) => { - self.send_reply(message, format!("Guardian {} was not found.", &name)); - } - Err(_) => { - self.send_reply(message, "Error querying guardian name."); - } - } - } -} diff --git a/src/commands/psn_command.rs b/src/commands/psn_command.rs deleted file mode 100644 index e232aa76..00000000 --- a/src/commands/psn_command.rs +++ /dev/null @@ -1,164 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::match_command, - models::{Guardian, NewGuardian}, - schema::guardians::dsl::*, - BotCommand, - }, - diesel::{self, prelude::*}, - riker::actors::Tell, -}; - -command_actor!(PsnCommand, [ActorUpdateMessage]); - -impl PsnCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S, format: Format) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), format, Notify::Off), - None, - ); - } -} - -impl BotCommand for PsnCommand { - fn prefix() -> &'static str { - "/psn" - } - - fn description() -> &'static str { - "Link your telegram user to PSN" - } -} - -impl Receive for PsnCommand { - type Msg = PsnCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), name) = - match_command(message.update.text(), Self::prefix(), &self.bot_name) - { - log::info!("PSN command"); - - if name.is_none() { - return self.send_reply( - &message, - "Usage: /psn psnid\nFor example: /psn KPOTA_B_ATEOHE", - Format::Html, - ); - } - - let name = name.unwrap(); - - let from = match message.update.from() { - None => { - return self.send_reply(&message, "Message has no sender info.", Format::Plain); - } - Some(from) => from, - }; - - let username = match from.username { - None => { - return self.send_reply( - &message, - "You have no telegram username, register your telegram account first.", - Format::Plain, - ); - } - Some(ref name) => name, - }; - - let connection = self.connection(); - let user_id = from.id; - - let db_user = guardians - .filter(telegram_id.eq(&user_id)) - .first::(&connection) - .optional(); - - match db_user { - Ok(Some(user)) => { - let another_user = guardians - .filter(psn_name.ilike(&name)) - .filter(telegram_id.ne(&user_id)) - .first::(&connection) - .optional(); - - match another_user { - Ok(Some(_)) => { - self.send_reply( - &message, - format!( - "The psn {psn} is already used by somebody else.", - psn = name - ), - Format::Plain, - ); - } - Ok(None) => { - use diesel_derives_traits::Model; - - let mut user = user; - user.telegram_name = username.to_string(); - user.psn_name = name.to_string(); - if user.save(&connection).is_err() { - self.send_reply( - &message, - "Failed to update telegram and PSN names.", - Format::Plain, - ); - } else { - self.send_reply( - &message, - format!( - "Your telegram @{username} is linked with PSN {psn}", - username = username, - psn = name - ), - Format::Plain, - ); - } - } - Err(_) => { - self.send_reply( - &message, - "Error querying guardian PSN.", - Format::Plain, - ); - } - } - } - Ok(None) => { - use crate::schema::guardians; - - let guardian = NewGuardian { - telegram_name: username, - telegram_id: user_id, - psn_name: &name, - }; - - diesel::insert_into(guardians::table) - .values(&guardian) - .execute(&connection) - .expect("Unexpected error saving guardian"); - - self.send_reply( - &message, - format!( - "Linking telegram @{username} with PSN {psn}", - username = username, - psn = name - ), - Format::Plain, - ); - } - Err(_) => { - self.send_reply(&message, "Error querying guardian name.", Format::Plain); - } - }; - } - } -} diff --git a/src/commands/uptime_command.rs b/src/commands/uptime_command.rs deleted file mode 100644 index 18dcbdcd..00000000 --- a/src/commands/uptime_command.rs +++ /dev/null @@ -1,61 +0,0 @@ -#[cfg(target_os = "linux")] -use procfs::process::Process; -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::match_command, - BotCommand, - }, - riker::actors::Tell, -}; - -command_actor!(UptimeCommand, [ActorUpdateMessage]); - -#[cfg(target_os = "linux")] -fn get_process_info() -> String { - if let Ok(process) = Process::myself() { - use thousands::Separable; - let stat = process.stat().unwrap(); - let page_size = procfs::page_size(); - format!( - "- ๐Ÿงต {thn} threads\n- ๐Ÿ“ƒ {vmb} bytes ({vmp} pages) virtual memory\n- ๐Ÿ“ƒ {rmb} bytes ({rmp} pages) resident memory", - thn = stat.num_threads, - vmb = stat.vsize.separate_with_commas(), - vmp = (stat.vsize / page_size).separate_with_commas(), - rmp = stat.rss.separate_with_commas(), - rmb = (stat.rss * page_size).separate_with_commas(), - ) - } else { - "- Couldn't access process information".to_string() - } -} - -#[cfg(not(target_os = "linux"))] -fn get_process_info() -> String { - "- Process info only available on Linux hosts.".to_string() -} - -impl BotCommand for UptimeCommand { - fn prefix() -> &'static str { - "/uptime" - } - - fn description() -> &'static str { - "Show bot uptime and statistics" - } -} - -impl Receive for UptimeCommand { - type Msg = UptimeCommandMsg; - - fn receive(&mut self, _ctx: &Context, msg: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), _) = match_command(msg.update.text(), Self::prefix(), &self.bot_name) { - let uptime = crate::datetime::format_uptime(); - let message = format!("- โฐ Started {uptime}\n{}", get_process_info()); - self.bot_ref.tell( - SendMessageReply(message, msg, Format::Plain, Notify::Off), - None, - ); - } - } -} diff --git a/src/commands/whois_command.rs b/src/commands/whois_command.rs deleted file mode 100644 index 1436de3f..00000000 --- a/src/commands/whois_command.rs +++ /dev/null @@ -1,77 +0,0 @@ -use { - crate::{ - bot_actor::{ActorUpdateMessage, Format, Notify, SendMessageReply}, - commands::{guardian_lookup, match_command, validate_username}, - BotCommand, - }, - riker::actors::Tell, -}; - -command_actor!(WhoisCommand, [ActorUpdateMessage]); - -impl WhoisCommand { - fn send_reply(&self, message: &ActorUpdateMessage, reply: S) - where - S: Into, - { - self.bot_ref.tell( - SendMessageReply(reply.into(), message.clone(), Format::Plain, Notify::Off), - None, - ); - } -} - -impl BotCommand for WhoisCommand { - fn prefix() -> &'static str { - "/whois" - } - - fn description() -> &'static str { - "Query telegram or PSN id" - } -} - -impl Receive for WhoisCommand { - type Msg = WhoisCommandMsg; - - fn receive(&mut self, _ctx: &Context, message: ActorUpdateMessage, _sender: Sender) { - if let (Some(_), name) = - match_command(message.update.text(), Self::prefix(), &self.bot_name) - { - if name.is_none() { - return self.send_reply( - &message, - "To query user provide his @TelegramId (starting with @) or PsnId", - ); - } - - let name = name.unwrap(); - let connection = self.connection(); - - if validate_username(&self.bot_ref, &message, &connection).is_none() { - return; // TODO: say something? - } - - let guardian = guardian_lookup(&name, &connection); - - match guardian { - Ok(Some(guardian)) => { - self.send_reply( - &message, - format!( - "Guardian @{telegram_name} PSN {psn_name}", - telegram_name = guardian.telegram_name, - psn_name = guardian.psn_name - ), - ); - } - Ok(None) => { - self.send_reply(&message, format!("Guardian {} was not found.", name)); - } - Err(_) => { - self.send_reply(&message, "Error querying guardian name."); - } - } - } - } -} diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index 6792d697..00000000 --- a/src/errors.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// Implement failure Fail for various types used in the bot -/// @todo use anyhow -use failure::Error; - -#[derive(Debug, Fail)] -enum BotError { - DbError(diesel::result::Error), -} diff --git a/src/models.rs b/src/models.rs deleted file mode 100644 index 52647056..00000000 --- a/src/models.rs +++ /dev/null @@ -1,624 +0,0 @@ -//================================================================================================= -// DB Models and Tera templates -//================================================================================================= - -use { - crate::{ - datetime::{format_start_time, reference_date}, - render_template, - schema::*, - DbConnection, - }, - chrono::{prelude::*, Duration}, - diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}, - diesel_derives_traits::Model, - serde::{Deserialize, Serialize}, - serde_json::Value, - std::{fmt, sync::LazyLock}, -}; - -//------------------------------------------------------------------------------------------------- -// ActivityShortcut -//------------------------------------------------------------------------------------------------- - -#[derive(Debug, Queryable, Identifiable, AsChangeset, Associations, Model)] -#[table_name = "activityshortcuts"] -#[belongs_to(Activity, foreign_key = "link")] -pub struct ActivityShortcut { - pub id: i32, - pub name: String, - pub game: String, - pub link: i32, -} - -#[derive(Clone, Insertable, NewModel)] -#[table_name = "activityshortcuts"] -#[model(ActivityShortcut)] -pub struct NewActivityShortcut { - pub name: String, - pub game: String, - pub link: i32, -} - -impl ActivityShortcut { - pub fn find_one_by_name( - connection: &DbConnection, - act_name: &str, - ) -> diesel::result::QueryResult> { - use crate::schema::activityshortcuts::dsl::*; - - ::table() - .filter(name.eq(act_name)) - .get_result::(connection) - .optional() - } -} - -//------------------------------------------------------------------------------------------------- -// Activity -//------------------------------------------------------------------------------------------------- - -#[derive(Debug, Queryable, Identifiable, AsChangeset, Model)] -#[table_name = "activities"] -pub struct Activity { - pub id: i32, - pub name: String, - pub mode: Option, - pub min_fireteam_size: i32, - pub max_fireteam_size: i32, - pub min_light: Option, - pub min_level: Option, -} - -#[derive(Clone, Insertable, NewModel)] -#[table_name = "activities"] -#[model(Activity)] -pub struct NewActivity { - pub name: String, - pub mode: Option, - pub min_fireteam_size: i32, - pub max_fireteam_size: i32, - pub min_light: Option, - pub min_level: Option, -} - -impl Activity { - pub fn format_name(&self) -> String { - format!("{} {}", self.name, self.mode.clone().unwrap_or_default()) - } -} - -//------------------------------------------------------------------------------------------------- -// Alert -//------------------------------------------------------------------------------------------------- - -#[derive(Debug, Queryable, Identifiable, AsChangeset, Model)] -pub struct Alert { - pub id: i32, - pub guid: String, - pub title: String, - pub kind: String, - #[column_name = "startdate"] - pub start_date: DateTime, - #[column_name = "expirydate"] - pub expiry_date: Option>, - pub faction: Option, - pub flavor: Option, -} - -#[derive(Clone, Insertable, NewModel)] -#[table_name = "alerts"] -#[model(Alert)] -pub struct NewAlert { - pub guid: String, - pub title: String, - pub kind: Option, - #[column_name = "startdate"] - pub start_date: Option>, - #[column_name = "expirydate"] - pub expiry_date: Option>, - pub faction: Option, - pub flavor: Option, -} - -impl fmt::Display for Alert { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{} {} {}", - self.type_icon(), - self.reward_icon(), - self.title - ) - } -} - -impl Alert { - pub fn is_important(&self) -> bool { - self.is_forma() - || self.is_nitain() - || self.is_orokin_reactor() - || (self.expiry_date.is_some() - && self.expiry_date.unwrap() - self.start_date >= Duration::minutes(90)) - } - - pub fn type_icon(&self) -> String { - match self.kind.as_str() { - "Alert" => "โœŠ".into(), - "Invasion" => "๐Ÿ›".into(), - "Outbreak" => "โ›“".into(), - _ => format!("โ‰๏ธ {}", self.kind), - } - } - - pub fn reward_icon(&self) -> String { - if self.is_forma() { - "โš–" - } else if self.is_nitain() { - "โœจ" - } else if self.is_orokin_reactor() { - "๐Ÿฎ" - } else if self.is_endo() { - "๐Ÿ”ฎ" - } else if self.is_blueprint() { - "๐Ÿ—ฟ" - } else if self.is_resource() { - "๐Ÿ”‹" - } else if self.is_mod() { - "โš™" - } else if self.is_aura() { - "โ„๏ธ" - } else if self.is_credits() { - "๐Ÿ’ฐ" - } else { - "" - } - .into() - } - - pub fn is_blueprint(&self) -> bool { - self.title.contains("(Blueprint)") - } - - pub fn is_resource(&self) -> bool { - self.title.contains("(Resource)") - } - - pub fn is_mod(&self) -> bool { - self.title.contains("(Mod)") - } - - pub fn is_aura(&self) -> bool { - self.title.contains("(Aura)") - } - - pub fn is_credits(&self) -> bool { - static CREDITS: LazyLock = - LazyLock::new(|| regex::Regex::new(r"^\d+cr ").unwrap()); - CREDITS.is_match(&self.title) - } - - pub fn is_forma(&self) -> bool { - self.title.contains("Forma") - } - - pub fn is_nitain(&self) -> bool { - self.title.contains("Nitain Extract") - } - - pub fn is_orokin_reactor(&self) -> bool { - self.title.contains("Orokin Reactor") - } - - pub fn is_endo(&self) -> bool { - self.title.contains("ENDO") - } -} - -//------------------------------------------------------------------------------------------------- -// Guardian -//------------------------------------------------------------------------------------------------- - -#[derive(Debug, Clone, Queryable, Identifiable, AsChangeset, Model)] -pub struct Guardian { - pub id: i32, - pub telegram_name: String, - pub telegram_id: i64, - pub psn_name: String, - pub email: Option, - pub psn_clan: Option, - pub created_at: DateTime, - pub updated_at: DateTime, - pub deleted_at: Option>, - pub tokens: Option, - pub pending_activation_code: Option, - pub is_admin: bool, - pub is_superadmin: bool, -} - -#[derive(Insertable, NewModel)] -#[table_name = "guardians"] -#[model(Guardian)] -pub struct NewGuardian<'a> { - pub telegram_name: &'a str, - pub telegram_id: i64, - pub psn_name: &'a str, -} - -impl Guardian { - pub fn format_name(&self) -> String { - format!("{} (t.me/{})", self.psn_name, self.telegram_name) - } - - pub fn names(&self) -> (String, String) { - (self.telegram_name.clone(), self.psn_name.clone()) - } -} - -impl fmt::Display for Guardian { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} (t.me/{})", self.psn_name, self.telegram_name) - } -} - -//------------------------------------------------------------------------------------------------- -// PlannedActivity -//------------------------------------------------------------------------------------------------- - -// class PlannedActivity(id: EntityID) : IntEntity(id) { -// var author by Guardian referencedOn PlannedActivities.authorId -// var activity by Activity referencedOn PlannedActivities.activityId -// var start by PlannedActivities.start -// var details by PlannedActivities.details -// val members by PlannedActivityMember referrersOn PlannedActivityMembers.plannedActivityId - -#[derive(Debug, Queryable, Identifiable, AsChangeset, Associations, Model)] -#[belongs_to(Guardian, foreign_key = "author_id")] -#[belongs_to(Activity, foreign_key = "activity_id")] -#[table_name = "plannedactivities"] -pub struct PlannedActivity { - pub id: i32, - pub author_id: i32, // refs Guardians - pub activity_id: i32, // refs Activities - pub details: Option, - pub start: DateTime, -} - -// Output information -#[derive(Serialize, Deserialize)] -pub struct PlannedActivityTemplate { - pub id: i32, - pub name: String, - pub details: String, - pub members: Vec, - pub count: usize, - pub time: String, - pub fireteam_full: bool, - pub fireteam_joined: bool, - pub join_link: String, - pub leave_link: String, -} - -#[derive(Insertable, NewModel)] -#[table_name = "plannedactivities"] -#[model(PlannedActivity)] -pub struct NewPlannedActivity { - pub author_id: i32, // refs Guardians - pub activity_id: i32, // refs Activities - pub start: DateTime, -} - -impl PlannedActivity { - pub fn to_template( - &self, - guardian: Option<&Guardian>, - connection: &DbConnection, - ) -> PlannedActivityTemplate { - let activity = self.activity(connection); - - let count = activity.max_fireteam_size as usize - self.members_count(connection); - - PlannedActivityTemplate { - id: self.id, - name: activity.format_name(), - details: self.format_details(), - members: self - .members(connection) - .into_iter() - .map(|m| m.to_template(connection)) - .collect(), - count, - time: format_start_time(self.start, reference_date()), - fireteam_full: count == 0, - join_link: self.join_prompt(connection), - fireteam_joined: self.find_member(connection, guardian).is_some(), - leave_link: self.cancel_link(), - } - } - - pub fn upcoming_activities(connection: &DbConnection) -> Vec { - use { - crate::{datetime::nowtz, schema::plannedactivities::dsl::*}, - diesel::dsl::IntervalDsl, - }; - - plannedactivities - .filter(start.ge(nowtz() - 60_i32.minutes())) - .order(start.asc()) - .load::(connection) - .expect("TEMP failed to load planned activities @FIXME") - } - - pub fn author(&self, connection: &DbConnection) -> Option { - Guardian::find_one(connection, &self.author_id) - .expect("Failed to load PlannedActivity author") - } - - pub fn activity(&self, connection: &DbConnection) -> Activity { - Activity::find_one(connection, &self.activity_id) - .expect("Failed to load associated Activity") - .expect("PlannedActivity without Activity shouldn't exist") - } - - pub fn members(&self, connection: &DbConnection) -> Vec { - use crate::schema::plannedactivitymembers::dsl::*; - plannedactivitymembers - .filter(planned_activity_id.eq(self.id)) - .order(added.asc()) - .load::(connection) - .expect("Failed to load PlannedActivity members") - } - - pub fn members_count(&self, connection: &DbConnection) -> usize { - //@TODO replace with proper diesel query - self.members(connection).len() - } - - pub fn join_link(&self) -> String { - format!("/join{}", self.id) - } - - pub fn cancel_link(&self) -> String { - format!("/cancel{}", self.id) - } - - pub fn join_prompt(&self, connection: &DbConnection) -> String { - if self.is_full(connection) { - "This activity fireteam is full.".into() - } else { - let count = self.activity(connection).max_fireteam_size as usize - - self.members_count(connection); - format!( - "Enter `{joinLink}` to join this group. Up to {count} more can join.", - joinLink = self.join_link(), - count = count - ) - } - } - - pub fn is_full(&self, connection: &DbConnection) -> bool { - self.members(connection).len() >= self.activity(connection).max_fireteam_size as usize - } - - pub fn requires_more_members(&self, connection: &DbConnection) -> bool { - self.members(connection).len() < self.activity(connection).min_fireteam_size as usize - } - - pub fn format_details(&self) -> String { - self.details.clone().map(|s| s + "\n").unwrap_or_default() - } - - pub fn members_formatted(&self, connection: &DbConnection, joiner: &str) -> String { - self.members(connection) - .into_iter() - .map(|guardian| guardian.format_name(connection)) - .collect::>() - .as_slice() - .join(joiner) - } - - pub fn members_formatted_list(&self, connection: &DbConnection) -> String { - self.members_formatted(connection, ", ") - } - - pub fn members_formatted_column(&self, connection: &DbConnection) -> String { - self.members_formatted(connection, "\n") - } - - pub fn find_member( - &self, - connection: &DbConnection, - guardian: Option<&Guardian>, - ) -> Option { - use crate::schema::plannedactivitymembers::dsl::*; - - guardian.and_then(|g| { - plannedactivitymembers - .filter(user_id.eq(g.id)) - .filter(planned_activity_id.eq(self.id)) - .first::(connection) - .optional() - .expect("Failed to run SQL") - }) - } - - // Makes a telegram Html formatted display. - pub fn to_string(&self, connection: &DbConnection, g: Option<&Guardian>) -> String { - let event = self.to_template(g, connection); - render_template!("list/event", ("event", &event)) - .expect("Failed to render list event template") - } -} - -//------------------------------------------------------------------------------------------------- -// PlannedActivityMember -//------------------------------------------------------------------------------------------------- - -#[derive(Debug, Queryable, Identifiable, AsChangeset, Associations, Model)] -#[belongs_to(Guardian, foreign_key = "user_id")] -#[belongs_to(Activity, foreign_key = "planned_activity_id")] -#[table_name = "plannedactivitymembers"] -pub struct PlannedActivityMember { - pub id: i32, - pub planned_activity_id: i32, - pub user_id: i32, - pub added: DateTime, -} - -#[derive(Serialize, Deserialize)] -pub struct ActivityMemberTemplate { - pub psn_name: String, - pub telegram_name: String, - pub icon: String, -} - -#[derive(Insertable, NewModel)] -#[table_name = "plannedactivitymembers"] -#[model(PlannedActivityMember)] -pub struct NewPlannedActivityMember { - pub planned_activity_id: i32, - pub user_id: i32, - pub added: DateTime, -} - -impl PlannedActivityMember { - pub fn format_name(&self, connection: &DbConnection) -> String { - Guardian::find_one(connection, &self.user_id) - .expect("Failed to load associated Guardian") - .expect("Failed to find associated activity member") - .format_name() - } - - pub fn to_template(&self, connection: &DbConnection) -> ActivityMemberTemplate { - let (telegram_name, psn_name) = Guardian::find_one(connection, &self.user_id) - .expect("Failed to load associated Guardian") - .expect("Failed to find associated activity member") - .names(); - ActivityMemberTemplate { - psn_name, - telegram_name, - icon: self.icon(), - } - } - - pub fn icon(&self) -> String { - static ICON_POOL: LazyLock> = LazyLock::new(|| { - vec![ - "๐Ÿ’‚๐Ÿป", - "๐Ÿ•ต๐Ÿผ", - "๐Ÿง‘๐Ÿฝโ€๐Ÿญ", - "๐Ÿง‘โ€๐Ÿ’ป", - "๐Ÿง‘๐Ÿผโ€๐Ÿš’", - "๐Ÿง‘๐Ÿพโ€๐Ÿš€", - "๐Ÿฅท๐Ÿพ", - "๐Ÿฅท๐Ÿป", - "๐Ÿง™๐Ÿฝ", - "๐Ÿง๐Ÿผ", - "๐ŸงŒ", - "๐Ÿง›๐Ÿผ", - "๐ŸงŸ", - ] - }); - ICON_POOL[self.user_id.unsigned_abs() as usize % ICON_POOL.len()].into() - } -} - -//================================================================================================= -// Tests -//================================================================================================= - -#[cfg(test)] -mod tests { - use {super::*, crate::establish_db_connection, diesel::prelude::*}; - - #[test] - #[ignore] - fn test_guardians() -> Result<(), r2d2::Error> { - use crate::schema::guardians::dsl::*; - - dotenv::dotenv().ok(); - let pool = establish_db_connection(); - let connection = pool.get()?; - - let results = guardians - // .filter(published.eq(true)) - .limit(5) - .load::(&connection) - .expect("Error loading guardians"); - - println!("Displaying {} guardians", results.len()); - for guar in results { - println!("{}", guar); - } - - Ok(()) - } - - #[test] - #[ignore] - fn test_activities() -> Result<(), r2d2::Error> { - use crate::schema::activities::dsl::*; - - dotenv::dotenv().ok(); - let pool = establish_db_connection(); - let connection = pool.get()?; - - let results = activities - .load::(&connection) - .expect("Error loading activities"); - - println!("Displaying {} activities", results.len()); - for act in results { - println!("{}", act.format_name()); - } - - Ok(()) - } - - #[test] - #[ignore] - fn test_alerts() -> Result<(), r2d2::Error> { - use crate::schema::alerts::dsl::*; - - dotenv::dotenv().ok(); - let pool = establish_db_connection(); - let connection = pool.get()?; - - let results = alerts - .limit(5) - .load::(&connection) - .expect("Error loading alerts"); - - println!("Displaying {} alerts", results.len()); - for alrt in results { - println!("{}", alrt.title); - } - - Ok(()) - } - - #[test] - #[ignore] - fn test_planned_activities() -> Result<(), r2d2::Error> { - use crate::schema::guardians::dsl::*; - - dotenv::dotenv().ok(); - let pool = establish_db_connection(); - let connection = pool.get()?; - - let guar = guardians - .find(1) - .first::(&connection) - .expect("Guardian with id 1 not found"); - let results = PlannedActivity::belonging_to(&guar) - .load::(&connection) - .expect("Error loading activities"); - - println!("Displaying {} planned activities", results.len()); - for act in results { - println!("{}", act.to_string(&connection, Some(&guar))); - } - - Ok(()) - } -} diff --git a/src/schema.patch b/src/schema.patch deleted file mode 100644 index 9d87b779..00000000 --- a/src/schema.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/src/schema.rs b/src/schema.rs -index b23e3a3..4123d7a 100644 ---- a/src/schema.rs -+++ b/src/schema.rs -@@ -21,13 +21,13 @@ table! { - - table! { - alerts (id) { - id -> Int4, - guid -> Text, - title -> Text, - #[sql_name = "type"] -- type_ -> Text, -+ kind -> Text, - startdate -> Timestamptz, - expirydate -> Nullable, - faction -> Nullable, - flavor -> Nullable, - } - } diff --git a/src/schema.rs b/src/schema.rs deleted file mode 100644 index f6c8cf0d..00000000 --- a/src/schema.rs +++ /dev/null @@ -1,86 +0,0 @@ -table! { - activities (id) { - id -> Int4, - name -> Text, - mode -> Nullable, - min_fireteam_size -> Int4, - max_fireteam_size -> Int4, - min_light -> Nullable, - min_level -> Nullable, - } -} - -table! { - activityshortcuts (id) { - id -> Int4, - name -> Text, - game -> Text, - link -> Int4, - } -} - -table! { - alerts (id) { - id -> Int4, - guid -> Text, - title -> Text, - #[sql_name = "type"] - kind -> Text, - startdate -> Timestamptz, - expirydate -> Nullable, - faction -> Nullable, - flavor -> Nullable, - } -} - -table! { - guardians (id) { - id -> Int4, - telegram_name -> Text, - telegram_id -> Int8, - psn_name -> Text, - email -> Nullable, - psn_clan -> Nullable, - created_at -> Timestamptz, - updated_at -> Timestamptz, - deleted_at -> Nullable, - tokens -> Nullable, - pending_activation_code -> Nullable, - is_admin -> Bool, - is_superadmin -> Bool, - } -} - -table! { - plannedactivities (id) { - id -> Int4, - author_id -> Int4, - activity_id -> Int4, - details -> Nullable, - start -> Timestamptz, - } -} - -table! { - plannedactivitymembers (id) { - id -> Int4, - planned_activity_id -> Int4, - user_id -> Int4, - added -> Timestamptz, - } -} - -joinable!(activityshortcuts -> activities (link)); -joinable!(plannedactivities -> activities (activity_id)); -joinable!(plannedactivities -> guardians (author_id)); -joinable!(plannedactivitymembers -> guardians (user_id)); -joinable!(plannedactivitymembers -> plannedactivities (planned_activity_id)); - -allow_tables_to_appear_in_same_query!( - activities, - activityshortcuts, - alerts, - guardians, - plannedactivities, - plannedactivitymembers, -); diff --git a/src/services/mod.rs b/src/services/mod.rs deleted file mode 100644 index 5002bebf..00000000 --- a/src/services/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod destiny_schedule; -pub use self::destiny_schedule::*; -pub mod reminder; -pub use self::reminder::*; -pub mod reminder_actor; diff --git a/src/services/reminder.rs b/src/services/reminder.rs deleted file mode 100644 index 9fde7fbb..00000000 --- a/src/services/reminder.rs +++ /dev/null @@ -1,39 +0,0 @@ -use { - crate::{ - bot_actor::{BotActorMsg, Format, Notify, SendMessage}, - datetime::reference_date, - models::PlannedActivity, - BotConnection, - }, - riker::{actor::Tell, actors::ActorRef}, - teloxide::types::ChatId, -}; - -pub fn check(bot: ActorRef, connection: BotConnection, chat_id: ChatId) { - // log::info!("reminder check at {}", reference_date()); - - let reference = reference_date(); - - let upcoming_events: Vec = PlannedActivity::upcoming_activities(&connection) - .into_iter() - .filter(|event| { - if event.start > reference { - matches!((event.start - reference).num_minutes(), 60 | 15 | 0) - } else { - false - } - }) - .collect(); - - if upcoming_events.is_empty() { - return; - } - - let text = upcoming_events - .into_iter() - .fold("Activities starting soon:\n\n".to_owned(), |acc, event| { - acc + &format!("{}\n\n", event.to_string(&connection, None)) - }); - - bot.tell(SendMessage(text, chat_id, Format::Html, Notify::On), None); -} diff --git a/src/services/reminder_actor.rs b/src/services/reminder_actor.rs deleted file mode 100644 index 9768693d..00000000 --- a/src/services/reminder_actor.rs +++ /dev/null @@ -1,146 +0,0 @@ -use { - crate::{ - bot_actor::BotActorMsg, - datetime::{d2_reset_time, reference_date, start_at_time, start_at_weekday_time}, - services::{destiny_schedule, reminder}, - BotConnection, DbConnPool, - }, - chrono::Timelike, - riker::{ - actors::{ - actor, Actor, ActorFactoryArgs, ActorRef, BasicActorRef, Context, Receive, Sender, Tell, - }, - system::Timer, - }, - teloxide::types::ChatId, -}; - -#[actor( - Reminders, - DailyReset, - WeeklyReset, - ScheduleNextMinute, - ScheduleNextDay, - ScheduleNextWeek -)] -pub struct ReminderActor { - bot_ref: ActorRef, - lfg_chat: i64, - connection_pool: DbConnPool, -} - -impl ReminderActor { - pub fn connection(&self) -> BotConnection { - self.connection_pool.get().unwrap() - } -} - -impl Actor for ReminderActor { - type Msg = ReminderActorMsg; - - fn recv(&mut self, ctx: &Context, msg: Self::Msg, sender: Sender) { - self.receive(ctx, msg, sender); - } -} - -impl ActorFactoryArgs<(ActorRef, i64, DbConnPool)> for ReminderActor { - fn create_args( - (bot_ref, lfg_chat, connection_pool): (ActorRef, i64, DbConnPool), - ) -> Self { - Self { - bot_ref, - lfg_chat, - connection_pool, - } - } -} - -#[derive(Clone, Debug)] -pub struct Reminders; - -#[derive(Clone, Debug)] -pub struct DailyReset; - -#[derive(Clone, Debug)] -pub struct WeeklyReset; - -impl Receive for ReminderActor { - type Msg = ReminderActorMsg; - - fn receive(&mut self, ctx: &Context, _msg: Reminders, _sender: Sender) { - reminder::check( - self.bot_ref.clone(), - self.connection(), - ChatId::Id(self.lfg_chat), - ); - ctx.myself().tell(ScheduleNextMinute, None); - } -} - -impl Receive for ReminderActor { - type Msg = ReminderActorMsg; - - fn receive(&mut self, ctx: &Context, _msg: DailyReset, _sender: Sender) { - destiny_schedule::daily_reset(self.bot_ref.clone(), ChatId::Id(self.lfg_chat)); - ctx.myself().tell(ScheduleNextDay, None); - } -} - -impl Receive for ReminderActor { - type Msg = ReminderActorMsg; - - fn receive(&mut self, ctx: &Context, _msg: WeeklyReset, _sender: Sender) { - destiny_schedule::major_weekly_reset(self.bot_ref.clone(), ChatId::Id(self.lfg_chat)); - ctx.myself().tell(ScheduleNextWeek, None); - } -} - -#[derive(Clone, Debug)] -pub struct ScheduleNextMinute; - -#[derive(Clone, Debug)] -pub struct ScheduleNextDay; - -#[derive(Clone, Debug)] -pub struct ScheduleNextWeek; - -impl Receive for ReminderActor { - type Msg = ReminderActorMsg; - - fn receive(&mut self, ctx: &Context, _msg: ScheduleNextMinute, _sender: Sender) { - ctx.schedule_at_time( - (reference_date() + chrono::Duration::minutes(1)) - .with_second(0) - .unwrap(), - ctx.myself(), - None, - Reminders, - ); - } -} - -impl Receive for ReminderActor { - type Msg = ReminderActorMsg; - - fn receive(&mut self, ctx: &Context, _msg: ScheduleNextDay, _sender: Sender) { - ctx.schedule_at_time( - start_at_time(reference_date(), d2_reset_time()), - ctx.myself(), - None, - DailyReset, - ); - } -} - -impl Receive for ReminderActor { - type Msg = ReminderActorMsg; - - fn receive(&mut self, ctx: &Context, _msg: ScheduleNextWeek, _sender: Sender) { - ctx.schedule_at_time( - start_at_weekday_time(reference_date(), chrono::Weekday::Tue, d2_reset_time()), - ctx.myself(), - None, - WeeklyReset, - ); - } -} diff --git a/templates/join/joined.tera b/templates/join/joined.tera deleted file mode 100644 index 71e629e7..00000000 --- a/templates/join/joined.tera +++ /dev/null @@ -1,3 +0,0 @@ -{{guarName}} has joined {{actName}} group {{actTime}} -{{otherGuars}} are going -{{joinPrompt}}{# join link and a few ifs? #} diff --git a/templates/join/usage.tera b/templates/join/usage.tera deleted file mode 100644 index e60af32a..00000000 --- a/templates/join/usage.tera +++ /dev/null @@ -1,2 +0,0 @@ -To join a fireteam provide fireteam id -Fireteam IDs are available from the output of `/list` command.