diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..bfcf7218a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.swo +*.swp +*~ +.env +.idea/ +__pycache__/ +secrets.toml +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..959c7d0729 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1608 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliri_braid" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4961918d91b2182cf3adc6e3cd8d6f44452e0f42db367691447af31afdd1ad7d" +dependencies = [ + "aliri_braid_impl", +] + +[[package]] +name = "aliri_braid_impl" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e21d6298c933984056b381a70c7248a97370b76dfdbc95dc3da0fcbcd994342" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anyhow" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" + +[[package]] +name = "async-trait" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "winapi", +] + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "easyenv" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8044ea5153408cb704ee2a9dafbd33aa5d14c8a34c8405910d90e8c8ef45944" +dependencies = [ + "dotenv", + "env_logger", + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" + +[[package]] +name = "futures-executor" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" + +[[package]] +name = "futures-macro" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" + +[[package]] +name = "futures-task" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" + +[[package]] +name = "futures-util" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +dependencies = [ + "autocfg", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "h2" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" + +[[package]] +name = "httpdate" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "hyper" +version = "0.14.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "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 = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" +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.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core", +] + +[[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 = [ + "aho-corasick", + "memchr", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rpassword" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.0-beta2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f85530c337a44ed700862acf71f9013cd62600f79de996d3c3ea87773df910d5" +dependencies = [ + "log", + "ring", + "sct 0.7.0", + "webpki 0.22.0", +] + +[[package]] +name = "rustls-native-certs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +dependencies = [ + "openssl-probe", + "rustls 0.19.1", + "schannel", + "security-framework", +] + +[[package]] +name = "ryu" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0421d4f173fab82d72d6babf36d57fae38b994ca5c2d78e704260ba6d12118b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "slab" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" + +[[package]] +name = "socket2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "syn" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" +dependencies = [ + "proc-macro2", + "quote", + "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", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls 0.19.1", + "tokio", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" +dependencies = [ + "futures-util", + "log", + "pin-project", + "rustls 0.19.1", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki 0.21.4", + "webpki-roots", +] + +[[package]] +name = "tokio-util" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "tungstenite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand", + "rustls 0.19.1", + "rustls-native-certs", + "sha-1", + "thiserror", + "url", + "utf-8", + "webpki 0.21.4", +] + +[[package]] +name = "twitch-tts-desktop-client" +version = "0.0.0" +dependencies = [ + "anyhow", + "easyenv", + "futures-util", + "log", + "rand", + "reqwest", + "rpassword", + "rustls 0.20.0-beta2", + "serde", + "serde_derive", + "tokio", + "tokio-tungstenite", + "toml", + "twitch_api2", + "twitch_oauth2", +] + +[[package]] +name = "twitch_api2" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003e4481fa1bc7cf6587dd212a4ce5f3b69dee893c9f7fdf8a0ed7149f470f1b" +dependencies = [ + "aliri_braid", + "async-trait", + "chrono", + "displaydoc", + "futures", + "http", + "once_cell", + "reqwest", + "serde", + "serde_derive", + "serde_json", + "serde_path_to_error", + "serde_repr", + "thiserror", + "twitch_oauth2", + "typed-builder", + "url", + "version_check", +] + +[[package]] +name = "twitch_oauth2" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3ac686c0fd364beafa745e4471c602d945ff5e1962e36e27c38ad92660e929" +dependencies = [ + "aliri_braid", + "async-trait", + "base64", + "displaydoc", + "http", + "once_cell", + "rand", + "reqwest", + "serde", + "serde_json", + "thiserror", + "url", + "version_check", +] + +[[package]] +name = "typed-builder" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a46ee5bd706ff79131be9c94e7edcb82b703c487766a114434e5790361cf08c5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "unicode-bidi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki 0.21.4", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..11846476ba --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "twitch-tts-desktop-client" +version = "0.0.0" +edition = "2018" +authors = [ + "Brandon Thomas ", + "Brandon Thomas " +] + +[[bin]] +name = "twitch-tts-desktop-client" +path = "src/main.rs" + +[dependencies] +# Async runtime, Futures, etc. +tokio = { version = "1.10.0", features = ["rt-multi-thread", "macros", "time"] } +futures-util = "0.3.17" + +# Crypto +rustls = "0.20.0-beta2" + +# Websockets +tokio-tungstenite = { version = "0.15.0", features = ["rustls", "rustls-tls"] } + +# HTTP +reqwest = { version = "0.11.4" } + +# Serialization +serde = "1.0.130" +serde_derive = "1.0.130" +toml = "0.5.8" + +# Twitch +# (This is a fairly mature and robust Twitch chat library) +#twitchchat = { version = "0.14.8", features = ["tokio", "tokio-util", "async", "webpki-roots", "tokio-rustls"] } +# Twitch Helix etc. API, but no PubSub just yet +#twitch_api2 = "0.4.1" +#twitch_oauth2 = "0.4.1" +#twitch_api2 = "0.5.0" +#twitch_oauth2 = "0.5.0" +# NB "unsupported" pubsub features may disappear from the Twitch API at any time. It's an unofficial API. +twitch_api2 = { version = "0.6.0-rc.2", features = ["helix", "reqwest_client", "pubsub", "chrono", "unsupported"] } +twitch_oauth2 = "0.6.0-rc.2" + +# Utils +anyhow = "1.0.44" +easyenv = "0.0.6" +log = "0.4.14" +rand = "0.8.4" + +# Temporary +rpassword = "5.0.1" + +#[build-dependencies] + diff --git a/README.md b/README.md new file mode 100644 index 0000000000..743134816e --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +twitch-tts-desktop-client +========================= +Name TBD + + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000000..0a41932192 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,282 @@ +// TODO: These are temporary - +#![allow(unused_imports)] +#![allow(unused_mut)] +#![allow(dead_code)] + +use crate::twitch::secrets::TwitchSecrets; +use crate::twitch::websocket_client::{TwitchWebsocketClient, PollingTwitchWebsocketClient}; +use crate::util::anyhow_result::AnyhowResult; +use futures_util::{SinkExt, StreamExt}; +use log::info; +use reqwest::Url; +use std::time::Duration; +use tokio_tungstenite::{connect_async, tungstenite::{Error as TungsteniteError, Result as TungsteniteResult}, connect_async_with_config}; +use twitch_api2::TwitchClient; +use twitch_api2::helix::channels::GetChannelInformationRequest; +use twitch_api2::pubsub::Topic; +use twitch_api2::pubsub; +use twitch_oauth2::{AppAccessToken, Scope, TwitchToken, tokens::errors::AppAccessTokenError, ClientId, ClientSecret}; +use twitch_oauth2::tokens::UserTokenBuilder; +use twitch_api2::helix::users::GetUsersRequest; +use twitch_api2::types::Nickname; +use crate::twitch::client::TwitchClientWrapper; + +pub mod twitch; +pub mod util; + +const DEFAULT_LOG : &'static str = "debug,rustls=warn,tungstenite=warn"; + +const TEST_CHANNEL_ID : u32 = 650154491; // Test channel + +fn main() -> AnyhowResult<()> { + easyenv::init_all_with_default_logging(Some(DEFAULT_LOG)); + + info!("Starting"); + + use std::error::Error; + if let Err(err) = run() { + println!("Error: {:?}", err); + //let mut e: &'_ dyn Error = err.as_ref(); + //while let Some(cause) = e.source() { + // println!("Caused by: {}", cause); + // e = cause; + //} + } + + Ok(()) +} + +#[tokio::main] +async fn run() -> AnyhowResult<()> { + // ==================== APPLICATION ACCESS ==================== + + let secrets = TwitchSecrets::from_file("secrets.toml")?; + let client_id = ClientId::new(&secrets.app_client_id); + let client_secret = ClientSecret::new(&secrets.app_client_secret); + + info!("Getting app access token..."); + + //let scopes = Scope::all(); + let scopes = vec![ + Scope::BitsRead, + Scope::UserReadEmail, + ]; + + let mut twitch_client = TwitchClientWrapper::new(client_id.clone(), client_secret.clone()); + twitch_client.request_access_token(scopes).await?; + + info!("Getting user id ..."); + + //let user_id = twitch_client.get_user_id_from_username("testytest512").await?; + let user_id = twitch_client.get_user_id_from_username("vocodes").await?; + + info!("User ID: {}", user_id); + + //std::thread::sleep(Duration::from_secs(5000)); + + + // ==================== OAUTH FLOW ==================== + + println!("Oauth flow..."); + + let redirect_url = twitch_oauth2::url::Url::parse("http://localhost/test")?; + let mut builder = UserTokenBuilder::new(client_id, client_secret, redirect_url) + .set_scopes(Scope::all()) + .force_verify(true); + + //builder.add_scope(Scope::BitsRead); + + let (url, _csrf_token) = builder.generate_url(); + + println!("Go to this page: {}", url); + + //std::thread::sleep(Duration::from_secs(3000)); + + + // ==================== PUBSUB SUBSCRIPTION AND MAIN LOOP ==================== + + let mut client = PollingTwitchWebsocketClient::new()?; + + println!("Connecting PubSub..."); + client.connect().await?; + + println!("Connected"); + + //println!("Starting polling thread..."); + //client.start_ping_thread().await; + + println!("Sending PING..."); + + client.send_ping().await?; + + println!("Try read next..."); + let r = client.try_next().await?; + println!("Result: {:?}", r); + + let input = rpassword::prompt_password_stdout( + "Paste in the resulting adress after authenticating (input hidden): ", + )?; + + + let u = twitch_oauth2::url::Url::parse(&input)?; + + let map: std::collections::HashMap<_, _> = u.query_pairs().collect(); + + let user_token = match (map.get("state"), map.get("code")) { + (Some(state), Some(code)) => { + let token = builder + .get_user_token( + &reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build()?, + state, + code, + ) + .await?; + println!("Got token: {:?}", token); + + token + } + _ => match (map.get("error"), map.get("error_description")) { + (std::option::Option::Some(error), std::option::Option::Some(error_description)) => { + anyhow::bail!( + "twitch errored with error: {} - {}", + error, + error_description + ); + } + _ => anyhow::bail!("invalid url passed"), + }, + }; + + let auth_token = user_token.access_token.as_str(); + + println!("Begin LISTEN..."); + + let bit_topic = pubsub::channel_bits::ChannelBitsEventsV2 { + channel_id: user_id, + }.into_topic(); + + let topics = [bit_topic]; + + client.listen(&auth_token, &topics).await?; + + println!("Try read next..."); + let r = client.try_next().await?; + println!("Result: {:?}", r); + + /* + Result: Some(Message { data: ChannelBitsEventsV2 { topic: ChannelBitsEventsV2 { + channel_id: 652567283 }, reply: BitsEvent { data: BitsEventData { badge_entitlement: + Some(BadgeEntitlement { new_version: 100, previous_version: 1 }), bits_used: 100, + channel_id: "652567283", channel_name: "vocodes", chat_message: "Cheer100 testing the cheer", + context: Cheer, is_anonymous: false, time: "2021-09-27T04:30:53.627717085Z", + total_bits_used: 101, user_id: "650154491", user_name: "testytest512" }, + message_id: "793f745e-8f3e-5f71-bc41-4808c4b49a53", version: "1.0", is_anonymous: false } } }) + */ + + + + println!("Try read next..."); + let r = client.try_next().await?; + println!("Result: {:?}", r); + + println!("Sleep..."); + std::thread::sleep(Duration::from_millis(30000)); + + //let text = msg.into_text()?; + //println!("Text: {}", text); + + Ok(()) +} + + +async fn get_case_count() -> TungsteniteResult { + let (mut socket, _) = connect_async( + Url::parse("ws://localhost:9001/getCaseCount").expect("Can't connect to case count URL"), + ) + .await?; + let msg = socket.next().await.expect("Can't fetch case count")?; + socket.close(None).await?; + Ok(msg.into_text()?.parse::().expect("Can't parse case count")) +} + + +/*let client_id = ClientId::new(&secrets.app_client_id); +let client_secret = ClientSecret::new(&secrets.app_client_secret); + +let client: TwitchClient = TwitchClient::default(); +let token = AppAccessToken::get_app_access_token( + &client, + client_id, + client_secret, + Scope::all(), +).await?; + +let req = GetChannelInformationRequest::builder() + //.broadcaster_id("27620241") + .broadcaster_id("650154491") + .build(); + +println!( + "{:?}", + &client.helix.req_get(req, &token).await?.data.unwrap().title +);*/ + + +// Want this: +// twitch_api2::pubsub::channel_bits::BitsEventData + + + +/*// We want to subscribe to moderator actions on channel with id 1234 +// as if we were a user with id 4321 that is moderator on the channel. +let chat_mod_actions = pubsub::moderation::ChatModeratorActions { + user_id: 4321, + channel_id: 1234, +} + .into_topic();*/ + +// Listen to follows as well +/*let follows = pubsub::following::Following { channel_id: 1234 }.into_topic(); +let topics = [follows]; +// Create the topic command to send to twitch +let command = pubsub::listen_command( + &topics, + "authtoken", + "random nonce string", +)?; + +println!("Pubsub command: {:?}", command);*/ + +// Send the message with your favorite websocket client +//send_command(command).unwrap(); +// To parse the websocket messages, use pubsub::Response::parse + + +/*println!("Connected"); +let mut ws_client = TwitchWebsocketClient::new()?; +ws_client.connect().await?; + +ws_client.send_ping().await?; + +println!("success ping");*/ + +/* + + // We want to subscribe to moderator actions on channel with id 1234 + // as if we were a user with id 4321 that is moderator on the channel. + let chat_mod_actions = pubsub::moderation::ChatModeratorActions { + user_id: 4321, + channel_id: 1234, + }.into_topic(); + + // Listen to follows as well + let follows = pubsub::following::Following { channel_id: 1234 }.into_topic(); + */ + +//match UserToken::from_existing(reqwest_http_client, token, None, None).await { +// Ok(t) => println!("user_token: {}", t.token().secret()), +// Err(e) => panic!("got error: {}", e), +//} + diff --git a/src/twitch/client.rs b/src/twitch/client.rs new file mode 100644 index 0000000000..072d0ead60 --- /dev/null +++ b/src/twitch/client.rs @@ -0,0 +1,65 @@ +//! The Helix client is independent of PubSub +//! This just wraps `twitch_api2` for more convenience. + +use crate::util::anyhow_result::AnyhowResult; +use twitch_api2::helix::users::GetUsersRequest; +use anyhow::anyhow; +use twitch_api2::TwitchClient; +use twitch_oauth2::{ClientSecret, ClientId, AccessToken, AppAccessToken, Scope}; +use twitch_api2::types::{Nickname, UserId}; + +pub struct TwitchClientWrapper<'a> { + client_id: ClientId, + client_secret: ClientSecret, + twitch_client: TwitchClient<'a, reqwest::Client>, + access_token: Option, +} + +impl <'a> TwitchClientWrapper <'a> { + pub fn new(client_id: ClientId, client_secret: ClientSecret) -> Self { + Self { + client_id, + client_secret, + twitch_client: TwitchClient::default(), + access_token: None, + } + } + + pub async fn request_access_token(&mut self, scopes: Vec) -> AnyhowResult<()> { + self.get_access_token(scopes).await?; + Ok(()) + } + + async fn get_access_token(&mut self, scopes: Vec) -> AnyhowResult<()> { + let mut http_client = reqwest::Client::new(); + let access_token = twitch_oauth2::AppAccessToken::get_app_access_token( + &http_client, + self.client_id.clone(), + self.client_secret.clone(), + scopes, + ).await?; + + self.access_token = Some(access_token); + Ok(()) + } + + /// We frequently need to look up IDs from usernames. + pub async fn get_user_id_from_username(&self, username: &str) -> AnyhowResult { + let access_token = match self.access_token { + None => return Err(anyhow!("no access token has been granted")), + Some(ref token) => token, + }; + + let req = GetUsersRequest::builder() + .login(vec![Nickname::new(username)]) + .build(); + + let response = &self.twitch_client.helix.req_get(req, access_token) + .await?; + + match response.data.get(0) { + None => Err(anyhow!("No such user: `{}`.", username)), + Some(user) => Ok(user.id.as_str().parse::()?), + } + } +} diff --git a/src/twitch/mod.rs b/src/twitch/mod.rs new file mode 100644 index 0000000000..6f58dedfeb --- /dev/null +++ b/src/twitch/mod.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod secrets; +pub mod twitch_client_old; +pub mod websocket_client; \ No newline at end of file diff --git a/src/twitch/secrets.rs b/src/twitch/secrets.rs new file mode 100644 index 0000000000..539a6507b0 --- /dev/null +++ b/src/twitch/secrets.rs @@ -0,0 +1,43 @@ +use crate::util::anyhow_result::AnyhowResult; + +// TODO: Not sure I need all of these. The less I need to keep, the better. +// Oauth delegation in particular seems not necessary. +#[derive(Clone, serde::Deserialize)] +pub struct TwitchSecrets { + pub username: String, + pub stream_key: String, + pub app_client_id: String, + pub app_client_secret: String, +} + +impl TwitchSecrets { + pub fn new( + username: &str, + stream_key: &str, + app_client_id: &str, + app_client_secret: &str, + ) -> Self { + Self { + username: username.to_string(), + stream_key: stream_key.to_string(), + app_client_id: app_client_id.to_string(), + app_client_secret: app_client_secret.to_string(), + } + } + + pub fn from_env() -> AnyhowResult { + Ok(Self::new( + &easyenv::get_env_string_required("TWITCH_USERNAME")?, + &easyenv::get_env_string_required("TWITCH_STREAM_KEY")?, + &easyenv::get_env_string_required("TWITCH_APP_CLIENT_ID")?, + &easyenv::get_env_string_required("TWITCH_APP_CLIENT_SECRET")?, + )) + } + + pub fn from_file(filename: &str) -> AnyhowResult { + let contents = std::fs::read_to_string(filename)?; + let secrets = toml::from_str(&contents)?; + Ok(secrets) + } +} + diff --git a/src/twitch/twitch_client_old.rs b/src/twitch/twitch_client_old.rs new file mode 100644 index 0000000000..95054c57cf --- /dev/null +++ b/src/twitch/twitch_client_old.rs @@ -0,0 +1,172 @@ +/*use crate::twitch::secrets::TwitchSecrets; +use crate::util::anyhow_result::AnyhowResult; +use log::debug; +use log::info; +use log::warn; +use std::time::Duration; +use twitchchat::messages::Commands::*; +use twitchchat::messages::Join; +use twitchchat::messages::Privmsg; +use twitchchat::{AsyncRunner, Status, UserConfig}; + +pub struct TwitchClient2 { + secrets: TwitchSecrets, + channels_to_join: Vec, + + /// This manages our connection. + /// It's meant to be absent when not connected. + /// TODO: Probably need to reset this when we disconnect. + runner: Option, +} + +impl TwitchClient2 { + + pub fn new(secrets: &TwitchSecrets) -> Self + { + Self { + secrets: secrets.clone(), + //channels_to_join: secrets.watch_channels.clone(), // TODO: Configure these elsewhere. + channels_to_join: vec![], + runner: None, + } + } + + pub async fn connect(&mut self) -> AnyhowResult<()> { + let user_config = get_user_config(&self.secrets)?; + let connector = twitchchat::connector::tokio::Connector::twitch()?; + + info!("Connecting to Twitch..."); + let mut runner = AsyncRunner::connect(connector, &user_config).await?; + + info!("Connected. Our Twitch identity: {:#?}", runner.identity); + + for channel in self.channels_to_join.iter() { + info!("Attempting to join channel '{}'", channel); + let _ = runner.join(&channel).await?; + + info!("Joined channel '{}'!", channel); + } + + self.runner = Some(runner); + + Ok(()) + } + + pub async fn main_loop(&mut self) { + loop { + let mut runner = match &mut self.runner { + Some(runner) => runner, + None => { + info!("Connecting to Twitch..."); + match self.connect().await { + Ok(_) => {}, + Err(e) => { + warn!("Failure to connect to Twitch: {:?}", e); + std::thread::sleep(Duration::from_secs(5)); + }, + } + continue; + }, + }; + + let next_message = match runner.next_message().await { + Ok(msg) => msg, + Err(e) => { + warn!("Error getting message from Twitch: {:?}", e); + std::thread::sleep(Duration::from_secs(5)); + continue; + }, + }; + + match next_message { + Status::Message(message) => { + // NB: Handles across all channels (and notifications from Twitch) + match self.handle_message(message).await { + Ok(_) => {}, + Err(e) => { + warn!("Error handling most recent message: {:?}", e); + } + } + }, + Status::Quit => { + // we signaled a quit + warn!("Status: Quit."); + }, + Status::Eof => { + // the connection closed normally + warn!("Status: EOF."); + }, + } + } + } + + async fn handle_message<'a>( + &mut self, + message: twitchchat::messages::Commands<'a> + ) -> AnyhowResult<()> { + match message { + // These happen when you initially connect + IrcReady(_) => {} + Ready(_) => {} + Cap(_) => {} + + // This is the one users send to channels + Privmsg(msg) => self.handle_privmsg(&msg).await?, + + // This is when a user joins the channel + // NB: These messages are delayed about 15-30 seconds. + // Doesn't appear to work...? + Join(msg) => info!("Join: {:?}", msg), + + // This should happen on subscription, etc. + // Doesn't appear to work...? + UserNotice(msg) => info!("UserNotice: {:?}", msg), + + // This should happen when a user joins, etc. + // Doesn't appear to work...? + // https://dev.twitch.tv/docs/irc/tags#userstate-twitch-tags + UserState(msg) => info!("UserState: {:?}", msg), + + // and a bunch of other messages you may be interested in + ClearChat(msg) => debug!("ClearChat: {:?}", msg), + ClearMsg(msg) => debug!("ClearMsg: {:?}", msg), + GlobalUserState(msg) => debug!("GlobalUserState: {:?}", msg), + HostTarget(msg) => debug!("HostTarget: {:?}", msg), + Notice(msg) => debug!("Notice: {:?}", msg), + Part(msg) => debug!("Part: {:?}", msg), + Ping(_) => {} + Pong(_) => {} + Reconnect(_) => {} + RoomState(msg) => debug!("RoomState: {:?}", msg), + Whisper(msg) => debug!("Whisper: {:?}", msg), + + // This one is special, if twitch adds any new message + // types, this will catch it until future releases of + // this crate add them. + Raw(msg) => debug!("Raw: {:?}", msg), + + _ => {} + } + + Ok(()) + } + + /// Handle user messages. + /// The "privmsg" type is for normal in-channel messages. + async fn handle_privmsg<'a>(&mut self, message: &Privmsg<'a>) -> AnyhowResult<()> { + info!("[{}] {}: {}", message.channel(), message.name(), message.data()); + debug!("Full Message: {:?}", message); + + Ok(()) + } +} + +pub fn get_user_config(twitch_secrets: &TwitchSecrets) -> AnyhowResult { + let config = UserConfig::builder() + .name(twitch_secrets.username.to_string()) + .token(twitch_secrets.oauth_access_token.to_string()) + .enable_all_capabilities() + .build()?; + Ok(config) +} +*/ \ No newline at end of file diff --git a/src/twitch/websocket_client.rs b/src/twitch/websocket_client.rs new file mode 100644 index 0000000000..486bd68800 --- /dev/null +++ b/src/twitch/websocket_client.rs @@ -0,0 +1,273 @@ +use anyhow::anyhow; +use anyhow::bail; +use crate::util::anyhow_result::AnyhowResult; +use futures_util::{SinkExt, StreamExt, TryStreamExt}; +use log::debug; +use reqwest::Url; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, PoisonError, MutexGuard}; +use std::time::Duration; +use tokio::net::TcpStream; +use tokio; +use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::{connect_async, WebSocketStream, MaybeTlsStream}; +use twitch_api2::pubsub::{Response, TopicData, Topic, Topics}; +use twitch_api2::pubsub; +use crate::util::random_nonce::random_nonce; + +// Reference javascript Twitch Websocket client: +// https://github.com/twitchdev/pubsub-javascript-sample/blob/main/main.js + +const WEBSOCKET_GATEWAY : &'static str = "wss://pubsub-edge.twitch.tv"; + +const PING_COMMAND : &'static str = "{ \"type\": \"PING\" }"; +const PING_SECONDS : u64 = 1 * 60; // Twitch calls for every 5 minutes, we'll do every 1 minute. + +pub struct PollingTwitchWebsocketClient { + client: Arc>, + is_polling: AtomicBool, +} + +impl PollingTwitchWebsocketClient { + pub fn new() -> AnyhowResult { + Ok(Self { + client: Arc::new(Mutex::new(TwitchWebsocketClient::new()?)), + is_polling: AtomicBool::new(false), + }) + } + + pub async fn connect(&mut self) -> AnyhowResult<()> { + match self.client.lock() { + Err(e) => Err(anyhow!("unlock client err: {:?}", e)), + Ok(mut client) => { + client.connect().await + } + } + } + + pub async fn listen<'t>(&mut self, auth_token: &str, topics: &'t [Topics]) -> AnyhowResult<()> { + match self.client.lock() { + Err(e) => Err(anyhow!("unlock client err: {:?}", e)), + Ok(mut client) => { + client.listen(auth_token, topics).await + } + } + } + + pub async fn send_ping(&mut self) -> AnyhowResult<()> { + match self.client.lock() { + Err(e) => Err(anyhow!("unlock client err: {:?}", e)), + Ok(mut client) => { + client.send_ping().await + } + } + } + + /// Start a pinging background thread. + /// Per Twitch PubSub docs, + /// To keep the server from closing the connection, clients must send a PING command + /// at least once every 5 minutes. If a client does not receive a PONG message within + /// 10 seconds of issuing a PING command, it should reconnect to the server. + pub async fn start_ping_thread(&self) { + let client = self.client.clone(); + tokio::spawn(async move { + loop { + match client.lock() { + Ok(mut c) => { + // TODO: Future isn't Send + //let _r = c.send_ping().await; + // TODO: Need to read from all call sites due to ordering. + //c.read_response(); + } + Err(_) => { + // TODO + } + } + tokio::time::sleep(Duration::from_secs(PING_SECONDS)).await; + } + }); + } + + pub async fn next(&mut self) -> AnyhowResult { + match self.client.lock() { + Err(e) => Err(anyhow!("unlock client err: {:?}", e)), + Ok(mut client) => { + client.next().await + } + } + } + + pub async fn try_next(&mut self) -> AnyhowResult> { + match self.client.lock() { + Err(e) => Err(anyhow!("unlock client err: {:?}", e)), + Ok(mut client) => { + client.try_next().await + } + } + } + + /*async fn start_polling(&self) { + if self.is_polling.load(Ordering::Relaxed) { + return; + } + + tokio::spawn(async move { + self.is_polling.store(true, Ordering::Relaxed); + + let command = pubsub::channel_sub_gifts( + &topics, + "authtoken", + "random nonce string", + )?; + + }); + / + }*/ + + + /*let message = Message::Text(PING_COMMAND.to_string()); + socket.send(message).await?; + match self.read_response().await { + Err(e) => println!("Error getting PONG: {:?}", e), + Ok(typed_response) => { + match typed_response { + Response::Pong => { println!("Pong") } + Response::Reconnect => { println!("Reconnect") } + Response::Response(_0) => { println!("Sub/Unsub (pubsub?)") } + Response::Message { data } => { + match data { + TopicData::ChannelCheerEventsPublicV1 { topic, reply } => {} + TopicData::ChannelSubGiftsV1 { topic, reply } => {} + TopicData::ChannelSubscribeEventsV1 { topic, reply } => {} + TopicData::CommunityPointsChannelV1 { topic, reply } => {} + TopicData::Following { topic, reply} => {} + // Not interested in these events + TopicData::AutoModQueue { .. } => {} + TopicData::ChannelBitsBadgeUnlocks { .. } => {} + TopicData::ChannelBitsEventsV2 { .. } => {} + TopicData::ChannelPointsChannelV1 { .. } => {} + TopicData::ChatModeratorActions { .. } => {} + TopicData::HypeTrainEventsV1 { .. } => {} + TopicData::HypeTrainEventsV1Rewards { .. } => {} + TopicData::Raid { .. } => {} + TopicData::UserModerationNotifications { .. } => {} + TopicData::VideoPlayback { .. } => {} + TopicData::VideoPlaybackById { .. } => {} + } + } + } + } + }*/ + +} + +pub struct TwitchWebsocketClient { + socket: Option>>, +} + +impl TwitchWebsocketClient { + pub fn new() -> AnyhowResult { + Ok(Self { + socket: None, + }) + } + + pub async fn connect(&mut self) -> AnyhowResult<()> { + let url = Url::parse(WEBSOCKET_GATEWAY)?; + let (mut socket, _response) = + connect_async(url).await?; + self.socket = Some(socket); + Ok(()) + } + + pub async fn close(&mut self) -> AnyhowResult<()> { + let mut socket = match self.socket { + None => return Ok(()), // Already closed. + Some(ref mut socket) => socket, + }; + + socket.close(None).await?; + self.socket = None; + + Ok(()) + } + + pub async fn listen<'t>(&mut self, auth_token: &str, topics: &'t [Topics]) -> AnyhowResult<()> { + let mut socket = match self.socket { + None => return Err(anyhow!("not connected")), + Some(ref mut socket) => socket, + }; + + let nonce = random_nonce(128); + + let command = pubsub::listen_command( + topics, + auth_token, + nonce.as_ref(), + )?; + + debug!("Sending LISTEN"); + let message = Message::Text(command); + socket.send(message).await?; + + Ok(()) + } + + pub fn is_connected(&self) -> bool { + // TODO: This probably isn't accurate + self.socket.is_some() + } + + pub async fn send_ping(&mut self) -> AnyhowResult<()> { + let mut socket = match self.socket { + None => return Err(anyhow!("not connected")), + Some(ref mut socket) => socket, + }; + + debug!("Sending PING"); + let message = Message::Text(PING_COMMAND.to_string()); + socket.send(message).await?; + + // TODO: Need to read from all call sites due to ordering. + //match self.read_response().await { + // Err(e) => Err(e), + // Ok(Response::Pong) => Ok(()), + // _ => Err(bail!("wrong response type")), + //} + Ok(()) + } + + pub async fn next(&mut self) -> AnyhowResult { + let socket = match self.socket { + None => return Err(anyhow!("not connected")), + Some(ref mut s) => s, + }; + + match socket.next().await { + None => Err(anyhow!("nothing to read")), + Some(Err(e)) => Err(anyhow!("websocket error: {:?}", e)), + Some(Ok(response)) => { + let text = response.to_text()?; + let typed_response = Response::parse(text)?; + Ok(typed_response) + }, + } + } + + pub async fn try_next(&mut self) -> AnyhowResult> { + let socket = match self.socket { + None => return Err(anyhow!("not connected")), + Some(ref mut s) => s, + }; + + match socket.try_next().await { + Err(e) => Err(anyhow!("websocket error: {:?}", e)), + Ok(None) => Ok(None), + Ok(Some(response)) => { + let text = response.to_text()?; + let typed_response = Response::parse(text)?; + Ok(Some(typed_response)) + }, + } + } +} diff --git a/src/util/anyhow_result.rs b/src/util/anyhow_result.rs new file mode 100644 index 0000000000..cf8fba51aa --- /dev/null +++ b/src/util/anyhow_result.rs @@ -0,0 +1,2 @@ + +pub type AnyhowResult = anyhow::Result; \ No newline at end of file diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000000..145df6fe0c --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,3 @@ +pub mod anyhow_result; +pub mod random_nonce; + diff --git a/src/util/random_nonce.rs b/src/util/random_nonce.rs new file mode 100644 index 0000000000..5f9034d3aa --- /dev/null +++ b/src/util/random_nonce.rs @@ -0,0 +1,24 @@ +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; + +/// Returns a random nonce for Twitch +pub fn random_nonce(length: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} + +#[cfg(test)] +mod tests { + use crate::util::random_nonce::random_nonce; + + #[test] + fn random_nonce_length() { + assert_eq(1, random_nonce(1).len()); + assert_eq(10, random_nonce(10).len()); + assert_eq(15, random_nonce(15).len()); + assert_eq(64, random_nonce(64).len()); + } +} \ No newline at end of file