diff --git a/.gitignore b/.gitignore index ffd9a01..734bea2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /result /.env +state.yml diff --git a/Cargo.lock b/Cargo.lock index bb296af..d92d34c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "async-channel" version = "1.6.1" @@ -281,7 +290,7 @@ version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ - "ansi_term", + "ansi_term 0.11.0", "atty", "bitflags", "strsim", @@ -310,6 +319,22 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "core-foundation" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.1" @@ -321,9 +346,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +checksum = "3825b1e8580894917dc4468cb634a1b4e9745fddc854edad72d9c04644c0319f" dependencies = [ "cfg-if", ] @@ -363,6 +388,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "encoding_rs" version = "0.8.29" @@ -419,6 +450,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -431,12 +477,13 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -445,9 +492,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" dependencies = [ "futures-core", "futures-sink", @@ -455,15 +502,26 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" + +[[package]] +name = "futures-executor" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" [[package]] name = "futures-lite" @@ -482,12 +540,10 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" dependencies = [ - "autocfg", - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -495,23 +551,22 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" dependencies = [ - "autocfg", "futures-channel", "futures-core", "futures-io", @@ -521,8 +576,6 @@ dependencies = [ "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] @@ -547,17 +600,34 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + [[package]] name = "glados" version = "0.1.0" dependencies = [ "dotenv", + "influx_db_client", "lazy_static", + "nom", "rcon", + "rustbreak", + "serde", "serenity", "structopt", "thiserror", "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -646,9 +716,9 @@ checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" @@ -689,6 +759,19 @@ dependencies = [ "webpki", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.1.0", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.2.3" @@ -710,6 +793,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "influx_db_client" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1a5abf7f7759f14075abcc0140d79a339729e50042ddf93fc0de9d11ddb1f" +dependencies = [ + "bytes 1.1.0", + "futures", + "reqwest", + "serde", + "serde_json", +] + [[package]] name = "input_buffer" version = "0.3.1" @@ -766,9 +862,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.107" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "log" @@ -780,6 +882,15 @@ dependencies = [ "value-bag", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.9" @@ -808,6 +919,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -840,6 +957,35 @@ dependencies = [ "winapi", ] +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + [[package]] name = "ntapi" version = "0.3.6" @@ -890,6 +1036,39 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking" version = "2.0.0" @@ -934,6 +1113,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" + [[package]] name = "polling" version = "2.2.0" @@ -977,18 +1162,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - [[package]] name = "proc-macro2" version = "1.0.32" @@ -1013,11 +1186,23 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom", + "getrandom 0.1.16", "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", ] [[package]] @@ -1027,7 +1212,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", ] [[package]] @@ -1036,7 +1231,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom", + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", ] [[package]] @@ -1045,20 +1249,71 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", ] [[package]] name = "rcon" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a6f903164a399084787547a026b83e7937bc576d8acdbd9e41ebf5de90a85" +checksum = "6b7fdd146f86bd90fa2d4cf83a28b45f058e90bcf11ed0cce134e757928771e6" dependencies = [ "async-std", "bytes 1.1.0", "err-derive", ] +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "reqwest" version = "0.11.6" @@ -1074,12 +1329,14 @@ dependencies = [ "http-body", "hyper", "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "lazy_static", "log", "mime", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", "rustls", @@ -1087,6 +1344,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", + "tokio-native-tls", "tokio-rustls", "url", "wasm-bindgen", @@ -1111,6 +1369,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustbreak" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460d97902465327d69ecfe8cefdb5972c6f94d6127ac9e992acdb51458bebc27" +dependencies = [ + "serde", + "serde_yaml", + "tempfile", + "thiserror", +] + [[package]] name = "rustls" version = "0.19.1" @@ -1136,6 +1406,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + [[package]] name = "sct" version = "0.6.1" @@ -1146,6 +1426,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.130" @@ -1168,9 +1471,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3" +checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19" dependencies = [ "itoa", "ryu", @@ -1189,6 +1492,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af" +dependencies = [ + "dtoa", + "indexmap", + "serde", + "yaml-rust", +] + [[package]] name = "serenity" version = "0.10.9" @@ -1229,12 +1544,27 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "slab" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + [[package]] name = "socket2" version = "0.4.2" @@ -1310,6 +1640,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1340,13 +1684,21 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.1.44" +name = "thread_local" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -1392,6 +1744,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.22.0" @@ -1456,6 +1818,35 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507ec620f809cdf07cccb5bc57b13069a88031b795efd4079b1c71b66c1613d" +dependencies = [ + "ansi_term 0.12.1", + "lazy_static", + "matchers", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -1475,7 +1866,7 @@ dependencies = [ "httparse", "input_buffer", "log", - "rand", + "rand 0.7.3", "sha-1", "url", "utf-8", @@ -1575,6 +1966,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -1611,9 +2008,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" @@ -1758,3 +2155,12 @@ checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ "winapi", ] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index d8c7cbc..2ac2a2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,15 @@ toolchain = "nightly" dotenv = "0.15.0" lazy_static = "1.4.0" rcon = "0.5.1" +rustbreak = { version = "2.0.0", features = [ "yaml_enc" ] } +serde = { version = "1.0.130", features = [ "derive" ] } serenity = "0.10.9" structopt = "0.3.25" thiserror = "1.0.30" +tracing = "0.1.29" +tracing-subscriber = { version = "0.3.2", features = [ "env-filter" ] } +influx_db_client = "0.5.0" +nom = "7.1.0" [dependencies.tokio] version = "1.14.0" diff --git a/flake.lock b/flake.lock index 335cb07..5327992 100644 --- a/flake.lock +++ b/flake.lock @@ -22,11 +22,11 @@ "rustOverlay": "rustOverlay" }, "locked": { - "lastModified": 1637129418, - "narHash": "sha256-bO6rLgIiqK6pdeF2ewKyD6c+hNAcBEfXDqiTRaWzNmo=", + "lastModified": 1637302206, + "narHash": "sha256-X82LW/R35vCxNSk9jcddZFbjO6ZMjsq+KhIGC/GMkJg=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "1faede2be6c28a68a00b3479b77c849720324511", + "rev": "a25206065a3a19d3dbcb2192d9bd273eea5cd919", "type": "github" }, "original": { @@ -59,11 +59,11 @@ "rustOverlay": { "flake": false, "locked": { - "lastModified": 1637115307, - "narHash": "sha256-G+RKZeE1yLrnq+ExHF+HnSJsT+QJWebGhssgZHz3B00=", + "lastModified": 1637288133, + "narHash": "sha256-x5XWEK333KEhy2WL3TafE1vSa8/A1sGdbirTIV2bmSc=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "8a2e5fa870df3d34667d28fb3383d19516d182e4", + "rev": "ccc467eff80b2fbb8000cf425e999ef14fbe200c", "type": "github" }, "original": { diff --git a/src/conf.rs b/src/conf.rs index fe6aa96..3a46562 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -1,7 +1,8 @@ -use std::collections::HashSet; +use std::{collections::HashSet, path::PathBuf}; +use influx_db_client::reqwest::Url; use lazy_static::lazy_static; -use serenity::model::id::UserId; +use serenity::model::id::{ChannelId, GuildId, UserId}; use structopt::StructOpt; lazy_static! { @@ -14,17 +15,29 @@ pub struct Args { short = "t", value_name = "TOKEN", validator = validate_token, - env = "DISCORD_TOKEN")] + env = "DISCORD_TOKEN", + hide_env_values = true)] pub discord_token: String, #[structopt(long, value_name = "URL", env = "RCON_ADDRESS")] pub rcon_address: String, - #[structopt(long, value_name = "PASSWORD", env = "RCON_PASSWORD")] + #[structopt( + long, + value_name = "PASSWORD", + env = "RCON_PASSWORD", + hide_env_values = true + )] pub rcon_password: String, - #[structopt(long, short, value_name = "ID", env = "DISCORD_OWNERS")] - pub owners: Vec, + #[structopt( + long, + short, + value_name = "ID", + env = "DISCORD_OWNERS", + use_delimiter = true + )] + pub owner: Vec, #[structopt( long, @@ -34,6 +47,36 @@ pub struct Args { default_value = "~" )] pub prefix: String, + + #[structopt( + long = "db", + short, + value_name = "PATH", + env = "DB_PATH", + default_value = "state.yml" + )] + pub database_path: PathBuf, + + #[structopt(long, short, value_name = "ID", env = "GUILD_ID")] + pub guild_id: u64, + + #[structopt(long, value_name = "ID", env = "GUILD_ADMIN_CHANNEL")] + pub admin_channel: u64, + + #[structopt(long, value_name = "ID", env = "GUILD_INFO_CHANNEL")] + pub info_channel: u64, + + #[structopt(long, value_name = "URL", env = "INFLUX_HOST")] + pub influx_host: Url, + + #[structopt(long, value_name = "DB", env = "INFLUX_DB")] + pub influx_db: String, + + #[structopt(long, value_name = "USER", env = "INFLUX_USER")] + pub influx_user: String, + + #[structopt(long, value_name = "PW", env = "INFLUX_PW", hide_env_values = true)] + pub influx_password: String, } fn validate_token(raw: String) -> std::result::Result<(), String> { @@ -42,9 +85,21 @@ fn validate_token(raw: String) -> std::result::Result<(), String> { impl Args { pub fn owners(&self) -> HashSet { - if self.owners.is_empty() { - eprintln!("You should probably specify at least one `--owner`"); + if self.owner.is_empty() { + tracing::warn!("You should probably specify at least one `--owner`"); } - self.owners.iter().cloned().collect() + self.owner.iter().cloned().collect() + } + + pub fn guild_id(&self) -> GuildId { + self.guild_id.into() + } + + pub fn admin_channel(&self) -> ChannelId { + self.admin_channel.into() + } + + pub fn info_channel(&self) -> ChannelId { + self.info_channel.into() } } diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..c191a9b --- /dev/null +++ b/src/data.rs @@ -0,0 +1,16 @@ +use rustbreak::deser::Yaml; +use serenity::prelude::TypeMapKey; + +use crate::{rcon::RconHandle, state::State}; + +pub struct StateKey; + +impl TypeMapKey for StateKey { + type Value = rustbreak::FileDatabase; +} + +pub struct Rcon; + +impl TypeMapKey for Rcon { + type Value = RconHandle; +} diff --git a/src/discord.rs b/src/discord.rs index 5e9f07b..4a04058 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,33 +1,55 @@ -use serenity::{ - framework::{standard::macros::group, StandardFramework}, - Client, -}; +//! # Workflows +//! +//! - `~ping` +//! - Pong! +//! +//! - `~invite Random#1234` +//! - Invite someone into the guild +//! +//! - Member_add +//! - Notify guild about new member +//! +//! - Member_leave +//! - Notify guild about leave + +use lazy_static::lazy_static; +use serenity::{client::bridge::gateway::GatewayIntents, Client}; use crate::{ conf::ARGS, + data, error::{Error, Result}, + influx::{InfluxDb, InfluxKey}, + rcon::RconHandle, + state::State, }; -pub struct Api { +mod framework; +mod handler; +mod util; + +use self::handler::Handler; + +lazy_static! { + static ref INTENTS: GatewayIntents = { + use GatewayIntents as GI; + GI::DIRECT_MESSAGES | GI::DIRECT_MESSAGE_REACTIONS | GI::GUILD_MEMBERS | GI::GUILD_MESSAGES + }; +} + +pub struct Bot { client: Client, } -#[group] -#[owners_only] -struct Owner; - -impl Api { +impl Bot { pub async fn init() -> Result { - let framework = StandardFramework::new() - .configure(|c| { - c.with_whitespace(true) - .owners(ARGS.owners()) - .prefix(&ARGS.prefix) - .delimiters(vec![", ", ","]) - }) - .group(&OWNER_GROUP); let client = Client::builder(&ARGS.discord_token) - .framework(framework) + .framework(framework::init()) + .intents(*INTENTS) + .event_handler(Handler::default()) + .type_map_insert::(RconHandle::init().await) + .type_map_insert::(InfluxDb::init()) + .type_map_insert::(State::load().await?) .await .map_err(Error::CreatingDiscordClient)?; Ok(Self { client }) diff --git a/src/discord/framework.rs b/src/discord/framework.rs new file mode 100644 index 0000000..77d43ef --- /dev/null +++ b/src/discord/framework.rs @@ -0,0 +1,67 @@ +use std::collections::HashSet; + +use serenity::{ + client::Context, + framework::{ + standard::{ + help_commands, + macros::{command, group, help}, + Args, CommandGroup, CommandResult, HelpOptions, + }, + StandardFramework, + }, + model::{channel::Message, id::UserId}, +}; + +use crate::conf::ARGS; + +pub fn init() -> StandardFramework { + StandardFramework::new() + .configure(|c| { + c.with_whitespace(true) + .owners(ARGS.owners()) + .prefix(&ARGS.prefix) + .delimiters(vec![", ", ","]) + }) + .help(&BOT_HELP) + .group(&OWNER_GROUP) + .group(&GENERAL_GROUP) +} + +#[group] +#[owners_only] +#[commands(ping, invite)] +struct Owner; + +#[group] +struct General; + +#[command] +#[description = "Ping the bot to see if it's online"] +async fn ping(ctx: &Context, msg: &Message) -> CommandResult { + msg.reply(&ctx.http, "Pong!").await?; + Ok(()) +} + +#[command] +#[description = "Invite a new member to the guild"] +async fn invite(ctx: &Context, msg: &Message) -> CommandResult { + msg.reply(&ctx.http, "TBD").await?; + Ok(()) +} + +#[help] +#[max_levenshtein_distance(3)] +#[lacking_permissions = "Hide"] +#[lacking_role = "Nothing"] +async fn bot_help( + context: &Context, + msg: &Message, + args: Args, + help_options: &'static HelpOptions, + groups: &[&'static CommandGroup], + owners: HashSet, +) -> CommandResult { + let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await; + Ok(()) +} diff --git a/src/discord/handler.rs b/src/discord/handler.rs new file mode 100644 index 0000000..11eaf81 --- /dev/null +++ b/src/discord/handler.rs @@ -0,0 +1,57 @@ +use serenity::{ + client::{Context, EventHandler}, + model::{ + guild::Member, + id::GuildId, + prelude::{Ready, User}, + }, +}; + +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::{conf::ARGS, error::ResultExt, tracking::track_server_status}; + +use super::util::{initial_state_sanitization, workflow_member_add, workflow_member_removal}; + +#[derive(Debug, Default)] +pub struct Handler { + setup_done: AtomicBool, +} + +#[serenity::async_trait] +impl EventHandler for Handler { + async fn ready(&self, ctx: Context, _data_about_bot: Ready) { + // Only do this once! + if !self.setup_done.load(Ordering::Relaxed) { + let ctx_clone = ctx.clone(); + + initial_state_sanitization(&ctx_clone) + .await + .log_warn("sanitizing state at startup"); + + tokio::spawn(track_server_status(ctx_clone)); + + self.setup_done.swap(true, Ordering::Relaxed); + } + } + + async fn guild_member_addition(&self, ctx: Context, guild_id: GuildId, new_member: Member) { + if guild_id != ARGS.guild_id { + return; + } + workflow_member_add(&ctx, new_member).await; + } + + async fn guild_member_removal( + &self, + ctx: Context, + guild_id: GuildId, + user: User, + _member_data: Option, + ) { + if guild_id != ARGS.guild_id { + return; + } + workflow_member_removal(&ctx, user).await; + } +} diff --git a/src/discord/util.rs b/src/discord/util.rs new file mode 100644 index 0000000..c2f5b49 --- /dev/null +++ b/src/discord/util.rs @@ -0,0 +1,107 @@ +use serenity::{ + client::Context, + model::{guild::Member, prelude::User}, +}; + +use core::fmt; +use std::collections::HashSet; + +use crate::{ + conf::ARGS, + error::{Result, ResultExt}, + state::{GuildMember, State}, +}; + +pub async fn initial_state_sanitization(ctx: &Context) -> Result { + catch_up_on_guild_members(ctx).await?; + + Ok(()) +} + +pub async fn workflow_member_add(ctx: &Context, new_member: Member) { + tracing::info!("Adding new member {:?}", new_member.user.name); + // Keep track of the new member + State::write(ctx, |state| { + state.guild_members.insert(new_member.user.id.into()) + }) + .await + .log_warn("adding new member to guild list"); + // Notify about new arrival + admin_channel_say(ctx, format!("{} joined the guild!", new_member.user)) + .await + .log_warn("notifying about new guild member"); + // Greet the new member with a DM + welcome_new_member(ctx, &new_member) + .await + .log_warn("welcoming new member"); +} + +pub async fn workflow_member_removal(ctx: &Context, user: User) { + let old_member: Option = + State::write(ctx, |state| state.guild_members.take(&user.id)) + .await + .log_warn("removing ex-guild-member") + .flatten(); + if old_member.is_some() { + admin_channel_say(ctx, format!("{} left the server!", user)) + .await + .log_warn("notifying guild about leaving member"); + } +} + +async fn welcome_new_member(ctx: &Context, member: &Member) -> Result { + let channel = member.user.create_dm_channel(ctx.http.clone()).await?; + channel.send_message(ctx.http.clone(), |m| { + m.content(format!(r#"Hey {}, +Welcome to the CCQCraft Server! Most information can be found in the {info_channel} channel! If you have any questions message either megamanmalte or Gim. Not me, I'm just a bot :)"#, + member.user.name, + info_channel=ARGS.info_channel())) + }).await?; + Ok(()) +} + +async fn admin_channel_say(ctx: &Context, what: impl fmt::Display) -> Result { + ARGS.admin_channel().say(ctx.http.clone(), what).await?; + Ok(()) +} + +async fn catch_up_on_guild_members(ctx: &Context) -> Result { + let actual_members: HashSet<_> = ARGS + .guild_id() + .members(ctx.http.clone(), None, None) + .await? + .into_iter() + .map(|member| member.user.id) + .collect(); + let known_members: HashSet<_> = State::read(ctx, |state| { + state.guild_members.iter().map(|member| member.id).collect() + }) + .await?; + + for old in known_members.difference(&actual_members) { + let old_user = match old + .to_user(ctx.http.clone()) + .await + .log_warn("retrieving user based on id") + { + Some(member) => member, + None => continue, + }; + workflow_member_removal(ctx, old_user).await; + } + + for new in actual_members.difference(&known_members) { + let new_member = match ARGS + .guild_id() + .member(ctx.http.clone(), new) + .await + .log_warn("retrieving member based on id") + { + Some(member) => member, + None => continue, + }; + workflow_member_add(ctx, new_member).await; + } + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index 8ebd8e2..0dc4efb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,11 @@ +use core::fmt; + use thiserror::Error; pub type Result = ::std::result::Result; #[derive(Debug, Error)] pub enum Error { - #[error("Configuration key {_0:?} is missing. Try setting it through args or the environment")] - MissingConfiguration(String), #[error("Failed to initialize RCON: {_0}")] InitializingRcon(#[source] rcon::Error), #[error("Failed to execute RCON command {_0:?}: {_1}")] @@ -14,4 +14,53 @@ pub enum Error { CreatingDiscordClient(#[source] serenity::Error), #[error("Error while listening for Discord events: {_0}")] Serenity(#[from] serenity::Error), + #[error("Error while handling database: {_0}")] + Rustbreak(#[from] rustbreak::error::RustbreakError), + #[error("Error while talking to Influx db: {_0}")] + Influx(#[from] influx_db_client::Error), + #[error("Parsing results of `tps` command: {_0}")] + RconTpsCmd(#[source] nom::Err>), + #[error("Parsing results of `list` command: {_0}")] + RconListCmd(#[source] nom::Err>), +} + +pub trait ResultExt { + fn log_info(self, when: &str) -> Option; + fn log_warn(self, when: &str) -> Option; + fn log_error(self, when: &str) -> Option; +} + +impl ResultExt for std::result::Result +where + E: fmt::Display, +{ + fn log_info(self, when: &str) -> Option { + match self { + Ok(val) => Some(val), + Err(why) => { + tracing::info!("Error occured while {}: {}", when, why); + None + } + } + } + + fn log_warn(self, when: &str) -> Option { + match self { + Ok(val) => Some(val), + Err(why) => { + tracing::warn!("Error occured while {}: {}", when, why); + None + } + } + } + + fn log_error(self, when: &str) -> Option { + match self { + Ok(val) => Some(val), + Err(why) => { + tracing::error!("Error occured while {}: {}", when, why); + None + } + } + } } diff --git a/src/influx.rs b/src/influx.rs new file mode 100644 index 0000000..ce15861 --- /dev/null +++ b/src/influx.rs @@ -0,0 +1,44 @@ +use influx_db_client::{Client, Point}; +use serenity::{client::Context, prelude::TypeMapKey}; + +use crate::{conf::ARGS, error::Result}; + +#[derive(Debug)] +pub struct InfluxDb { + inner: Client, +} + +pub struct InfluxKey; + +impl TypeMapKey for InfluxKey { + type Value = InfluxDb; +} + +impl InfluxDb { + pub fn init() -> Self { + let inner = Client::new(ARGS.influx_host.clone(), &ARGS.influx_db) + .set_authentication(&ARGS.influx_user, &ARGS.influx_password); + Self { inner } + } + + //pub async fn write_point(point: Point, ctx: &Context) -> Result { + // let client = ctx.data.read().await; + // let client = client + // .get::() + // .expect("BUG: No Influx client present"); + // client.inner.write_point(point, None, None).await?; + // Ok(()) + //} + + pub async fn write_points(points: T, ctx: &Context) -> Result + where + T: Iterator, + { + let client = ctx.data.read().await; + let client = client + .get::() + .expect("BUG: No Influx client present"); + client.inner.write_points(points, None, None).await?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index b53dd20..7f37377 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,13 +3,16 @@ //! Helper tool for my minecraft instance. mod conf; +mod data; mod discord; mod error; +mod influx; mod rcon; mod state; +mod tracking; +use discord::Bot; use error::Result; -use state::State; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -21,7 +24,7 @@ async fn main() { async fn real_main() -> Result { dotenv::dotenv().ok(); + tracing_subscriber::fmt::init(); - let state = State::init().await?; - state.run_bot().await + Bot::init().await?.start().await } diff --git a/src/rcon.rs b/src/rcon.rs index 4d1eafd..c1e49cf 100644 --- a/src/rcon.rs +++ b/src/rcon.rs @@ -1,33 +1,69 @@ +use std::time::Duration; + +use serenity::client::Context; + use crate::{ conf::ARGS, - error::{Error, Result}, + data, + error::{Error, Result, ResultExt}, }; -pub struct Rcon { - inner: ::rcon::Connection, +pub struct RconHandle { + inner: Option<::rcon::Connection>, } -impl Rcon { - pub async fn init() -> Result { +pub trait RconCommand { + type Output; + fn as_string(&self) -> String; + fn parse(&self, raw: String) -> Result; +} + +impl RconHandle { + pub async fn init() -> Self { + let mut handle = Self { inner: None }; + handle.connect().await; + handle + } + + pub async fn cmd(cmd: &C, ctx: &Context) -> Result + where + C: RconCommand, + { + let mut rcon = ctx.data.write().await; + let rcon = rcon.get_mut::().expect("BUG: No rcon client"); + rcon.reconnect_until_success().await; + let cmd_str = cmd.as_string(); + let raw_output = rcon + .inner + .as_mut() + // We must be connected here + .unwrap() + .cmd(&cmd_str) + .await + .map_err(|why| Error::RconCommand(cmd_str, why))?; + cmd.parse(raw_output) + } + + async fn reconnect_until_success(&mut self) { + let mut wait = Duration::from_secs(1); + if self.inner.is_none() { + self.connect().await; + } + while self.inner.is_none() { + tokio::time::sleep(wait).await; + self.connect().await; + wait *= 2; + } + } + + async fn connect(&mut self) { let addr = &ARGS.rcon_address; let password = &ARGS.rcon_password; - let inner = rcon::Connection::builder() + self.inner = rcon::Connection::builder() .enable_minecraft_quirks(true) - .connect(addr, &password) + .connect(addr, password) .await - .map_err(Error::InitializingRcon)?; - Ok(Self { inner }) - } - - pub async fn cmd(&mut self, cmd: &str) -> Result { - self.inner - .cmd(cmd) - .await - .map_err(|why| Error::RconCommand(cmd.into(), why)) - } - - pub async fn greet(&mut self) -> Result { - let _ = self.cmd("say GLaDOS is now online").await?; - Ok(()) + .map_err(Error::InitializingRcon) + .log_warn("initializing rcon connection"); } } diff --git a/src/state.rs b/src/state.rs index 1144c60..6097dd7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,18 +1,95 @@ -use crate::{discord::Api, error::Result, rcon::Rcon}; +use rustbreak::{deser::Yaml, FileDatabase}; +use serde::{Deserialize, Serialize}; +use serenity::{client::Context, model::id::UserId}; +use std::{borrow::Borrow, collections::HashSet, hash::Hash}; + +use crate::{ + conf::ARGS, + data, + error::{Error, Result, ResultExt}, +}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct State { - pub rcon: Rcon, - pub api: Api, + /// Keeps track of the current guild members + pub guild_members: HashSet, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GuildMember { + /// Discord ID of the member + pub id: UserId, } impl State { - pub async fn init() -> Result { - let rcon = Rcon::init().await?; - let api = Api::init().await?; - Ok(State { rcon, api }) + pub async fn load() -> Result> { + let db = FileDatabase::load_from_path_or_default(&ARGS.database_path)?; + Ok(db) } - pub async fn run_bot(self) -> Result { - self.api.start().await + pub async fn save(ctx: &Context) -> Result { + ctx.data + .read() + .await + .get::() + .expect("BUG: Missing state in TypeMap") + .save()?; + Ok(()) + } + + pub async fn read(ctx: &Context, f: F) -> Result + where + F: FnOnce(&State) -> R, + { + ctx.data + .read() + .await + .get::() + .expect("BUG: Missing state in TypeMap") + .read(f) + .map_err(Error::from) + } + + pub async fn write(ctx: &Context, f: F) -> Result + where + F: FnOnce(&mut State) -> R, + { + let res = ctx + .data + .read() + .await + .get::() + .expect("BUG: Missing state in TypeMap") + .write(f) + .map_err(Error::from); + State::save(ctx).await.log_warn("saving state data to disk"); + res + } +} + +impl From for GuildMember { + fn from(id: UserId) -> Self { + Self { id } + } +} + +impl Hash for GuildMember { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl PartialEq for GuildMember { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl Eq for GuildMember {} + +impl Borrow for GuildMember { + fn borrow(&self) -> &UserId { + &self.id } } diff --git a/src/tracking.rs b/src/tracking.rs new file mode 100644 index 0000000..36b9e21 --- /dev/null +++ b/src/tracking.rs @@ -0,0 +1,26 @@ +use serenity::client::Context; + +use std::time::Duration; + +mod status; + +use crate::{error::ResultExt, tracking::status::Status}; + +const TIME_BETWEEN_TRACKS: Duration = Duration::from_secs(60); + +pub async fn track_server_status(ctx: Context) { + loop { + tracing::info!("Collecting status information"); + // Fetch status via rcon and if successful, push it to the influx db + if let Some(status) = Status::get_from_rcon(&ctx) + .await + .log_warn("fetching status via rcon") + { + status + .send_to_influx_db(&ctx) + .await + .log_warn("sending status to influx endpoint"); + } + tokio::time::sleep(TIME_BETWEEN_TRACKS).await; + } +} diff --git a/src/tracking/status.rs b/src/tracking/status.rs new file mode 100644 index 0000000..dbcbc92 --- /dev/null +++ b/src/tracking/status.rs @@ -0,0 +1,40 @@ +use influx_db_client::Point; +use serenity::client::Context; + +mod player_info; +mod tps; + +use crate::{error::Result, influx::InfluxDb, rcon::RconHandle}; + +use self::{ + player_info::{ListCmd, PlayerInfo}, + tps::{Tps, TpsCmd}, +}; + +pub struct Status { + pub tps: Tps, + pub player_info: PlayerInfo, +} + +impl Status { + pub async fn get_from_rcon(ctx: &Context) -> Result { + let tps = RconHandle::cmd(&TpsCmd, ctx).await?; + let player_info = RconHandle::cmd(&ListCmd, ctx).await?; + Ok(Status { tps, player_info }) + } + + pub async fn send_to_influx_db(&self, ctx: &Context) -> Result { + let mut points = vec![]; + let status = Point::new("status") + .add_field("tps", self.tps.min1) + // Safe, since we started with u32 + .add_field("player_count", self.player_info.amount_online as i64); + points.push(status); + for player in &self.player_info.players { + let point = Point::new("activity").add_tag("user_id", player.uuid.clone()); + points.push(point); + } + + InfluxDb::write_points(points.into_iter(), ctx).await + } +} diff --git a/src/tracking/status/player_info.rs b/src/tracking/status/player_info.rs new file mode 100644 index 0000000..62c9833 --- /dev/null +++ b/src/tracking/status/player_info.rs @@ -0,0 +1,68 @@ +use nom::{ + bytes::complete::{is_not, tag}, + character::complete::{self, char}, + combinator::{all_consuming, map}, + multi::separated_list0, + sequence::{delimited, tuple}, + IResult, +}; + +use crate::{ + error::{Error, Result}, + rcon::RconCommand, +}; + +pub struct ListCmd; + +pub struct PlayerInfo { + pub amount_online: u32, + pub players: Vec, +} + +pub struct Player { + pub uuid: String, +} + +impl RconCommand for ListCmd { + type Output = PlayerInfo; + + fn as_string(&self) -> String { + "minecraft:list uuids".into() + } + + /// Expected: + /// ``` + /// There are 6 of a max of 20 players online: SomeName (00000000-0000-0000-0000-000000000000), ... + /// ``` + fn parse(&self, raw: String) -> Result { + parse_list_result(&raw) + .map(|(_, info)| info) + .map_err(|why| Error::RconListCmd(why.to_owned())) + } +} + +fn parse_list_result(inp: &str) -> IResult<&str, PlayerInfo> { + all_consuming(map( + tuple(( + tag("There are "), + complete::u32, + tag(" of a max of "), + complete::u32, + tag(" players online: "), + separated_list0(tag(", "), parse_player), + )), + |(_, num, _, _max, _, players)| PlayerInfo { + amount_online: num, + players, + }, + ))(inp) +} + +fn parse_player(inp: &str) -> IResult<&str, Player> { + map( + tuple((is_not(" "), delimited(char('('), is_not(")"), char(')')))), + |(_name, uuid): (&str, &str)| Player { + uuid: uuid.to_owned(), + }, + )(inp) +} diff --git a/src/tracking/status/tps.rs b/src/tracking/status/tps.rs new file mode 100644 index 0000000..b28583d --- /dev/null +++ b/src/tracking/status/tps.rs @@ -0,0 +1,58 @@ +use nom::{ + bytes::complete::tag, + combinator::{all_consuming, map, opt}, + number::complete::double, + sequence::{preceded, tuple}, + IResult, +}; + +use crate::{ + error::{Error, Result}, + rcon::RconCommand, +}; + +pub struct TpsCmd; + +pub struct Tps { + pub min1: f64, + pub min5: f64, + pub min15: f64, +} + +impl RconCommand for TpsCmd { + type Output = Tps; + + fn as_string(&self) -> String { + "spigot:tps".into() + } + + /// Expected: + /// ``` + /// TPS from last 1m, 5m, 15m: 20.0, 20.0, 20.0 + /// ``` + fn parse(&self, raw: String) -> Result { + parse_tps_result(&raw) + .map(|(_, tps)| tps) + .map_err(|why| Error::RconTpsCmd(why.to_owned())) + } +} + +fn parse_tps_result(inp: &str) -> IResult<&str, Tps> { + let start = tag("§6TPS from last 1m, 5m, 15m: §a"); + all_consuming(map( + tuple(( + start, + tps_value, + tag(", §a"), + tps_value, + tag(", §a"), + tps_value, + tag("\n"), + )), + |(_, min1, _, min5, _, min15, _)| Tps { min1, min5, min15 }, + ))(inp) +} + +fn tps_value(inp: &str) -> IResult<&str, f64> { + preceded(opt(tag("*")), double)(inp) +}