├── .github └── workflows │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── config.rs ├── error.rs ├── lib.rs ├── liveu.rs ├── liveu_monitor.rs ├── main.rs ├── nginx.rs └── twitch.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.* 7 | 8 | jobs: 9 | create-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: taiki-e/create-gh-release-action@v1 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | upload-assets: 18 | strategy: 19 | matrix: 20 | os: 21 | - ubuntu-latest 22 | - macos-latest 23 | - windows-latest 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: taiki-e/upload-rust-binary-action@v1 28 | with: 29 | bin: liveu_stats_bot 30 | tar: unix 31 | zip: windows 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | CARGO_PROFILE_RELEASE_LTO: true 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | config.json 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anyhow" 37 | version = "1.0.81" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" 40 | 41 | [[package]] 42 | name = "async-trait" 43 | version = "0.1.78" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" 46 | dependencies = [ 47 | "proc-macro2", 48 | "quote", 49 | "syn", 50 | ] 51 | 52 | [[package]] 53 | name = "autocfg" 54 | version = "1.1.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 57 | 58 | [[package]] 59 | name = "backtrace" 60 | version = "0.3.71" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 63 | dependencies = [ 64 | "addr2line", 65 | "cc", 66 | "cfg-if", 67 | "libc", 68 | "miniz_oxide", 69 | "object", 70 | "rustc-demangle", 71 | ] 72 | 73 | [[package]] 74 | name = "base64" 75 | version = "0.21.7" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 78 | 79 | [[package]] 80 | name = "bitflags" 81 | version = "1.3.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 84 | 85 | [[package]] 86 | name = "bitflags" 87 | version = "2.5.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 90 | 91 | [[package]] 92 | name = "bumpalo" 93 | version = "3.15.4" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" 96 | 97 | [[package]] 98 | name = "bytes" 99 | version = "1.6.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 102 | 103 | [[package]] 104 | name = "cc" 105 | version = "1.0.90" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" 108 | 109 | [[package]] 110 | name = "cfg-if" 111 | version = "1.0.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 114 | 115 | [[package]] 116 | name = "chrono" 117 | version = "0.4.35" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" 120 | dependencies = [ 121 | "android-tzdata", 122 | "iana-time-zone", 123 | "js-sys", 124 | "num-traits", 125 | "wasm-bindgen", 126 | "windows-targets 0.52.4", 127 | ] 128 | 129 | [[package]] 130 | name = "core-foundation" 131 | version = "0.9.4" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 134 | dependencies = [ 135 | "core-foundation-sys", 136 | "libc", 137 | ] 138 | 139 | [[package]] 140 | name = "core-foundation-sys" 141 | version = "0.8.6" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 144 | 145 | [[package]] 146 | name = "either" 147 | version = "1.10.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 150 | 151 | [[package]] 152 | name = "encoding_rs" 153 | version = "0.8.33" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 156 | dependencies = [ 157 | "cfg-if", 158 | ] 159 | 160 | [[package]] 161 | name = "enum_dispatch" 162 | version = "0.3.12" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" 165 | dependencies = [ 166 | "once_cell", 167 | "proc-macro2", 168 | "quote", 169 | "syn", 170 | ] 171 | 172 | [[package]] 173 | name = "equivalent" 174 | version = "1.0.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 177 | 178 | [[package]] 179 | name = "errno" 180 | version = "0.3.8" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 183 | dependencies = [ 184 | "libc", 185 | "windows-sys 0.52.0", 186 | ] 187 | 188 | [[package]] 189 | name = "fastrand" 190 | version = "2.0.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 193 | 194 | [[package]] 195 | name = "fnv" 196 | version = "1.0.7" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 199 | 200 | [[package]] 201 | name = "foreign-types" 202 | version = "0.3.2" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 205 | dependencies = [ 206 | "foreign-types-shared", 207 | ] 208 | 209 | [[package]] 210 | name = "foreign-types-shared" 211 | version = "0.1.1" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 214 | 215 | [[package]] 216 | name = "form_urlencoded" 217 | version = "1.2.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 220 | dependencies = [ 221 | "percent-encoding", 222 | ] 223 | 224 | [[package]] 225 | name = "futures-channel" 226 | version = "0.3.30" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 229 | dependencies = [ 230 | "futures-core", 231 | ] 232 | 233 | [[package]] 234 | name = "futures-core" 235 | version = "0.3.30" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 238 | 239 | [[package]] 240 | name = "futures-sink" 241 | version = "0.3.30" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 244 | 245 | [[package]] 246 | name = "futures-task" 247 | version = "0.3.30" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 250 | 251 | [[package]] 252 | name = "futures-util" 253 | version = "0.3.30" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 256 | dependencies = [ 257 | "futures-core", 258 | "futures-sink", 259 | "futures-task", 260 | "pin-project-lite", 261 | "pin-utils", 262 | "slab", 263 | ] 264 | 265 | [[package]] 266 | name = "getrandom" 267 | version = "0.2.12" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 270 | dependencies = [ 271 | "cfg-if", 272 | "libc", 273 | "wasi", 274 | ] 275 | 276 | [[package]] 277 | name = "gimli" 278 | version = "0.28.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 281 | 282 | [[package]] 283 | name = "h2" 284 | version = "0.4.3" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" 287 | dependencies = [ 288 | "bytes", 289 | "fnv", 290 | "futures-core", 291 | "futures-sink", 292 | "futures-util", 293 | "http", 294 | "indexmap", 295 | "slab", 296 | "tokio", 297 | "tokio-util 0.7.10", 298 | "tracing", 299 | ] 300 | 301 | [[package]] 302 | name = "hashbrown" 303 | version = "0.14.3" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 306 | 307 | [[package]] 308 | name = "hermit-abi" 309 | version = "0.3.9" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 312 | 313 | [[package]] 314 | name = "http" 315 | version = "1.1.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 318 | dependencies = [ 319 | "bytes", 320 | "fnv", 321 | "itoa", 322 | ] 323 | 324 | [[package]] 325 | name = "http-body" 326 | version = "1.0.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" 329 | dependencies = [ 330 | "bytes", 331 | "http", 332 | ] 333 | 334 | [[package]] 335 | name = "http-body-util" 336 | version = "0.1.1" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" 339 | dependencies = [ 340 | "bytes", 341 | "futures-core", 342 | "http", 343 | "http-body", 344 | "pin-project-lite", 345 | ] 346 | 347 | [[package]] 348 | name = "httparse" 349 | version = "1.8.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 352 | 353 | [[package]] 354 | name = "hyper" 355 | version = "1.2.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" 358 | dependencies = [ 359 | "bytes", 360 | "futures-channel", 361 | "futures-util", 362 | "h2", 363 | "http", 364 | "http-body", 365 | "httparse", 366 | "itoa", 367 | "pin-project-lite", 368 | "smallvec", 369 | "tokio", 370 | "want", 371 | ] 372 | 373 | [[package]] 374 | name = "hyper-tls" 375 | version = "0.6.0" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 378 | dependencies = [ 379 | "bytes", 380 | "http-body-util", 381 | "hyper", 382 | "hyper-util", 383 | "native-tls", 384 | "tokio", 385 | "tokio-native-tls", 386 | "tower-service", 387 | ] 388 | 389 | [[package]] 390 | name = "hyper-util" 391 | version = "0.1.3" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" 394 | dependencies = [ 395 | "bytes", 396 | "futures-channel", 397 | "futures-util", 398 | "http", 399 | "http-body", 400 | "hyper", 401 | "pin-project-lite", 402 | "socket2", 403 | "tokio", 404 | "tower", 405 | "tower-service", 406 | "tracing", 407 | ] 408 | 409 | [[package]] 410 | name = "iana-time-zone" 411 | version = "0.1.60" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 414 | dependencies = [ 415 | "android_system_properties", 416 | "core-foundation-sys", 417 | "iana-time-zone-haiku", 418 | "js-sys", 419 | "wasm-bindgen", 420 | "windows-core", 421 | ] 422 | 423 | [[package]] 424 | name = "iana-time-zone-haiku" 425 | version = "0.1.2" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 428 | dependencies = [ 429 | "cc", 430 | ] 431 | 432 | [[package]] 433 | name = "idna" 434 | version = "0.5.0" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 437 | dependencies = [ 438 | "unicode-bidi", 439 | "unicode-normalization", 440 | ] 441 | 442 | [[package]] 443 | name = "indexmap" 444 | version = "2.2.6" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 447 | dependencies = [ 448 | "equivalent", 449 | "hashbrown", 450 | ] 451 | 452 | [[package]] 453 | name = "ipnet" 454 | version = "2.9.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 457 | 458 | [[package]] 459 | name = "itertools" 460 | version = "0.10.5" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 463 | dependencies = [ 464 | "either", 465 | ] 466 | 467 | [[package]] 468 | name = "itoa" 469 | version = "1.0.10" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 472 | 473 | [[package]] 474 | name = "js-sys" 475 | version = "0.3.69" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 478 | dependencies = [ 479 | "wasm-bindgen", 480 | ] 481 | 482 | [[package]] 483 | name = "lazy_static" 484 | version = "1.4.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 487 | 488 | [[package]] 489 | name = "libc" 490 | version = "0.2.153" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 493 | 494 | [[package]] 495 | name = "linux-raw-sys" 496 | version = "0.4.13" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 499 | 500 | [[package]] 501 | name = "liveu_stats_bot" 502 | version = "0.6.0" 503 | dependencies = [ 504 | "anyhow", 505 | "quick-xml", 506 | "read_input", 507 | "reqwest", 508 | "serde", 509 | "serde_json", 510 | "thiserror", 511 | "tokio", 512 | "twitch-irc", 513 | "uuid", 514 | ] 515 | 516 | [[package]] 517 | name = "log" 518 | version = "0.4.21" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 521 | 522 | [[package]] 523 | name = "memchr" 524 | version = "2.7.1" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 527 | 528 | [[package]] 529 | name = "mime" 530 | version = "0.3.17" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 533 | 534 | [[package]] 535 | name = "miniz_oxide" 536 | version = "0.7.2" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 539 | dependencies = [ 540 | "adler", 541 | ] 542 | 543 | [[package]] 544 | name = "mio" 545 | version = "0.8.11" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 548 | dependencies = [ 549 | "libc", 550 | "wasi", 551 | "windows-sys 0.48.0", 552 | ] 553 | 554 | [[package]] 555 | name = "native-tls" 556 | version = "0.2.11" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 559 | dependencies = [ 560 | "lazy_static", 561 | "libc", 562 | "log", 563 | "openssl", 564 | "openssl-probe", 565 | "openssl-sys", 566 | "schannel", 567 | "security-framework", 568 | "security-framework-sys", 569 | "tempfile", 570 | ] 571 | 572 | [[package]] 573 | name = "num-traits" 574 | version = "0.2.18" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 577 | dependencies = [ 578 | "autocfg", 579 | ] 580 | 581 | [[package]] 582 | name = "num_cpus" 583 | version = "1.16.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 586 | dependencies = [ 587 | "hermit-abi", 588 | "libc", 589 | ] 590 | 591 | [[package]] 592 | name = "object" 593 | version = "0.32.2" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 596 | dependencies = [ 597 | "memchr", 598 | ] 599 | 600 | [[package]] 601 | name = "once_cell" 602 | version = "1.19.0" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 605 | 606 | [[package]] 607 | name = "openssl" 608 | version = "0.10.64" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" 611 | dependencies = [ 612 | "bitflags 2.5.0", 613 | "cfg-if", 614 | "foreign-types", 615 | "libc", 616 | "once_cell", 617 | "openssl-macros", 618 | "openssl-sys", 619 | ] 620 | 621 | [[package]] 622 | name = "openssl-macros" 623 | version = "0.1.1" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 626 | dependencies = [ 627 | "proc-macro2", 628 | "quote", 629 | "syn", 630 | ] 631 | 632 | [[package]] 633 | name = "openssl-probe" 634 | version = "0.1.5" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 637 | 638 | [[package]] 639 | name = "openssl-sys" 640 | version = "0.9.101" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" 643 | dependencies = [ 644 | "cc", 645 | "libc", 646 | "pkg-config", 647 | "vcpkg", 648 | ] 649 | 650 | [[package]] 651 | name = "percent-encoding" 652 | version = "2.3.1" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 655 | 656 | [[package]] 657 | name = "pin-project" 658 | version = "1.1.5" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 661 | dependencies = [ 662 | "pin-project-internal", 663 | ] 664 | 665 | [[package]] 666 | name = "pin-project-internal" 667 | version = "1.1.5" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 670 | dependencies = [ 671 | "proc-macro2", 672 | "quote", 673 | "syn", 674 | ] 675 | 676 | [[package]] 677 | name = "pin-project-lite" 678 | version = "0.2.13" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 681 | 682 | [[package]] 683 | name = "pin-utils" 684 | version = "0.1.0" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 687 | 688 | [[package]] 689 | name = "pkg-config" 690 | version = "0.3.30" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 693 | 694 | [[package]] 695 | name = "proc-macro2" 696 | version = "1.0.79" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 699 | dependencies = [ 700 | "unicode-ident", 701 | ] 702 | 703 | [[package]] 704 | name = "quick-xml" 705 | version = "0.26.0" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" 708 | dependencies = [ 709 | "memchr", 710 | "serde", 711 | ] 712 | 713 | [[package]] 714 | name = "quote" 715 | version = "1.0.35" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 718 | dependencies = [ 719 | "proc-macro2", 720 | ] 721 | 722 | [[package]] 723 | name = "read_input" 724 | version = "0.8.6" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "2f178674da3d005db760b30d6735a989d692da37b86337daec6f2e311223d608" 727 | 728 | [[package]] 729 | name = "reqwest" 730 | version = "0.12.1" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "e333b1eb9fe677f6893a9efcb0d277a2d3edd83f358a236b657c32301dc6e5f6" 733 | dependencies = [ 734 | "base64", 735 | "bytes", 736 | "encoding_rs", 737 | "futures-core", 738 | "futures-util", 739 | "h2", 740 | "http", 741 | "http-body", 742 | "http-body-util", 743 | "hyper", 744 | "hyper-tls", 745 | "hyper-util", 746 | "ipnet", 747 | "js-sys", 748 | "log", 749 | "mime", 750 | "native-tls", 751 | "once_cell", 752 | "percent-encoding", 753 | "pin-project-lite", 754 | "rustls-pemfile", 755 | "serde", 756 | "serde_json", 757 | "serde_urlencoded", 758 | "sync_wrapper", 759 | "system-configuration", 760 | "tokio", 761 | "tokio-native-tls", 762 | "tower-service", 763 | "url", 764 | "wasm-bindgen", 765 | "wasm-bindgen-futures", 766 | "web-sys", 767 | "winreg", 768 | ] 769 | 770 | [[package]] 771 | name = "rustc-demangle" 772 | version = "0.1.23" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 775 | 776 | [[package]] 777 | name = "rustix" 778 | version = "0.38.32" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" 781 | dependencies = [ 782 | "bitflags 2.5.0", 783 | "errno", 784 | "libc", 785 | "linux-raw-sys", 786 | "windows-sys 0.52.0", 787 | ] 788 | 789 | [[package]] 790 | name = "rustls-pemfile" 791 | version = "1.0.4" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 794 | dependencies = [ 795 | "base64", 796 | ] 797 | 798 | [[package]] 799 | name = "ryu" 800 | version = "1.0.17" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 803 | 804 | [[package]] 805 | name = "schannel" 806 | version = "0.1.23" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" 809 | dependencies = [ 810 | "windows-sys 0.52.0", 811 | ] 812 | 813 | [[package]] 814 | name = "security-framework" 815 | version = "2.9.2" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" 818 | dependencies = [ 819 | "bitflags 1.3.2", 820 | "core-foundation", 821 | "core-foundation-sys", 822 | "libc", 823 | "security-framework-sys", 824 | ] 825 | 826 | [[package]] 827 | name = "security-framework-sys" 828 | version = "2.9.1" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" 831 | dependencies = [ 832 | "core-foundation-sys", 833 | "libc", 834 | ] 835 | 836 | [[package]] 837 | name = "serde" 838 | version = "1.0.197" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 841 | dependencies = [ 842 | "serde_derive", 843 | ] 844 | 845 | [[package]] 846 | name = "serde_derive" 847 | version = "1.0.197" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 850 | dependencies = [ 851 | "proc-macro2", 852 | "quote", 853 | "syn", 854 | ] 855 | 856 | [[package]] 857 | name = "serde_json" 858 | version = "1.0.114" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 861 | dependencies = [ 862 | "itoa", 863 | "ryu", 864 | "serde", 865 | ] 866 | 867 | [[package]] 868 | name = "serde_urlencoded" 869 | version = "0.7.1" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 872 | dependencies = [ 873 | "form_urlencoded", 874 | "itoa", 875 | "ryu", 876 | "serde", 877 | ] 878 | 879 | [[package]] 880 | name = "slab" 881 | version = "0.4.9" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 884 | dependencies = [ 885 | "autocfg", 886 | ] 887 | 888 | [[package]] 889 | name = "smallvec" 890 | version = "1.13.2" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 893 | 894 | [[package]] 895 | name = "socket2" 896 | version = "0.5.6" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" 899 | dependencies = [ 900 | "libc", 901 | "windows-sys 0.52.0", 902 | ] 903 | 904 | [[package]] 905 | name = "syn" 906 | version = "2.0.53" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" 909 | dependencies = [ 910 | "proc-macro2", 911 | "quote", 912 | "unicode-ident", 913 | ] 914 | 915 | [[package]] 916 | name = "sync_wrapper" 917 | version = "0.1.2" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 920 | 921 | [[package]] 922 | name = "system-configuration" 923 | version = "0.5.1" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 926 | dependencies = [ 927 | "bitflags 1.3.2", 928 | "core-foundation", 929 | "system-configuration-sys", 930 | ] 931 | 932 | [[package]] 933 | name = "system-configuration-sys" 934 | version = "0.5.0" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 937 | dependencies = [ 938 | "core-foundation-sys", 939 | "libc", 940 | ] 941 | 942 | [[package]] 943 | name = "tempfile" 944 | version = "3.10.1" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 947 | dependencies = [ 948 | "cfg-if", 949 | "fastrand", 950 | "rustix", 951 | "windows-sys 0.52.0", 952 | ] 953 | 954 | [[package]] 955 | name = "thiserror" 956 | version = "1.0.58" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" 959 | dependencies = [ 960 | "thiserror-impl", 961 | ] 962 | 963 | [[package]] 964 | name = "thiserror-impl" 965 | version = "1.0.58" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" 968 | dependencies = [ 969 | "proc-macro2", 970 | "quote", 971 | "syn", 972 | ] 973 | 974 | [[package]] 975 | name = "tinyvec" 976 | version = "1.6.0" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 979 | dependencies = [ 980 | "tinyvec_macros", 981 | ] 982 | 983 | [[package]] 984 | name = "tinyvec_macros" 985 | version = "0.1.1" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 988 | 989 | [[package]] 990 | name = "tokio" 991 | version = "1.36.0" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" 994 | dependencies = [ 995 | "backtrace", 996 | "bytes", 997 | "libc", 998 | "mio", 999 | "num_cpus", 1000 | "pin-project-lite", 1001 | "socket2", 1002 | "tokio-macros", 1003 | "windows-sys 0.48.0", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "tokio-macros" 1008 | version = "2.2.0" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 1011 | dependencies = [ 1012 | "proc-macro2", 1013 | "quote", 1014 | "syn", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "tokio-native-tls" 1019 | version = "0.3.1" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1022 | dependencies = [ 1023 | "native-tls", 1024 | "tokio", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "tokio-stream" 1029 | version = "0.1.15" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" 1032 | dependencies = [ 1033 | "futures-core", 1034 | "pin-project-lite", 1035 | "tokio", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "tokio-util" 1040 | version = "0.6.10" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" 1043 | dependencies = [ 1044 | "bytes", 1045 | "futures-core", 1046 | "futures-sink", 1047 | "log", 1048 | "pin-project-lite", 1049 | "tokio", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "tokio-util" 1054 | version = "0.7.10" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 1057 | dependencies = [ 1058 | "bytes", 1059 | "futures-core", 1060 | "futures-sink", 1061 | "pin-project-lite", 1062 | "tokio", 1063 | "tracing", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "tower" 1068 | version = "0.4.13" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1071 | dependencies = [ 1072 | "futures-core", 1073 | "futures-util", 1074 | "pin-project", 1075 | "pin-project-lite", 1076 | "tokio", 1077 | "tower-layer", 1078 | "tower-service", 1079 | "tracing", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "tower-layer" 1084 | version = "0.3.2" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 1087 | 1088 | [[package]] 1089 | name = "tower-service" 1090 | version = "0.3.2" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1093 | 1094 | [[package]] 1095 | name = "tracing" 1096 | version = "0.1.40" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1099 | dependencies = [ 1100 | "log", 1101 | "pin-project-lite", 1102 | "tracing-core", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "tracing-core" 1107 | version = "0.1.32" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1110 | dependencies = [ 1111 | "once_cell", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "try-lock" 1116 | version = "0.2.5" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1119 | 1120 | [[package]] 1121 | name = "twitch-irc" 1122 | version = "3.0.1" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "e54cf6932e6dbe8f0af0ece6eeaed9a57af1bff66fceedf42cd78dec834b16d6" 1125 | dependencies = [ 1126 | "async-trait", 1127 | "bytes", 1128 | "chrono", 1129 | "enum_dispatch", 1130 | "futures-util", 1131 | "itertools", 1132 | "log", 1133 | "smallvec", 1134 | "thiserror", 1135 | "tokio", 1136 | "tokio-native-tls", 1137 | "tokio-stream", 1138 | "tokio-util 0.6.10", 1139 | ] 1140 | 1141 | [[package]] 1142 | name = "unicode-bidi" 1143 | version = "0.3.15" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1146 | 1147 | [[package]] 1148 | name = "unicode-ident" 1149 | version = "1.0.12" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1152 | 1153 | [[package]] 1154 | name = "unicode-normalization" 1155 | version = "0.1.23" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1158 | dependencies = [ 1159 | "tinyvec", 1160 | ] 1161 | 1162 | [[package]] 1163 | name = "url" 1164 | version = "2.5.0" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1167 | dependencies = [ 1168 | "form_urlencoded", 1169 | "idna", 1170 | "percent-encoding", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "uuid" 1175 | version = "1.8.0" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" 1178 | dependencies = [ 1179 | "getrandom", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "vcpkg" 1184 | version = "0.2.15" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1187 | 1188 | [[package]] 1189 | name = "want" 1190 | version = "0.3.1" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1193 | dependencies = [ 1194 | "try-lock", 1195 | ] 1196 | 1197 | [[package]] 1198 | name = "wasi" 1199 | version = "0.11.0+wasi-snapshot-preview1" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1202 | 1203 | [[package]] 1204 | name = "wasm-bindgen" 1205 | version = "0.2.92" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1208 | dependencies = [ 1209 | "cfg-if", 1210 | "wasm-bindgen-macro", 1211 | ] 1212 | 1213 | [[package]] 1214 | name = "wasm-bindgen-backend" 1215 | version = "0.2.92" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1218 | dependencies = [ 1219 | "bumpalo", 1220 | "log", 1221 | "once_cell", 1222 | "proc-macro2", 1223 | "quote", 1224 | "syn", 1225 | "wasm-bindgen-shared", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "wasm-bindgen-futures" 1230 | version = "0.4.42" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" 1233 | dependencies = [ 1234 | "cfg-if", 1235 | "js-sys", 1236 | "wasm-bindgen", 1237 | "web-sys", 1238 | ] 1239 | 1240 | [[package]] 1241 | name = "wasm-bindgen-macro" 1242 | version = "0.2.92" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1245 | dependencies = [ 1246 | "quote", 1247 | "wasm-bindgen-macro-support", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "wasm-bindgen-macro-support" 1252 | version = "0.2.92" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1255 | dependencies = [ 1256 | "proc-macro2", 1257 | "quote", 1258 | "syn", 1259 | "wasm-bindgen-backend", 1260 | "wasm-bindgen-shared", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "wasm-bindgen-shared" 1265 | version = "0.2.92" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1268 | 1269 | [[package]] 1270 | name = "web-sys" 1271 | version = "0.3.69" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 1274 | dependencies = [ 1275 | "js-sys", 1276 | "wasm-bindgen", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "windows-core" 1281 | version = "0.52.0" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1284 | dependencies = [ 1285 | "windows-targets 0.52.4", 1286 | ] 1287 | 1288 | [[package]] 1289 | name = "windows-sys" 1290 | version = "0.48.0" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1293 | dependencies = [ 1294 | "windows-targets 0.48.5", 1295 | ] 1296 | 1297 | [[package]] 1298 | name = "windows-sys" 1299 | version = "0.52.0" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1302 | dependencies = [ 1303 | "windows-targets 0.52.4", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "windows-targets" 1308 | version = "0.48.5" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1311 | dependencies = [ 1312 | "windows_aarch64_gnullvm 0.48.5", 1313 | "windows_aarch64_msvc 0.48.5", 1314 | "windows_i686_gnu 0.48.5", 1315 | "windows_i686_msvc 0.48.5", 1316 | "windows_x86_64_gnu 0.48.5", 1317 | "windows_x86_64_gnullvm 0.48.5", 1318 | "windows_x86_64_msvc 0.48.5", 1319 | ] 1320 | 1321 | [[package]] 1322 | name = "windows-targets" 1323 | version = "0.52.4" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 1326 | dependencies = [ 1327 | "windows_aarch64_gnullvm 0.52.4", 1328 | "windows_aarch64_msvc 0.52.4", 1329 | "windows_i686_gnu 0.52.4", 1330 | "windows_i686_msvc 0.52.4", 1331 | "windows_x86_64_gnu 0.52.4", 1332 | "windows_x86_64_gnullvm 0.52.4", 1333 | "windows_x86_64_msvc 0.52.4", 1334 | ] 1335 | 1336 | [[package]] 1337 | name = "windows_aarch64_gnullvm" 1338 | version = "0.48.5" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1341 | 1342 | [[package]] 1343 | name = "windows_aarch64_gnullvm" 1344 | version = "0.52.4" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 1347 | 1348 | [[package]] 1349 | name = "windows_aarch64_msvc" 1350 | version = "0.48.5" 1351 | source = "registry+https://github.com/rust-lang/crates.io-index" 1352 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1353 | 1354 | [[package]] 1355 | name = "windows_aarch64_msvc" 1356 | version = "0.52.4" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 1359 | 1360 | [[package]] 1361 | name = "windows_i686_gnu" 1362 | version = "0.48.5" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1365 | 1366 | [[package]] 1367 | name = "windows_i686_gnu" 1368 | version = "0.52.4" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 1371 | 1372 | [[package]] 1373 | name = "windows_i686_msvc" 1374 | version = "0.48.5" 1375 | source = "registry+https://github.com/rust-lang/crates.io-index" 1376 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1377 | 1378 | [[package]] 1379 | name = "windows_i686_msvc" 1380 | version = "0.52.4" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 1383 | 1384 | [[package]] 1385 | name = "windows_x86_64_gnu" 1386 | version = "0.48.5" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1389 | 1390 | [[package]] 1391 | name = "windows_x86_64_gnu" 1392 | version = "0.52.4" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 1395 | 1396 | [[package]] 1397 | name = "windows_x86_64_gnullvm" 1398 | version = "0.48.5" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1401 | 1402 | [[package]] 1403 | name = "windows_x86_64_gnullvm" 1404 | version = "0.52.4" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 1407 | 1408 | [[package]] 1409 | name = "windows_x86_64_msvc" 1410 | version = "0.48.5" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1413 | 1414 | [[package]] 1415 | name = "windows_x86_64_msvc" 1416 | version = "0.52.4" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 1419 | 1420 | [[package]] 1421 | name = "winreg" 1422 | version = "0.50.0" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1425 | dependencies = [ 1426 | "cfg-if", 1427 | "windows-sys 0.48.0", 1428 | ] 1429 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "liveu_stats_bot" 3 | version = "0.6.1" 4 | authors = ["Brian Spit "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | anyhow = "1.0" 11 | quick-xml = {version = "0.26", features = ["serialize"]} 12 | read_input = "0.8" 13 | reqwest = { version = "0.12", features = ["json"]} 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_json = "1.0" 16 | thiserror = "1.0" 17 | tokio = { version = "1.5", features = ["macros", "rt", "rt-multi-thread"] } 18 | twitch-irc = "3.0" 19 | uuid = { version = "1.8", features = ["v4"] } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brian Spit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LIVEU STATS BOT 2 | 3 | A chat bot that makes it easier to see the current status of the battery and modems. 4 | 5 | ## How do i run this? 6 | 7 | Just download the latest binary from [releases](https://github.com/715209/liveu_stats_bot/releases) and execute it. 8 | 9 | ## Config 10 | 11 | This config will be automatically generated upon running the binary and saved as `config.json`. 12 | 13 | ```JSON 14 | { 15 | "liveu": { 16 | "email": "YOUR LIVEU EMAIL", 17 | "password": "YOUR LIVEU PASSWORD", 18 | "id": null, 19 | "monitor": { 20 | "battery": true, 21 | "batteryNotification": [ 22 | 99, 23 | 50, 24 | 10, 25 | 5, 26 | 1 27 | ], 28 | "modems": true 29 | } 30 | }, 31 | "twitch": { 32 | "botUsername": "TWITCH BOT USERNAME", 33 | "botOauth": "TWITCH BOT OAUTH", 34 | "channel": "YOUR TWITCH CHANNEL", 35 | "adminUsers": ["b3ck", "travelwithgus"], 36 | "modOnly": true 37 | }, 38 | "commands": { 39 | "cooldown": 5, 40 | "stats": ["!lustats", "!liveustats", "!lus"], 41 | "battery": ["!battery", "!liveubattery", "!lub"], 42 | "start": "!lustart", 43 | "stop": "!lustop", 44 | "restart": "!lurestart", 45 | "reboot": "!lureboot", 46 | "delay": "!ludelay" 47 | }, 48 | "rtmp": { 49 | "url": "http://localhost/stat", 50 | "application": "publish", 51 | "key": "live" 52 | }, 53 | "customPortNames": { 54 | "ethernet": "ETH", 55 | "wifi": "WiFi", 56 | "usb1": "USB1", 57 | "usb2": "USB2", 58 | "sim1": "SIM1", 59 | "sim2": "SIM2" 60 | } 61 | } 62 | ``` 63 | 64 | ### Optional config settings 65 | 66 | You can remove these settings from the config if you don't want them or replace them with `null`. 67 | 68 | | Name | Description | 69 | | --------------- | ----------------------------------------------------------------------------------- | 70 | | id | When using mutliple units you can set a default unit by using the bossid | 71 | | adminUsers | A list of twitch usernames e.g. `["715209", "b3ck"]` | 72 | | rtmp | If you are using nginx you can also show the bitrate when using the `stats` command | 73 | | customPortNames | Customize the port names | 74 | 75 | ## Chat Commands 76 | 77 | After running the app successfully you can use the following default commands in your chat: 78 | 79 | | Name | Default command | Description | 80 | | ------- | --------------- | -------------------------------------------------- | 81 | | stats | !lus | Shows the current connected modems and bitrate | 82 | | battery | !lub | Shows the current battery charge percentage | 83 | | start | !lustart | Starts the stream (not the unit) | 84 | | stop | !lustop | Stops the stream | 85 | | restart | !lurestart | Restarts the stream | 86 | | reboot | !lureboot | Reboots the unit | 87 | | delay | !ludelay | Toggles between low delay and high resiliency mode | 88 | 89 | You can add, delete or change the commands to whatever you want in `config.json` under the `commands` section. 90 | 91 | The start, stop and restart commands are only available to the channel owner or adminUsers. 92 | 93 | ## Give specific users access to all commands 94 | 95 | Add the twitch username in adminUsers like this: `["715209", "b3ck"]`. 96 | 97 | ## Possible Chat Command Results: 98 | 99 | If your LiveU is offline you'll see this in chat: 100 | > ChatBot: LiveU Offline :( 101 | 102 | If your LiveU is online and ready you'll see this in chat: 103 | > ChatBot: LiveU Online and Ready 104 | 105 | If your LiveU is online, streaming but not using NGINX you'll see this in chat: 106 | > ChatBot: WiFi: 2453 Kbps, USB1: 2548 Kbps, USB2: 2328 Kbps, Ethernet: 2285 Kbps, Total LRT: 7000 Kbps 107 | 108 | If your LiveU is online, streaming and you're using NGINX you'll see this in chat: 109 | > ChatBot: WiFi: 2453 Kbps, USB1: 2548 Kbps, USB2: 2328 Kbps, Ethernet: 2285 Kbps, Total LRT: 7000 Kbps, RTMP: 6000 Kbps 110 | 111 | `Please note: if one of your connections is offline it will NOT show up at all in the stats.` 112 | 113 | ## Credits: 114 | [Cinnabarcorp (travelingwithgus)](https://twitch.tv/travelwithgus): Initial Idea, Feedback, Use Case, and Q&A Testing. 115 | 116 | [B3ck](https://twitch.tv/b3ck): Feedback and Q&A Testing. 117 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use error::Error; 2 | use read_input::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{fs, path::Path}; 5 | 6 | use crate::{error, liveu}; 7 | 8 | const CONFIG_FILE_NAME: &str = "config.json"; 9 | 10 | #[derive(Serialize, Deserialize, Debug, Clone)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct Liveu { 13 | pub email: String, 14 | pub password: String, 15 | pub id: Option, 16 | pub monitor: Monitor, 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Debug, Clone)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct Monitor { 22 | pub battery: bool, 23 | pub battery_charging: bool, 24 | pub battery_notification: Vec, 25 | pub battery_interval: u64, 26 | pub modems: bool, 27 | pub modems_interval: u64, 28 | } 29 | 30 | #[derive(Serialize, Deserialize, Debug, Clone)] 31 | #[serde(rename_all = "camelCase")] 32 | pub struct Twitch { 33 | pub bot_username: String, 34 | pub bot_oauth: String, 35 | pub channel: String, 36 | pub admin_users: Option>, 37 | pub mod_only: bool, 38 | } 39 | 40 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 41 | #[serde(rename_all = "camelCase")] 42 | pub struct Commands { 43 | pub cooldown: u16, 44 | pub stats: Vec, 45 | pub battery: Vec, 46 | pub start: String, 47 | pub stop: String, 48 | pub restart: String, 49 | pub reboot: String, 50 | pub delay: String, 51 | } 52 | 53 | #[derive(Serialize, Deserialize, Debug, Clone)] 54 | pub struct Rtmp { 55 | pub url: String, 56 | pub application: String, 57 | pub key: String, 58 | } 59 | 60 | #[derive(Serialize, Deserialize, Debug, Clone)] 61 | #[serde(rename_all = "camelCase")] 62 | pub struct Config { 63 | pub liveu: Liveu, 64 | pub twitch: Twitch, 65 | pub commands: Commands, 66 | pub rtmp: Option, 67 | pub custom_port_names: Option, 68 | } 69 | 70 | impl Config { 71 | /// Loads the config 72 | pub fn load

(path: P) -> Result 73 | where 74 | P: AsRef, 75 | { 76 | let file = fs::read_to_string(path)?; 77 | let mut config = serde_json::from_str::(&file)?; 78 | Self::lowercase_settings(&mut config); 79 | 80 | Ok(config) 81 | } 82 | 83 | /// Lowercase settings which should always be lowercase 84 | pub fn lowercase_settings(config: &mut Config) { 85 | let Twitch { 86 | bot_username, 87 | bot_oauth, 88 | channel, 89 | admin_users, 90 | .. 91 | } = &mut config.twitch; 92 | 93 | *channel = channel.to_lowercase(); 94 | *bot_oauth = bot_oauth.to_lowercase(); 95 | *bot_username = bot_username.to_lowercase(); 96 | 97 | if let Some(admin_users) = admin_users { 98 | for user in admin_users { 99 | *user = user.to_lowercase(); 100 | } 101 | } 102 | } 103 | 104 | /// Asks the user to enter settings and save it to disk 105 | pub async fn ask_for_settings() -> Result { 106 | println!("Please enter your Liveu details below"); 107 | 108 | let email = input().msg("Email: ").get(); 109 | let password = input().msg("Password: ").get(); // FIXME: Change password input? 110 | let monitor_enabled = input_to_bool(&input() 111 | .msg("\nDo you want to receive automatic chat messages about\nthe status of your battery or modems (Y/n): ") 112 | .add_test(|x: &String| x.to_lowercase() == "y" || x.to_lowercase() == "n") 113 | .err("Please enter y or n: ") 114 | .default("y".to_string()) 115 | .get()); 116 | 117 | let monitor = Monitor { 118 | battery: monitor_enabled, 119 | battery_notification: [99, 50, 10, 5, 1].to_vec(), 120 | modems: monitor_enabled, 121 | battery_interval: 10, 122 | modems_interval: 10, 123 | battery_charging: monitor_enabled, 124 | }; 125 | 126 | let mut liveu = Liveu { 127 | email, 128 | password, 129 | id: None, 130 | monitor, 131 | }; 132 | 133 | let lauth = liveu::Liveu::authenticate(liveu.clone()).await?; 134 | let inventories = lauth.get_inventories().await?; 135 | 136 | if inventories.units.len() > 1 { 137 | let option = input_to_bool( 138 | &input() 139 | .msg("Do you want to save a default unit to use in the config (y/N): ") 140 | .add_test(|x: &String| x.to_lowercase() == "y" || x.to_lowercase() == "n") 141 | .err("Please enter y or n: ") 142 | .default("n".to_string()) 143 | .get(), 144 | ); 145 | 146 | if option { 147 | let loc = liveu::Liveu::get_boss_id_location(&inventories); 148 | liveu.id = Some(inventories.units[loc].id.to_owned()); 149 | } 150 | } 151 | 152 | println!("\nPlease enter your Twitch details below"); 153 | let twitch = Twitch { 154 | bot_username: input().msg("Bot username: ").get(), 155 | bot_oauth: input() 156 | .msg("(You can generate an Oauth here: https://twitchapps.com/tmi/)\nBot oauth: ") 157 | .get(), 158 | channel: input().msg("Channel name: ").get(), 159 | admin_users: None, 160 | mod_only: input_to_bool( 161 | &input() 162 | .msg("Only allow mods to access the commands (Y/n): ") 163 | .add_test(|x: &String| x.to_lowercase() == "y" || x.to_lowercase() == "n") 164 | .err("Please enter y or n: ") 165 | .default("y".to_string()) 166 | .get(), 167 | ), 168 | }; 169 | 170 | let commands = Commands { 171 | cooldown: input() 172 | .msg("Command cooldown (default 5 seconds): ") 173 | .err("Please enter a number") 174 | .default(5) 175 | .get(), 176 | stats: vec![ 177 | "!lustats".to_string(), 178 | "!liveustats".to_string(), 179 | "!lus".to_string(), 180 | ], 181 | battery: vec![ 182 | "!battery".to_string(), 183 | "!liveubattery".to_string(), 184 | "!lub".to_string(), 185 | ], 186 | start: "!lustart".to_string(), 187 | stop: "!lustop".to_string(), 188 | restart: "!lurestart".to_string(), 189 | reboot: "!lureboot".to_string(), 190 | delay: "!ludelay".to_string(), 191 | }; 192 | 193 | let q: String = input() 194 | .msg("\nAre you using nginx and would you like to display its bitrate as well (y/N): ") 195 | .add_test(|x: &String| x.to_lowercase() == "y" || x.to_lowercase() == "n") 196 | .err("Please enter y or n: ") 197 | .default("n".to_string()) 198 | .get(); 199 | 200 | let mut rtmp = None; 201 | 202 | if q == "y" { 203 | rtmp = Some(Rtmp { 204 | url: input().msg("Please enter the stats page URL: ").get(), 205 | application: input().msg("Application name: ").get(), 206 | key: input().msg("Stream key: ").get(), 207 | }); 208 | } 209 | 210 | let q: String = input() 211 | .msg("\nWould you like to use a custom name for each port? (y/N): ") 212 | .add_test(|x: &String| x.to_lowercase() == "y" || x.to_lowercase() == "n") 213 | .err("Please enter y or n: ") 214 | .default("n".to_string()) 215 | .get(); 216 | 217 | let mut custom_unit_names = None; 218 | 219 | if q == "y" { 220 | println!("Press enter to keep using the default value"); 221 | 222 | let mut un = CustomUnitNames::default(); 223 | 224 | un.ethernet = input().msg("Ethernet: ").default(un.ethernet).get(); 225 | un.wifi = input().msg("WiFi: ").default(un.wifi).get(); 226 | un.usb1 = input().msg("USB1: ").default(un.usb1).get(); 227 | un.usb2 = input().msg("USB2: ").default(un.usb2).get(); 228 | un.sim1 = input().msg("SIM1: ").default(un.sim1).get(); 229 | un.sim2 = input().msg("SIM2: ").default(un.sim2).get(); 230 | 231 | custom_unit_names = Some(un); 232 | } 233 | 234 | let mut config = Config { 235 | liveu, 236 | twitch, 237 | commands, 238 | rtmp, 239 | custom_port_names: custom_unit_names, 240 | }; 241 | fs::write(CONFIG_FILE_NAME, serde_json::to_string_pretty(&config)?)?; 242 | 243 | // FIXME: Does not work on windows 244 | print!("\x1B[2J"); 245 | 246 | let mut path = std::env::current_dir()?; 247 | path.push(CONFIG_FILE_NAME); 248 | println!( 249 | "Saved settings to {} in {}", 250 | CONFIG_FILE_NAME, 251 | path.display() 252 | ); 253 | 254 | Self::lowercase_settings(&mut config); 255 | 256 | Ok(config) 257 | } 258 | } 259 | 260 | #[derive(Serialize, Deserialize, Debug, Clone)] 261 | pub struct CustomUnitNames { 262 | pub ethernet: String, 263 | pub wifi: String, 264 | pub usb1: String, 265 | pub usb2: String, 266 | pub sim1: String, 267 | pub sim2: String, 268 | } 269 | 270 | impl Default for CustomUnitNames { 271 | fn default() -> Self { 272 | CustomUnitNames { 273 | ethernet: "ETH".to_string(), 274 | wifi: "WiFi".to_string(), 275 | usb1: "USB1".to_string(), 276 | usb2: "USB2".to_string(), 277 | sim1: "SIM1".to_string(), 278 | sim2: "SIM2".to_string(), 279 | } 280 | } 281 | } 282 | 283 | /// Converts y or n to bool. 284 | fn input_to_bool(confirm: &str) -> bool { 285 | if confirm == "y" { 286 | return true; 287 | } 288 | 289 | false 290 | } 291 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | /// All errors that can happen 3 | pub enum Error { 4 | #[error("Json error: {0}")] 5 | Json(#[from] serde_json::error::Error), 6 | 7 | #[error("Error writing file: {0}")] 8 | Write(#[from] std::io::Error), 9 | 10 | #[error("Request failed: {0}")] 11 | RequestFailed(#[from] reqwest::Error), 12 | 13 | #[error("Invalid credentials can't login")] 14 | InvalidCredentials, 15 | 16 | #[error("XML parsing error: {0}")] 17 | Xml(#[from] quick_xml::DeError), 18 | 19 | #[error("Rtmp is offline: {0}")] 20 | RtmpDown(String), 21 | 22 | #[error("No inventories found")] 23 | NoInventoriesFound, 24 | 25 | #[error("No units found")] 26 | NoUnitsFound, 27 | 28 | #[error("Status not available")] 29 | StatusNotAvailable, 30 | 31 | #[error("Not enough permissions to use command")] 32 | NotEnoughPermissions, 33 | } 34 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod error; 3 | pub mod liveu; 4 | pub mod liveu_monitor; 5 | pub mod nginx; 6 | pub mod twitch; 7 | -------------------------------------------------------------------------------- /src/liveu.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{self, Liveu as Config_liveu}, 3 | error::Error, 4 | }; 5 | use read_input::prelude::*; 6 | use reqwest::{ 7 | header::{ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_TYPE}, 8 | Method, StatusCode, 9 | }; 10 | use serde::{Deserialize, Serialize}; 11 | use serde_json::Value; 12 | use std::{collections::HashMap, sync::Arc}; 13 | use tokio::sync::Mutex; 14 | use uuid::Uuid; 15 | 16 | const APPLICATION_ID: &str = "SlZ3SHqiqtYJRkF0zO"; 17 | const LIVEU_API: &str = "https://lu-central.liveu.tv/luc/luc-core-web/rest/v0"; 18 | const LIVEU_API_V2: &str = "https://lu-central.liveu.tv/luc/luc-core-web/rest/v2"; 19 | 20 | #[derive(Deserialize)] 21 | struct Res { 22 | data: Data, 23 | } 24 | 25 | #[derive(Deserialize)] 26 | struct Data { 27 | response: AuthRes, 28 | } 29 | 30 | #[derive(Deserialize, Debug, Clone)] 31 | struct AuthRes { 32 | access_token: String, 33 | expires_in: u64, 34 | } 35 | 36 | #[derive(Deserialize, Debug)] 37 | pub struct UnitInterfaces { 38 | pub interfaces: Vec, 39 | } 40 | 41 | #[derive(Deserialize, Debug)] 42 | #[serde(rename_all = "camelCase")] 43 | pub struct Interface { 44 | pub connected: bool, 45 | pub name: String, 46 | pub downlink_kbps: u32, 47 | pub uplink_kbps: u32, 48 | pub enabled: bool, 49 | pub port: String, 50 | pub technology: String, 51 | pub up_signal_quality: u32, 52 | pub down_signal_quality: u32, 53 | pub active_sim: Option, 54 | pub is_currently_roaming: bool, 55 | pub kbps: u32, 56 | pub signal_quality: u32, 57 | } 58 | 59 | #[derive(Deserialize, Debug)] 60 | pub struct Unit { 61 | pub id: String, 62 | pub reg_code: String, 63 | pub status: String, 64 | pub name: String, 65 | } 66 | 67 | #[derive(Deserialize, Debug)] 68 | pub struct Inventories { 69 | pub units: Vec, 70 | } 71 | 72 | #[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 73 | #[serde(rename_all = "camelCase")] 74 | pub struct Battery { 75 | pub connected: bool, 76 | pub percentage: u8, 77 | pub run_time_to_empty: u32, 78 | pub discharging: bool, 79 | pub charging: bool, 80 | } 81 | 82 | #[derive(Deserialize, Debug)] 83 | #[serde(rename_all = "camelCase")] 84 | pub struct Video { 85 | pub resolution: Option, 86 | pub bitrate: Option, 87 | } 88 | 89 | #[derive(Serialize, Deserialize, Debug, Clone)] 90 | #[serde(rename_all = "camelCase")] 91 | pub struct DelayReq { 92 | pub unit: Delay, 93 | } 94 | 95 | #[derive(Serialize, Deserialize, Debug, Clone)] 96 | #[serde(rename_all = "camelCase")] 97 | pub struct Delay { 98 | pub delay: u64, 99 | } 100 | 101 | #[derive(Debug, Clone)] 102 | pub struct Liveu { 103 | access_token: Arc>, 104 | config: Config_liveu, 105 | } 106 | 107 | impl Liveu { 108 | pub async fn authenticate(config: Config_liveu) -> Result { 109 | let token = if let Ok(token) = Self::get_access_token(&config).await { 110 | token 111 | } else { 112 | return Err(Error::InvalidCredentials); 113 | }; 114 | 115 | Ok(Liveu { 116 | access_token: Arc::new(Mutex::new(token)), 117 | config, 118 | }) 119 | } 120 | 121 | async fn get_access_token(config: &Config_liveu) -> Result { 122 | let user_session = Uuid::new_v4(); 123 | let client = reqwest::Client::new(); 124 | 125 | let res = client 126 | .post("https://solo-api.liveu.tv/v1_prod/zendesk/userlogin") 127 | .basic_auth(&config.email, Some(&config.password)) 128 | .header(ACCEPT, "application/json, text/plain, */*") 129 | .header(ACCEPT_LANGUAGE, "en-US,en;q=0.9") 130 | .header(CONTENT_TYPE, "application/json;charset=UTF-8") 131 | .header( 132 | "x-user-name", 133 | format!("{}{}", &config.email, &user_session.to_string()), 134 | ) 135 | .body(r#"{"return_to":"https://solo.liveu.tv/#/dashboard/units"}"#) 136 | .send() 137 | .await? 138 | .json::() 139 | .await?; 140 | 141 | Ok(res.data.response.access_token) 142 | } 143 | 144 | /// Sends the specified request. Gets a new token if unauthorized. 145 | pub async fn send_request( 146 | &self, 147 | method: Method, 148 | url: &str, 149 | payload: Option, 150 | ) -> Result { 151 | let mut res = self 152 | .try_send_request(method.clone(), url, payload.clone()) 153 | .await?; 154 | 155 | if res.status() == 401 { 156 | { 157 | let mut token = self.access_token.lock().await; 158 | *token = Self::get_access_token(&self.config).await?; 159 | } 160 | 161 | res = self 162 | .try_send_request(method.clone(), url, payload.clone()) 163 | .await?; 164 | } 165 | 166 | Ok(res) 167 | } 168 | 169 | pub async fn try_send_request( 170 | &self, 171 | method: Method, 172 | url: &str, 173 | payload: Option, 174 | ) -> Result { 175 | let client = reqwest::Client::new(); 176 | 177 | let mut client = client 178 | .request(method, url) 179 | .header(ACCEPT, "application/json, text/plain, */*") 180 | .header(ACCEPT_LANGUAGE, "en-US,en;q=0.9") 181 | .header( 182 | AUTHORIZATION, 183 | format!("Bearer {}", { &self.access_token.lock().await }), 184 | ) 185 | .header("application-id", APPLICATION_ID); 186 | 187 | if let Some(data) = payload { 188 | client = client.json(&data); 189 | } 190 | 191 | client.send().await 192 | } 193 | 194 | pub async fn get_inventories(&self) -> Result { 195 | let res = self 196 | .send_request( 197 | Method::GET, 198 | &format!("{}/inventories", LIVEU_API), 199 | None::<()>, 200 | ) 201 | .await?; 202 | 203 | if res.status().is_client_error() { 204 | return Err(Error::NoInventoriesFound); 205 | } 206 | 207 | let res_json: Value = res.json().await?; 208 | Ok(serde_json::from_value::( 209 | res_json["data"]["inventories"][0].to_owned(), 210 | )?) 211 | } 212 | 213 | pub async fn get_interfaces(&self, boss_id: &str) -> Result, Error> { 214 | let res = self 215 | .send_request( 216 | Method::GET, 217 | &format!("{}/units/{}/status/interfaces", LIVEU_API, &boss_id), 218 | None::<()>, 219 | ) 220 | .await?; 221 | 222 | match res.status() { 223 | StatusCode::OK => Ok(res.json().await?), 224 | StatusCode::NO_CONTENT => Ok(vec![]), 225 | _ => Err(Error::NoUnitsFound), 226 | } 227 | } 228 | 229 | pub async fn get_battery(&self, boss_id: &str) -> Result { 230 | let res = self 231 | .send_request( 232 | Method::GET, 233 | &format!("{}/units/{}/status/battery", LIVEU_API, &boss_id), 234 | None::<()>, 235 | ) 236 | .await?; 237 | 238 | match res.status() { 239 | StatusCode::OK => Ok(res.json().await?), 240 | _ => Err(Error::StatusNotAvailable), 241 | } 242 | } 243 | 244 | pub async fn get_video(&self, boss_id: &str) -> Result { 245 | let res = self 246 | .send_request( 247 | Method::GET, 248 | &format!("{}/units/{}/status/video", LIVEU_API, &boss_id), 249 | None::<()>, 250 | ) 251 | .await?; 252 | 253 | match res.status() { 254 | StatusCode::OK => Ok(res.json().await?), 255 | _ => Err(Error::StatusNotAvailable), 256 | } 257 | } 258 | 259 | pub async fn is_idle(&self, boss_id: &str) -> bool { 260 | let video = match self.get_video(boss_id).await { 261 | Ok(v) => v, 262 | Err(_) => return false, 263 | }; 264 | 265 | if video.resolution.is_some() && video.bitrate.is_none() { 266 | return true; 267 | } 268 | 269 | false 270 | } 271 | 272 | pub async fn is_streaming(&self, boss_id: &str) -> bool { 273 | let video = match self.get_video(boss_id).await { 274 | Ok(v) => v, 275 | Err(_) => return false, 276 | }; 277 | 278 | if video.bitrate.is_some() { 279 | return true; 280 | } 281 | 282 | false 283 | } 284 | 285 | pub async fn start_stream(&self, boss_id: &str) -> Result<(), Error> { 286 | let mut map = HashMap::new(); 287 | map.insert("unit_id", boss_id); 288 | 289 | let res = self 290 | .send_request( 291 | Method::POST, 292 | &format!("{}/units/{}/stream", LIVEU_API, &boss_id), 293 | Some(map), 294 | ) 295 | .await?; 296 | 297 | match res.status() { 298 | StatusCode::CREATED => Ok(()), 299 | _ => Err(Error::StatusNotAvailable), 300 | } 301 | } 302 | 303 | pub async fn stop_stream(&self, boss_id: &str) -> Result<(), Error> { 304 | let res = self 305 | .send_request( 306 | Method::DELETE, 307 | &format!("{}/units/{}/stream", LIVEU_API, &boss_id), 308 | None::<()>, 309 | ) 310 | .await?; 311 | 312 | match res.status() { 313 | StatusCode::NO_CONTENT => Ok(()), 314 | _ => Err(Error::StatusNotAvailable), 315 | } 316 | } 317 | 318 | pub async fn reboot_unit(&self, boss_id: &str) -> Result<(), Error> { 319 | let res = self 320 | .send_request( 321 | Method::POST, 322 | &format!("{}/units/{}/reboot", LIVEU_API_V2, &boss_id), 323 | None::<()>, 324 | ) 325 | .await?; 326 | 327 | match res.status() { 328 | StatusCode::NO_CONTENT => Ok(()), 329 | _ => Err(Error::StatusNotAvailable), 330 | } 331 | } 332 | 333 | pub async fn get_delay(&self, boss_id: &str) -> Result { 334 | let res = self 335 | .send_request( 336 | Method::GET, 337 | &format!("{}/units/{}?fields=delay", LIVEU_API, &boss_id), 338 | None::<()>, 339 | ) 340 | .await?; 341 | 342 | match res.status() { 343 | StatusCode::OK => { 344 | let value: serde_json::Value = res.json().await?; 345 | let delay = value["data"]["unit"]["delay"] 346 | .as_u64() 347 | .ok_or(Error::StatusNotAvailable)?; 348 | Ok(Delay { delay }) 349 | } 350 | _ => Err(Error::StatusNotAvailable), 351 | } 352 | } 353 | 354 | pub async fn set_delay(&self, boss_id: &str, delay: u64) -> Result<(), Error> { 355 | let res = self 356 | .send_request( 357 | Method::PUT, 358 | &format!("{}/units/{}/delay", LIVEU_API, &boss_id), 359 | Some(DelayReq { 360 | unit: Delay { delay }, 361 | }), 362 | ) 363 | .await?; 364 | 365 | match res.status() { 366 | StatusCode::NO_CONTENT => Ok(()), 367 | _ => Err(Error::StatusNotAvailable), 368 | } 369 | } 370 | 371 | /// Gets the location of the boss_id in the inventories 372 | pub fn get_boss_id_location(inventories: &Inventories) -> usize { 373 | let size = inventories.units.len(); 374 | 375 | if size == 0 { 376 | panic!("No units found!"); 377 | } 378 | 379 | if size > 1 { 380 | println!("Found {} units!\n", size); 381 | 382 | for (pos, unit) in inventories.units.iter().enumerate() { 383 | println!("({}) {}", pos + 1, unit.name); 384 | } 385 | 386 | let inp = input() 387 | .msg("\nPlease enter which one you want to use (1): ") 388 | .inside_err( 389 | 1..=size, 390 | format!("Please enter a number between 1 and {}: ", size), 391 | ) 392 | .err("That does not look like a number. Please try again:") 393 | .default(1) 394 | .get(); 395 | 396 | return inp - 1; 397 | } 398 | 399 | 0 400 | } 401 | 402 | pub async fn get_unit_custom_names( 403 | &self, 404 | boss_id: &str, 405 | custom_names: Option, 406 | ) -> Result, Error> { 407 | let custom_names = custom_names.unwrap_or_default(); 408 | 409 | Ok(self 410 | .get_interfaces(boss_id) 411 | .await? 412 | .into_iter() 413 | .filter(|x| x.connected) 414 | .map(|x| Self::change_interface_name_to_custom(x, &custom_names)) 415 | .collect()) 416 | } 417 | 418 | fn change_interface_name_to_custom( 419 | mut interface: Interface, 420 | custom_names: &config::CustomUnitNames, 421 | ) -> Interface { 422 | match interface.port.as_ref() { 423 | "eth0" => { 424 | interface.port = custom_names.ethernet.to_string(); 425 | } 426 | "wlan0" => { 427 | interface.port = custom_names.wifi.to_string(); 428 | } 429 | "0" => { 430 | interface.port = custom_names.sim1.to_string(); 431 | } 432 | "1" => { 433 | interface.port = custom_names.sim2.to_string(); 434 | } 435 | "2" => { 436 | interface.port = custom_names.usb1.to_string(); 437 | } 438 | "3" => { 439 | interface.port = custom_names.usb2.to_string(); 440 | } 441 | _ => {} 442 | } 443 | 444 | interface 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /src/liveu_monitor.rs: -------------------------------------------------------------------------------- 1 | use twitch_irc::{ 2 | login, 3 | transport::tcp::{TCPTransport, TLS}, 4 | TwitchIRCClient, 5 | }; 6 | 7 | use crate::{config, liveu}; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Monitor { 11 | pub client: TwitchIRCClient, login::StaticLoginCredentials>, 12 | pub config: config::Config, 13 | pub liveu: liveu::Liveu, 14 | pub boss_id: String, 15 | } 16 | 17 | impl Monitor { 18 | pub fn run(&self) { 19 | let modems = self.clone(); 20 | tokio::spawn(async move { modems.monitor_modems().await }); 21 | 22 | let battery = self.clone(); 23 | tokio::spawn(async move { battery.monitor_battery().await }); 24 | } 25 | 26 | pub async fn monitor_modems(&self) { 27 | let mut current_modems = Vec::new(); 28 | let mut ignore = false; 29 | 30 | for interface in self 31 | .liveu 32 | .get_unit_custom_names(&self.boss_id, self.config.custom_port_names.clone()) 33 | .await 34 | .unwrap() 35 | { 36 | current_modems.push(interface.port); 37 | } 38 | 39 | loop { 40 | tokio::time::sleep(tokio::time::Duration::from_secs( 41 | self.config.liveu.monitor.modems_interval, 42 | )) 43 | .await; 44 | 45 | if !self.liveu.is_streaming(&self.boss_id).await { 46 | ignore = true; 47 | continue; 48 | } 49 | 50 | let mut current = Vec::new(); 51 | let mut new_modems = Vec::new(); 52 | 53 | for interface in self 54 | .liveu 55 | .get_unit_custom_names(&self.boss_id, self.config.custom_port_names.clone()) 56 | .await 57 | .unwrap() 58 | { 59 | // we got a new interface 60 | if !current_modems.contains(&interface.port) { 61 | // println!("New modem {}", interface.port); 62 | new_modems.push(interface.port.to_owned()); 63 | current_modems.push(interface.port.to_owned()); 64 | } 65 | 66 | current.push(interface.port); 67 | } 68 | 69 | // check diff between current and prev 70 | let mut removed_modems = Vec::new(); 71 | for modem in current_modems.iter() { 72 | if !current.contains(modem) { 73 | // println!("Removed modem {}", modem); 74 | removed_modems.push(modem.to_owned()); 75 | } 76 | } 77 | 78 | for rem in removed_modems.iter() { 79 | let index = current_modems.iter().position(|m| m == rem).unwrap(); 80 | current_modems.swap_remove(index); 81 | } 82 | 83 | let message = Self::generate_modems_message(new_modems, removed_modems); 84 | 85 | if !ignore && !message.is_empty() { 86 | let _ = self 87 | .client 88 | .say( 89 | self.config.twitch.channel.to_owned(), 90 | "LiveU: ".to_string() + &message, 91 | ) 92 | .await; 93 | } 94 | 95 | if ignore { 96 | ignore = false; 97 | } 98 | } 99 | } 100 | 101 | fn generate_modems_message(new_modems: Vec, removed_modems: Vec) -> String { 102 | let mut message = String::new(); 103 | 104 | if !new_modems.is_empty() { 105 | let a = if new_modems.len() > 1 { "are" } else { "is" }; 106 | 107 | message += &format!("{} {} now connected", new_modems.join(", "), a); 108 | } 109 | 110 | if !removed_modems.is_empty() { 111 | let a = if removed_modems.len() > 1 { 112 | "have" 113 | } else { 114 | "has" 115 | }; 116 | 117 | message += &format!("{} {} disconnected", removed_modems.join(", "), a); 118 | } 119 | 120 | message 121 | } 122 | 123 | pub async fn monitor_battery(&self) { 124 | let mut prev = liveu::Battery { 125 | connected: false, 126 | percentage: 255, 127 | run_time_to_empty: 0, 128 | discharging: false, 129 | charging: false, 130 | }; 131 | 132 | loop { 133 | tokio::time::sleep(tokio::time::Duration::from_secs( 134 | self.config.liveu.monitor.battery_interval, 135 | )) 136 | .await; 137 | 138 | if !self.liveu.is_streaming(&self.boss_id).await { 139 | continue; 140 | } 141 | 142 | let battery = if let Ok(battery) = self.liveu.get_battery(&self.boss_id).await { 143 | if prev.percentage == 255 { 144 | prev = battery.clone(); 145 | } 146 | 147 | battery 148 | } else { 149 | continue; 150 | }; 151 | 152 | if self.config.liveu.monitor.battery_charging { 153 | self.battery_charging(&battery, &prev).await; 154 | } 155 | 156 | for percentage in &self.config.liveu.monitor.battery_notification { 157 | self.battery_percentage_message(*percentage, &battery, &prev) 158 | .await; 159 | } 160 | 161 | prev = battery; 162 | } 163 | } 164 | 165 | pub async fn battery_charging(&self, battery: &liveu::Battery, prev: &liveu::Battery) { 166 | if !battery.charging && battery.discharging && !prev.discharging { 167 | let _ = self 168 | .client 169 | .say( 170 | self.config.twitch.channel.to_owned(), 171 | "LiveU: RIP PowerBank / Cable Disconnected".to_string(), 172 | ) 173 | .await; 174 | } 175 | 176 | if battery.charging && !battery.discharging && !prev.charging { 177 | let _ = self 178 | .client 179 | .say( 180 | self.config.twitch.channel.to_owned(), 181 | "LiveU: Now charging".to_string(), 182 | ) 183 | .await; 184 | } 185 | 186 | if battery.percentage < 100 187 | && !battery.charging 188 | && !battery.discharging 189 | && (prev.charging || prev.discharging) 190 | { 191 | let _ = self 192 | .client 193 | .say( 194 | self.config.twitch.channel.to_owned(), 195 | "LiveU: Too hot to charge".to_string(), 196 | ) 197 | .await; 198 | } 199 | 200 | if battery.percentage == 100 201 | && !battery.charging 202 | && !battery.discharging 203 | && prev.charging 204 | && !prev.discharging 205 | { 206 | let _ = self 207 | .client 208 | .say( 209 | self.config.twitch.channel.to_owned(), 210 | "LiveU: Fully charged".to_string(), 211 | ) 212 | .await; 213 | } 214 | } 215 | 216 | pub async fn battery_percentage_message( 217 | &self, 218 | percentage: u8, 219 | current: &liveu::Battery, 220 | prev: &liveu::Battery, 221 | ) { 222 | if current.percentage == percentage && prev.percentage > percentage { 223 | let message = format!( 224 | "LiveU: Internal battery is at {}% and is {} charging", 225 | percentage, 226 | if current.charging { "" } else { "not" } 227 | ); 228 | 229 | let _ = self 230 | .client 231 | .say(self.config.twitch.channel.to_owned(), message) 232 | .await; 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use liveu_stats_bot::{config::Config, liveu::Liveu, liveu_monitor::Monitor, twitch::Twitch}; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | println!("Started liveu stats bot v{}", env!("CARGO_PKG_VERSION")); 7 | 8 | let config = match Config::load("config.json") { 9 | Ok(c) => c, 10 | Err(_) => Config::ask_for_settings().await?, 11 | }; 12 | 13 | println!("Liveu: Authenticating..."); 14 | let liveu = Liveu::authenticate(config.liveu.clone()) 15 | .await 16 | .context("Failed to authenticate. Are your login details correct?")?; 17 | println!("Liveu: Authenticated"); 18 | 19 | let liveu_boss_id = if let Some(boss_id) = &config.liveu.id { 20 | boss_id.to_owned() 21 | } else { 22 | let inventories = liveu 23 | .get_inventories() 24 | .await 25 | .context("Error getting inventories")?; 26 | let loc = Liveu::get_boss_id_location(&inventories); 27 | inventories.units[loc].id.to_owned() 28 | }; 29 | 30 | println!("\nTwitch: Connecting..."); 31 | let (twitch_client, twitch_join_handle) = 32 | Twitch::run(config.clone(), liveu.clone(), liveu_boss_id.to_owned()); 33 | println!("Twitch: Connected"); 34 | 35 | { 36 | let monitor = Monitor { 37 | client: twitch_client.clone(), 38 | config: config.clone(), 39 | liveu: liveu.clone(), 40 | boss_id: liveu_boss_id.to_owned(), 41 | }; 42 | 43 | if config.liveu.monitor.modems { 44 | println!("Liveu: monitoring modems"); 45 | let modems = monitor.clone(); 46 | tokio::spawn(async move { modems.monitor_modems().await }); 47 | } 48 | 49 | if config.liveu.monitor.battery { 50 | println!("Liveu: monitoring battery"); 51 | let battery = monitor; 52 | tokio::spawn(async move { battery.monitor_battery().await }); 53 | } 54 | } 55 | 56 | twitch_join_handle.await?; 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/nginx.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::{config, error::Error}; 4 | 5 | #[derive(Deserialize, Debug)] 6 | struct NginxRtmpStats { 7 | server: NginxRtmpServer, 8 | } 9 | 10 | #[derive(Deserialize, Debug)] 11 | struct NginxRtmpServer { 12 | application: Vec, 13 | } 14 | 15 | #[derive(Deserialize, Debug)] 16 | struct NginxRtmpApp { 17 | name: String, 18 | live: NginxRtmpLive, 19 | } 20 | 21 | #[derive(Deserialize, Debug)] 22 | struct NginxRtmpLive { 23 | stream: Option>, 24 | } 25 | 26 | #[derive(Deserialize, Debug)] 27 | struct NginxRtmpStream { 28 | name: String, 29 | bw_video: u32, 30 | } 31 | 32 | pub async fn get_rtmp_bitrate(config: &config::Rtmp) -> Result, Error> { 33 | let res = reqwest::get(&config.url).await?; 34 | 35 | if res.status() != reqwest::StatusCode::OK { 36 | return Err(Error::RtmpDown("Can't connect to RTMP stats".to_owned())); 37 | } 38 | 39 | let text = res.text().await?; 40 | let parsed: NginxRtmpStats = quick_xml::de::from_str(&text)?; 41 | 42 | let filter: Option = parsed 43 | .server 44 | .application 45 | .into_iter() 46 | .filter_map(|x| { 47 | if x.name == config.application { 48 | x.live.stream 49 | } else { 50 | None 51 | } 52 | }) 53 | .flatten() 54 | .filter(|x| x.name == config.key) 55 | .collect::>() 56 | .pop(); 57 | 58 | Ok(match filter { 59 | Some(stream) => Some(stream.bw_video / 1024), 60 | None => None, 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /src/twitch.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config, 3 | error::Error, 4 | liveu::{self, Liveu}, 5 | nginx, 6 | }; 7 | use std::sync::{ 8 | atomic::{AtomicBool, Ordering}, 9 | Arc, 10 | }; 11 | use twitch_irc::{ 12 | login::StaticLoginCredentials, 13 | message, 14 | transport::tcp::{TCPTransport, TLS}, 15 | ClientConfig, TwitchIRCClient, 16 | }; 17 | 18 | const OFFLINE_MSG: &str = "LiveU Offline :("; 19 | 20 | pub struct Twitch { 21 | client: TwitchIRCClient, StaticLoginCredentials>, 22 | liveu: Liveu, 23 | liveu_boss_id: String, 24 | config: config::Config, 25 | timeout: Arc, 26 | } 27 | 28 | impl Twitch { 29 | pub fn run( 30 | config: config::Config, 31 | liveu: Liveu, 32 | liveu_boss_id: String, 33 | ) -> ( 34 | TwitchIRCClient, StaticLoginCredentials>, 35 | tokio::task::JoinHandle<()>, 36 | ) { 37 | let config::Twitch { 38 | bot_username, 39 | bot_oauth, 40 | channel, 41 | mod_only, 42 | .. 43 | } = &config.twitch; 44 | 45 | let username = bot_username.to_lowercase(); 46 | let channel = channel.to_lowercase(); 47 | let mut oauth = bot_oauth.to_owned(); 48 | 49 | if let Some(strip_oauth) = oauth.strip_prefix("oauth:") { 50 | oauth = strip_oauth.to_string(); 51 | } 52 | 53 | let twitch_credentials = StaticLoginCredentials::new(username, Some(oauth)); 54 | let twitch_config = ClientConfig::new_simple(twitch_credentials); 55 | let (mut incoming_messages, client) = 56 | TwitchIRCClient::, StaticLoginCredentials>::new(twitch_config); 57 | 58 | client.join(channel); 59 | 60 | let mod_only = mod_only.to_owned(); 61 | let client_clone = client.clone(); 62 | let join_handler = tokio::spawn(async move { 63 | let t = Self { 64 | client: client_clone, 65 | liveu, 66 | liveu_boss_id, 67 | config, 68 | timeout: Arc::new(AtomicBool::new(false)), 69 | }; 70 | 71 | while let Some(message) = incoming_messages.recv().await { 72 | t.handle_chat(message, &mod_only).await; 73 | } 74 | }); 75 | 76 | (client, join_handler) 77 | } 78 | 79 | async fn handle_chat(&self, message: message::ServerMessage, mod_only: &bool) { 80 | let timeout = self.timeout.clone(); 81 | if timeout.load(Ordering::Acquire) { 82 | return; 83 | } 84 | 85 | match message { 86 | message::ServerMessage::Notice(msg) => { 87 | if msg.message_text == "Login authentication failed" { 88 | panic!("Twitch authentication failed"); 89 | } 90 | } 91 | message::ServerMessage::Privmsg(msg) => { 92 | let is_owner = msg.badges.contains(&twitch_irc::message::Badge { 93 | name: "broadcaster".to_string(), 94 | version: "1".to_string(), 95 | }); 96 | 97 | let is_mod = msg.badges.contains(&twitch_irc::message::Badge { 98 | name: "moderator".to_string(), 99 | version: "1".to_string(), 100 | }); 101 | 102 | let mut user_has_permission = false; 103 | 104 | if let Some(users) = &self.config.twitch.admin_users { 105 | for user in users { 106 | if user.to_lowercase() == msg.sender.login { 107 | user_has_permission = true; 108 | break; 109 | } 110 | } 111 | }; 112 | 113 | if *mod_only && !(is_owner || is_mod || user_has_permission) { 114 | return; 115 | } 116 | 117 | let command = msg 118 | .message_text 119 | .split_ascii_whitespace() 120 | .next() 121 | .unwrap_or("") 122 | .to_string(); 123 | 124 | let command = self.get_command(command); 125 | 126 | if command == Command::Unknown { 127 | return; 128 | } 129 | 130 | let cooldown = self.config.commands.cooldown; 131 | 132 | tokio::spawn(async move { 133 | timeout.store(true, Ordering::Release); 134 | tokio::time::sleep(tokio::time::Duration::from_secs(cooldown as u64)).await; 135 | timeout.store(false, Ordering::Release); 136 | }); 137 | 138 | let res = if command == Command::Stats || command == Command::Battery { 139 | self.handle_non_permission_commands(command).await 140 | } else { 141 | if !(is_owner || user_has_permission) { 142 | return; 143 | } 144 | 145 | self.handle_permission_commands(command, msg.channel_login.to_owned()) 146 | .await 147 | }; 148 | 149 | if let Ok(res) = res { 150 | let _ = self.client.say(msg.channel_login.to_owned(), res).await; 151 | } 152 | } 153 | _ => {} 154 | }; 155 | } 156 | 157 | async fn handle_non_permission_commands(&self, command: Command) -> Result { 158 | match command { 159 | Command::Stats => self.generate_liveu_modems_message().await, 160 | Command::Battery => self.generate_liveu_battery_message().await, 161 | _ => unreachable!(), 162 | } 163 | } 164 | 165 | async fn handle_permission_commands( 166 | &self, 167 | command: Command, 168 | channel: String, 169 | ) -> Result { 170 | match command { 171 | Command::Start => self.generate_liveu_start_message(channel).await, 172 | Command::Stop => self.generate_liveu_stop_message(channel).await, 173 | Command::Restart => self.generate_liveu_restart_message(channel).await, 174 | Command::Reboot => self.generate_liveu_reboot_message(channel).await, 175 | Command::Delay => self.toggle_delay(channel).await, 176 | _ => unreachable!(), 177 | } 178 | } 179 | 180 | // TODO: This needs a refactor 181 | fn get_command(&self, command: String) -> Command { 182 | let config::Commands { 183 | stats, 184 | battery, 185 | start, 186 | stop, 187 | restart, 188 | reboot, 189 | delay, 190 | .. 191 | } = &self.config.commands; 192 | 193 | if stats.contains(&command) { 194 | return Command::Stats; 195 | } 196 | 197 | if battery.contains(&command) { 198 | return Command::Battery; 199 | } 200 | 201 | if start == &command { 202 | return Command::Start; 203 | } 204 | 205 | if stop == &command { 206 | return Command::Stop; 207 | } 208 | 209 | if restart == &command { 210 | return Command::Restart; 211 | } 212 | 213 | if reboot == &command { 214 | return Command::Reboot; 215 | } 216 | 217 | if delay == &command { 218 | return Command::Delay; 219 | } 220 | 221 | Command::Unknown 222 | } 223 | 224 | async fn generate_liveu_modems_message(&self) -> Result { 225 | let interfaces: Vec = self 226 | .liveu 227 | .get_unit_custom_names(&self.liveu_boss_id, self.config.custom_port_names.clone()) 228 | .await?; 229 | 230 | if interfaces.is_empty() { 231 | return Ok(OFFLINE_MSG.to_string()); 232 | } 233 | 234 | let mut message = String::new(); 235 | let mut total_bitrate = 0; 236 | 237 | for interface in interfaces.iter() { 238 | message = message 239 | + &format!( 240 | "{}: {} Kbps{}{}, ", 241 | interface.port, 242 | interface.uplink_kbps, 243 | if !interface.technology.is_empty() { 244 | format!(" ({})", &interface.technology) 245 | } else { 246 | "".to_string() 247 | }, 248 | if interface.is_currently_roaming { 249 | " roaming" 250 | } else { 251 | "" 252 | } 253 | ); 254 | total_bitrate += interface.uplink_kbps; 255 | } 256 | 257 | if total_bitrate == 0 { 258 | return Ok("LiveU Online and Ready".to_string()); 259 | } 260 | 261 | message += &format!("Total LRT: {} Kbps", total_bitrate); 262 | 263 | if let Some(rtmp) = &self.config.rtmp { 264 | if let Ok(Some(bitrate)) = nginx::get_rtmp_bitrate(rtmp).await { 265 | message += &format!(", RTMP: {} Kbps", bitrate); 266 | }; 267 | } 268 | 269 | Ok(message) 270 | } 271 | 272 | async fn generate_liveu_battery_message(&self) -> Result { 273 | let battery = match self.liveu.get_battery(&self.liveu_boss_id).await { 274 | Ok(b) => b, 275 | Err(_) => return Ok(OFFLINE_MSG.to_string()), 276 | }; 277 | 278 | let estimated_battery_time = { 279 | if battery.run_time_to_empty != 0 && battery.discharging { 280 | let hours = battery.run_time_to_empty / 60; 281 | let minutes = battery.run_time_to_empty % 60; 282 | let mut time_string = String::new(); 283 | 284 | if hours != 0 { 285 | time_string += &format!("{}h", hours); 286 | } 287 | 288 | time_string += &format!(" {}m", minutes); 289 | format!("Estimated battery time: {}", time_string) 290 | } else { 291 | "".to_string() 292 | } 293 | }; 294 | 295 | let charging = { 296 | if battery.charging { 297 | "charging".to_string() 298 | } else if battery.percentage == 100 { 299 | let mut s = "fully charged".to_string(); 300 | 301 | if battery.connected { 302 | s += ", connected" 303 | } 304 | 305 | s 306 | } else if battery.percentage < 100 && !battery.charging && !battery.discharging { 307 | "too hot to charge".to_string() 308 | } else { 309 | "not charging".to_string() 310 | } 311 | }; 312 | 313 | let message = format!( 314 | "LiveU Internal Battery: {}% {} {}", 315 | battery.percentage, charging, estimated_battery_time 316 | ); 317 | 318 | Ok(message) 319 | } 320 | 321 | async fn generate_liveu_start_message(&self, channel: String) -> Result { 322 | let video = self.liveu.get_video(&self.liveu_boss_id).await; 323 | 324 | let video = match video { 325 | Ok(video) => video, 326 | Err(_) => return Ok(OFFLINE_MSG.to_string()), 327 | }; 328 | 329 | if video.resolution.is_none() { 330 | return Ok("LiveU no camera plugged in".to_string()); 331 | } 332 | 333 | if video.bitrate.is_some() { 334 | return Ok("LiveU already streaming".to_string()); 335 | } 336 | 337 | if self.liveu.start_stream(&self.liveu_boss_id).await.is_err() { 338 | return Ok("LiveU request error".to_string()); 339 | }; 340 | 341 | let confirm = DataUsedInThread { 342 | chat: self.client.clone(), 343 | liveu: self.liveu.clone(), 344 | boss_id: self.liveu_boss_id.to_owned(), 345 | channel, 346 | }; 347 | 348 | tokio::spawn(async move { 349 | confirm 350 | .confirm_action(15, true, "started".to_string(), "starting".to_string()) 351 | .await 352 | }); 353 | 354 | Ok("LiveU starting stream".to_string()) 355 | } 356 | 357 | async fn generate_liveu_stop_message(&self, channel: String) -> Result { 358 | if !self.liveu.is_streaming(&self.liveu_boss_id).await { 359 | return Ok("LiveU already stopped".to_string()); 360 | } 361 | 362 | if self.liveu.stop_stream(&self.liveu_boss_id).await.is_err() { 363 | return Ok("LiveU request error".to_string()); 364 | }; 365 | 366 | let confirm = DataUsedInThread { 367 | chat: self.client.clone(), 368 | liveu: self.liveu.clone(), 369 | boss_id: self.liveu_boss_id.to_owned(), 370 | channel, 371 | }; 372 | 373 | tokio::spawn(async move { 374 | confirm 375 | .confirm_action(10, false, "stopped".to_string(), "stopping".to_string()) 376 | .await 377 | }); 378 | 379 | Ok("LiveU stopping stream".to_string()) 380 | } 381 | 382 | async fn generate_liveu_restart_message(&self, channel: String) -> Result { 383 | if !self.liveu.is_streaming(&self.liveu_boss_id).await { 384 | return Ok("LiveU not streaming".to_string()); 385 | } 386 | 387 | let msg = "LiveU stream restarting".to_string(); 388 | let _ = self.client.say(channel.to_owned(), msg).await; 389 | 390 | self.generate_liveu_stop_message(channel.to_owned()).await?; 391 | tokio::time::sleep(tokio::time::Duration::from_secs(4)).await; 392 | self.generate_liveu_start_message(channel.to_owned()) 393 | .await?; 394 | 395 | Ok(String::new()) 396 | } 397 | 398 | async fn generate_liveu_reboot_message(&self, channel: String) -> Result { 399 | let is_streaming = self.liveu.is_streaming(&self.liveu_boss_id).await; 400 | 401 | let msg = "LiveU Rebooting, please wait approximately 2-3 minutes".to_string(); 402 | let _ = self.client.say(channel.to_owned(), msg).await; 403 | 404 | if is_streaming { 405 | self.generate_liveu_stop_message(channel.to_owned()).await?; 406 | tokio::time::sleep(tokio::time::Duration::from_secs(4)).await; 407 | } 408 | 409 | self.liveu.reboot_unit(&self.liveu_boss_id).await?; 410 | tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; 411 | 412 | let mut attempts = 0; 413 | let max_attempts = 20; 414 | 415 | while !self.liveu.is_idle(&self.liveu_boss_id).await && attempts != max_attempts { 416 | tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; 417 | attempts += 1; 418 | } 419 | 420 | if attempts == max_attempts { 421 | return Ok("LiveU took too long to reboot".to_string()); 422 | } 423 | 424 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; 425 | 426 | if is_streaming { 427 | self.generate_liveu_start_message(channel.to_owned()) 428 | .await?; 429 | return Ok(String::new()); 430 | } 431 | 432 | Ok("LiveU rebooted successfully".to_string()) 433 | } 434 | 435 | async fn toggle_delay(&self, channel: String) -> Result { 436 | let is_streaming = self.liveu.is_streaming(&self.liveu_boss_id).await; 437 | 438 | if is_streaming { 439 | self.generate_liveu_stop_message(channel.to_owned()).await?; 440 | tokio::time::sleep(tokio::time::Duration::from_secs(4)).await; 441 | } 442 | 443 | let current_delay = self.liveu.get_delay(&self.liveu_boss_id).await?; 444 | let delay = if current_delay.delay == 1000 { 445 | (5000, "LiveU high resiliency mode") 446 | } else { 447 | (1000, "LiveU low delay mode") 448 | }; 449 | 450 | self.liveu.set_delay(&self.liveu_boss_id, delay.0).await?; 451 | tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; 452 | 453 | if is_streaming { 454 | self.generate_liveu_start_message(channel.to_owned()) 455 | .await?; 456 | } 457 | 458 | Ok(delay.1.to_string()) 459 | } 460 | } 461 | 462 | #[derive(PartialEq, Eq)] 463 | enum Command { 464 | Stats, 465 | Battery, 466 | Start, 467 | Stop, 468 | Restart, 469 | Reboot, 470 | Delay, 471 | Unknown, 472 | } 473 | 474 | struct DataUsedInThread { 475 | chat: TwitchIRCClient, StaticLoginCredentials>, 476 | liveu: Liveu, 477 | boss_id: String, 478 | channel: String, 479 | } 480 | 481 | impl DataUsedInThread { 482 | async fn confirm_action( 483 | &self, 484 | max_attempts: u8, 485 | should_have_bitrate: bool, 486 | success: String, 487 | not_success: String, 488 | ) { 489 | let mut attempts = 0; 490 | 491 | while attempts != max_attempts { 492 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 493 | 494 | let video = self.liveu.get_video(&self.boss_id).await; 495 | 496 | if let Ok(video) = video { 497 | if video.bitrate.is_some() == should_have_bitrate { 498 | break; 499 | } 500 | } 501 | 502 | attempts += 1; 503 | } 504 | 505 | if attempts == max_attempts { 506 | let msg = format!( 507 | "LiveU {} stream took too long might not have worked", 508 | not_success 509 | ); 510 | let _ = self.chat.say(self.channel.to_owned(), msg).await; 511 | 512 | return; 513 | } 514 | 515 | let msg = format!("LiveU streaming {} successfully", success); 516 | let _ = self.chat.say(self.channel.to_owned(), msg).await; 517 | } 518 | } 519 | --------------------------------------------------------------------------------