commit 7cf731d50992befd535e642cd2e1c05d4c31a588
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Wed, 19 Jan 2022 19:36:14 -0800
init
Diffstat:
11 files changed, 6319 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,7 @@
+captcha.gif
+samples
+target
+dist
+*.log
+*.svg
+*.env
diff --git a/Cargo.lock b/Cargo.lock
@@ -0,0 +1,3067 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alsa"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18"
+dependencies = [
+ "alsa-sys",
+ "bitflags",
+ "libc",
+ "nix",
+]
+
+[[package]]
+name = "alsa-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base-x"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bhcli"
+version = "0.1.0"
+dependencies = [
+ "base64",
+ "chrono",
+ "clap 3.0.0-beta.4",
+ "clipboard",
+ "colors-transform",
+ "confy",
+ "crossbeam",
+ "crossbeam-channel",
+ "crossterm 0.21.0",
+ "http",
+ "image 0.23.14",
+ "lazy_static",
+ "linkify",
+ "rand 0.8.4",
+ "regex",
+ "reqwest",
+ "rodio",
+ "rpassword",
+ "select",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "termage",
+ "textwrap 0.14.2",
+ "toml",
+ "tui",
+ "unicode-width",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "peeking_take_while",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "regex",
+ "rustc-hash",
+ "shlex",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "bumpalo"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
+
+[[package]]
+name = "bytemuck"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "cc"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
+dependencies = [
+ "jobserver",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cexpr"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "time 0.1.43",
+ "winapi",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cf2cc85830eae84823884db23c5306442a6c3d5bfd3beb2f2a2c829faa1816"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim 0.8.0",
+ "textwrap 0.11.0",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "clap"
+version = "3.0.0-beta.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcd70aa5597dbc42f7217a543f9ef2768b2ef823ba29036072d30e1d88e98406"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "indexmap",
+ "lazy_static",
+ "os_str_bytes",
+ "strsim 0.10.0",
+ "termcolor",
+ "textwrap 0.14.2",
+ "vec_map",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.0.0-beta.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5bb0d655624a0b8770d1c178fb8ffcb1f91cc722cb08f451e3dc72465421ac"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "claxon"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
+
+[[package]]
+name = "clipboard"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7"
+dependencies = [
+ "clipboard-win",
+ "objc",
+ "objc-foundation",
+ "objc_id",
+ "x11-clipboard",
+]
+
+[[package]]
+name = "clipboard-win"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colors-transform"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9226dbc05df4fb986f48d730b001532580883c4c06c5d1c213f4b34c1c157178"
+
+[[package]]
+name = "combine"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "confy"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2913470204e9e8498a0f31f17f90a0de801ae92c8c5ac18c49af4819e6786697"
+dependencies = [
+ "directories",
+ "serde",
+ "toml",
+]
+
+[[package]]
+name = "const_fn"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
+
+[[package]]
+name = "cookie"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951"
+dependencies = [
+ "percent-encoding",
+ "time 0.2.27",
+ "version_check",
+]
+
+[[package]]
+name = "cookie_store"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3"
+dependencies = [
+ "cookie",
+ "idna",
+ "log",
+ "publicsuffix",
+ "serde",
+ "serde_json",
+ "time 0.2.27",
+ "url",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
+
+[[package]]
+name = "coreaudio-rs"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88"
+dependencies = [
+ "bitflags",
+ "coreaudio-sys",
+]
+
+[[package]]
+name = "coreaudio-sys"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa"
+dependencies = [
+ "bindgen",
+]
+
+[[package]]
+name = "cpal"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418"
+dependencies = [
+ "alsa",
+ "core-foundation-sys",
+ "coreaudio-rs",
+ "jni",
+ "js-sys",
+ "lazy_static",
+ "libc",
+ "mach",
+ "ndk 0.3.0",
+ "ndk-glue 0.3.0",
+ "nix",
+ "oboe",
+ "parking_lot",
+ "stdweb 0.1.3",
+ "thiserror",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "crossbeam"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+ "lazy_static",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
+dependencies = [
+ "cfg-if 1.0.0",
+ "lazy_static",
+]
+
+[[package]]
+name = "crossterm"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "darling"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "strsim 0.9.3",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
+dependencies = [
+ "darling_core",
+ "quote 1.0.9",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "deflate"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
+dependencies = [
+ "adler32",
+ "byteorder",
+]
+
+[[package]]
+name = "deflate"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
+dependencies = [
+ "adler32",
+ "byteorder",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "directories"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c"
+dependencies = [
+ "cfg-if 0.1.10",
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "discard"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
+
+[[package]]
+name = "futures-io"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
+
+[[package]]
+name = "futures-task"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2"
+
+[[package]]
+name = "futures-util"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
+dependencies = [
+ "autocfg",
+ "futures-core",
+ "futures-io",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.10.2+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "gif"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af"
+dependencies = [
+ "color_quant",
+ "lzw",
+]
+
+[[package]]
+name = "gif"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
+[[package]]
+name = "h2"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hound"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549"
+
+[[package]]
+name = "html5ever"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "http"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
+
+[[package]]
+name = "httpdate"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
+
+[[package]]
+name = "hyper"
+version = "0.14.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13f67199e765030fa08fe0bd581af683f0d5bc04ea09c2b1102012c5fb90e7fd"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "image"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebdff791af04e30089bde8ad2a632b86af433b40c04db8d70ad4b21487db7a6a"
+dependencies = [
+ "byteorder",
+ "gif 0.10.3",
+ "jpeg-decoder",
+ "lzw",
+ "num-derive 0.2.5",
+ "num-iter",
+ "num-rational 0.1.42",
+ "num-traits",
+ "png 0.12.0",
+ "scoped_threadpool",
+]
+
+[[package]]
+name = "image"
+version = "0.23.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "gif 0.11.2",
+ "jpeg-decoder",
+ "num-iter",
+ "num-rational 0.3.2",
+ "num-traits",
+ "png 0.16.8",
+ "scoped_threadpool",
+ "tiff",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "inflate"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff"
+dependencies = [
+ "adler32",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "jni"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "jobserver"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "jpeg-decoder"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+dependencies = [
+ "rayon",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "lewton"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
+dependencies = [
+ "byteorder",
+ "ogg",
+ "tinyvec",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
+
+[[package]]
+name = "libloading"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
+dependencies = [
+ "cfg-if 1.0.0",
+ "winapi",
+]
+
+[[package]]
+name = "linkify"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04d828fdc1ffceb369a5a9183bd4df2dbb3678f40c8b3fbaa9231de32beb29f9"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "lzw"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "mach"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "markup5ever"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
+dependencies = [
+ "log",
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "markup5ever_rcdom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b"
+dependencies = [
+ "html5ever",
+ "markup5ever",
+ "tendril",
+ "xml5ever",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "memoffset"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minimp3"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372"
+dependencies = [
+ "minimp3-sys",
+ "slice-deque",
+ "thiserror",
+]
+
+[[package]]
+name = "minimp3-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
+dependencies = [
+ "adler32",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
+name = "mio"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ndk"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab"
+dependencies = [
+ "jni-sys",
+ "ndk-sys",
+ "num_enum",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c"
+dependencies = [
+ "bitflags",
+ "jni-sys",
+ "ndk-sys",
+ "num_enum",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-glue"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk 0.3.0",
+ "ndk-macro",
+ "ndk-sys",
+]
+
+[[package]]
+name = "ndk-glue"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk 0.4.0",
+ "ndk-macro",
+ "ndk-sys",
+]
+
+[[package]]
+name = "ndk-macro"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d"
+dependencies = [
+ "darling",
+ "proc-macro-crate 0.1.5",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d"
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+
+[[package]]
+name = "nix"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "5.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
+dependencies = [
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational 0.4.0",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76e97c412795abf6c24ba30055a8f20642ea57ca12875220b854cfa501bf1e48"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eafd0b45c5537c3ba526f79d3e75120036502bebacbb3f3220914067ce39dbf2"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.1.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a"
+dependencies = [
+ "autocfg",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f"
+dependencies = [
+ "derivative",
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9"
+dependencies = [
+ "proc-macro-crate 1.0.0",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "objc-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
+dependencies = [
+ "block",
+ "objc",
+ "objc_id",
+]
+
+[[package]]
+name = "objc_id"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
+dependencies = [
+ "objc",
+]
+
+[[package]]
+name = "oboe"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1"
+dependencies = [
+ "jni",
+ "ndk 0.4.0",
+ "ndk-glue 0.4.0",
+ "num-derive 0.3.3",
+ "num-traits",
+ "oboe-sys",
+]
+
+[[package]]
+name = "oboe-sys"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ogg"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "openssl"
+version = "0.10.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a"
+dependencies = [
+ "bitflags",
+ "cfg-if 1.0.0",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
+
+[[package]]
+name = "png"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f54b9600d584d3b8a739e1662a595fab051329eff43f20e7d8cc22872962145b"
+dependencies = [
+ "bitflags",
+ "deflate 0.7.20",
+ "inflate",
+ "num-iter",
+]
+
+[[package]]
+name = "png"
+version = "0.16.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "deflate 0.8.6",
+ "miniz_oxide 0.3.7",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
+dependencies = [
+ "toml",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92"
+dependencies = [
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-macro2"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
+dependencies = [
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
+dependencies = [
+ "unicode-xid 0.2.2",
+]
+
+[[package]]
+name = "publicsuffix"
+version = "1.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95b4ce31ff0a27d93c8de1849cf58162283752f065a90d508f1105fa6c9a213f"
+dependencies = [
+ "idna",
+ "url",
+]
+
+[[package]]
+name = "quote"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
+dependencies = [
+ "proc-macro2 0.4.30",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2 1.0.28",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc 0.2.0",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.3",
+ "rand_hc 0.3.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom 0.2.3",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rayon"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
+dependencies = [
+ "autocfg",
+ "crossbeam-deque",
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "lazy_static",
+ "num_cpus",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+dependencies = [
+ "getrandom 0.2.3",
+ "redox_syscall",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22"
+dependencies = [
+ "base64",
+ "bytes",
+ "cookie",
+ "cookie_store",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "mime_guess",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_urlencoded",
+ "time 0.2.27",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-socks",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "rodio"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9"
+dependencies = [
+ "claxon",
+ "cpal",
+ "hound",
+ "lewton",
+ "minimp3",
+]
+
+[[package]]
+name = "rpassword"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "scoped_threadpool"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "security-framework"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b9bd29cdffb8875b04f71c51058f940cf4e390bbfd2ce669c4f22cd70b492a5"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "num",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19133a286e494cc3311c165c4676ccb1fd47bed45b55f9d71fbd784ad4cea6f8"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "select"
+version = "0.6.0-alpha.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b51951aa2af52d64920acfe8b0df8d8f4071811a9e575db56a037dd5ee3b894"
+dependencies = [
+ "bit-set",
+ "html5ever",
+ "markup5ever_rcdom",
+]
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+dependencies = [
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
+
+[[package]]
+name = "shlex"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2"
+
+[[package]]
+name = "signal-hook"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1"
+
+[[package]]
+name = "slab"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
+
+[[package]]
+name = "slice-deque"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25"
+dependencies = [
+ "libc",
+ "mach",
+ "winapi",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
+
+[[package]]
+name = "smawk"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
+
+[[package]]
+name = "socket2"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "standback"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "stdweb"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e"
+
+[[package]]
+name = "stdweb"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
+dependencies = [
+ "discard",
+ "rustc_version",
+ "stdweb-derive",
+ "stdweb-internal-macros",
+ "stdweb-internal-runtime",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "stdweb-derive"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
+dependencies = [
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "serde",
+ "serde_derive",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "stdweb-internal-macros"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
+dependencies = [
+ "base-x",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "stdweb-internal-runtime"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
+
+[[package]]
+name = "string_cache"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a"
+dependencies = [
+ "lazy_static",
+ "new_debug_unreachable",
+ "phf_shared",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+]
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "strsim"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "0.15.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7"
+dependencies = [
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "unicode-xid 0.2.2",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "rand 0.8.4",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "termage"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "462dc01ca2f07b9bacbd0dc2411f1cb810b6d615d530d72300980a2a1e1347bd"
+dependencies = [
+ "clap 2.33.3",
+ "image 0.19.0",
+ "terminal_graphics",
+ "terminal_size",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_graphics"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3585912e123cb72d56d923dde7419b329481d4ed76cb01d8c0d733281484edb"
+
+[[package]]
+name = "terminal_size"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71"
+dependencies = [
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "tiff"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
+dependencies = [
+ "jpeg-decoder",
+ "miniz_oxide 0.4.4",
+ "weezl",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "time"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
+dependencies = [
+ "const_fn",
+ "libc",
+ "standback",
+ "stdweb 0.4.20",
+ "time-macros",
+ "version_check",
+ "winapi",
+]
+
+[[package]]
+name = "time-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
+dependencies = [
+ "proc-macro-hack",
+ "time-macros-impl",
+]
+
+[[package]]
+name = "time-macros-impl"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "standback",
+ "syn 1.0.75",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-socks"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
+dependencies = [
+ "either",
+ "futures-util",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
+dependencies = [
+ "cfg-if 1.0.0",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "tui"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23"
+dependencies = [
+ "bitflags",
+ "cassowary",
+ "crossterm 0.20.0",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
+dependencies = [
+ "regex",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
+[[package]]
+name = "unicode-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0"
+dependencies = [
+ "cfg-if 1.0.0",
+ "serde",
+ "serde_json",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95fded345a6559c2cfee778d562300c581f7d4ff3edb9b0d230d69800d213972"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef"
+dependencies = [
+ "quote 1.0.9",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad"
+dependencies = [
+ "proc-macro2 1.0.28",
+ "quote 1.0.9",
+ "syn 1.0.75",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29"
+
+[[package]]
+name = "web-sys"
+version = "0.3.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winreg"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "x11-clipboard"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea"
+dependencies = [
+ "xcb",
+]
+
+[[package]]
+name = "xcb"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de"
+dependencies = [
+ "libc",
+ "log",
+]
+
+[[package]]
+name = "xml5ever"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "time 0.1.43",
+]
diff --git a/Cargo.toml b/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "bhcli"
+version = "0.1.0"
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+base64 = "0.13.0"
+chrono = "0.4.19"
+clap = "3.0.0-beta.4"
+clipboard = "0.5.0"
+colors-transform = "0.2.4"
+confy = "0.4.0"
+crossbeam = "0.8.1"
+crossbeam-channel = "0.5.1"
+crossterm = { version = "0.21.0" }
+http = "0.2.4"
+image = "0.23.14"
+lazy_static = "1.4.0"
+linkify = "0.7.0"
+rand = "0.8.4"
+regex = "1.5.4"
+reqwest = { version = "0.11.4", features = ["blocking", "cookies", "socks", "multipart"] }
+rodio = "0.14.0"
+rpassword = "5.0.1"
+select = "0.6.0-alpha.1"
+serde = "1.0.130"
+serde_derive = "1.0.88"
+serde_json = "1.0"
+termage = "1.1.1"
+textwrap = "0.14.2"
+toml = "0.5.8"
+tui = { version = "0.16.0", features = ["crossterm"], default-features = false }
+unicode-width = "0.1.8"
+\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
@@ -0,0 +1,3 @@
+FROM rust:1.54 as bhcli-builder
+RUN apt-get update
+RUN apt-get install -y pkg-config libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev libx11-dev
diff --git a/Makefile b/Makefile
@@ -0,0 +1,37 @@
+PWD = $(shell pwd)
+
+build-docker-bin:
+ docker run --rm -it -v $(PWD):/Documents/bhcli -w /Documents/bhcli bhcli sh -c \
+ 'CARGO_TARGET_DIR=./target/linux cargo build --release'
+
+build-darwin:
+ cargo build --release
+ cp target/release/bhcli dist/bhcli.darwin.amd64
+ tar -czvf dist/bhcli.darwin.amd64.tar.gz dist/bhcli.darwin.amd64
+ openssl dgst -sha256 dist/bhcli.darwin.amd64.tar.gz | cut -d ' ' -f 2 > dist/bhcli.darwin.amd64.tar.gz.checksum
+ rm dist/bhcli.darwin.amd64
+
+build-linux: build-docker-bin
+ cp target/linux/release/bhcli dist/bhcli.linux.amd64
+ tar -czvf dist/bhcli.linux.amd64.tar.gz dist/bhcli.linux.amd64
+ openssl dgst -sha256 dist/bhcli.linux.amd64.tar.gz | cut -d ' ' -f 2 > dist/bhcli.linux.amd64.tar.gz.checksum
+ rm dist/bhcli.linux.amd64
+
+cross-compile-windows:
+ cargo build --release --target x86_64-pc-windows-gnu
+ cp target/x86_64-pc-windows-gnu/release/bhcli.exe dist/bhcli.windows.amd64.exe
+ zip dist/bhcli.windows.amd64.zip dist/bhcli.windows.amd64.exe
+ openssl dgst -sha256 dist/bhcli.windows.amd64.zip | cut -d ' ' -f 2 > dist/bhcli.windows.amd64.zip.checksum
+ rm dist/bhcli.windows.amd64.exe
+
+process-windows:
+ zip dist/bhcli.windows.amd64.zip dist/bhcli.exe
+ openssl dgst -sha256 dist/bhcli.windows.amd64.zip | cut -d ' ' -f 2 > dist/bhcli.windows.amd64.zip.checksum
+ rm dist/bhcli.exe
+
+rsync:
+ rsync --recursive --times --compress --progress dist/ torsv:/root/dist/downloads-bhcli
+
+deploy: build-darwin cross-compile-windows build-linux rsync
+
+.PHONY: build-darwin process-windows cross-compile-windows rsync
diff --git a/README.md b/README.md
@@ -0,0 +1,71 @@
+# BHCLI
+
+
+
+## Description
+
+This is a CLI client for any of [le-chat-php](https://github.com/DanWin/le-chat-php)
+currently supported chats are [Black Hat Chat](http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion) and
+[Daniel's chat](http://danschat356lctri3zavzh6fbxg2a7lo6z3etgkctzzpspewu7zdsaqd.onion)
+
+## Pre-built binaries
+
+Pre-buit binaries can be found on the [official website](http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/bhcli)
+
+## Features
+
+- Sound notifications when tagged/pmmed
+- Private messages `/pm username message`
+- Kick someone `/kick username message` | `/k username message`
+- Delete last message `/dl`
+- Delete last X message `/dl5` will delete the last 5 messages
+- Delete all messages `/dall`
+- Ignore someone `/ignore username`
+- Unignore someone `/unignore username`
+- Toggle notifications sound `m`
+- Toggle a "guest" view, by filtering out PMs and "Members chat" `shift+G`
+- Filter messages `/f terms`
+- Copy a selected message to clipboard `ctrl+C` | `y`
+- Copy the first link in a message to clipboard `shift+Y`
+- Directly tag author of selected message `t` will prefil the input with `@username `
+- Directly private message author of selected message `p` will prefil the input with `/pm username `
+- Shortcut to kick author of selected message `ctrl+k` will prefil the input with `/kick username `
+- captcha is displayed directly in terminal 10 times the real size
+- Upload file `/u C:\path\to\file.png @username message` (@username is optional) `@members` for members group
+- `<tab>` to autocomplete usernames while typing
+
+### Editing mode
+- `ctrl+A` Move cursor to start of line
+- `ctrl+E` Move cursor to end of line
+- `ctrl+F` Move cursor a word forward
+- `ctrl+B` Move cursor a word backward
+
+### Messages navigation
+- Page down the messages list `ctrl+D` | `page down`
+- Page up the messages list `ctrl+U` | `page up`
+- Going down 1 message `j` | `down arrow`
+- Going up 1 message `k` | `up arrow`
+
+## Build from source
+
+### Windows
+
+- Install C++ build tools https://visualstudio.microsoft.com/visual-cpp-build-tools/
+- Install Rust https://www.rust-lang.org/learn/get-started
+- Download & extract code source
+- Compile with `cargo build --release`
+
+### OSx
+
+- Install Rust https://www.rust-lang.org/learn/get-started
+- Compile with `cargo build --release`
+
+### Linux
+
+- Install Rust
+- Install dependencies `apt-get install -y pkg-config libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev libx11-dev`
+- Compile with `cargo build --release`
+
+## Cross compile
+
+`cargo build --release --target x86_64-pc-windows-gnu`
diff --git a/screenshot.png b/screenshot.png
Binary files differ.
diff --git a/src/main.rs b/src/main.rs
@@ -0,0 +1,3042 @@
+mod util;
+
+use base64::decode;
+use chrono::{DateTime, Datelike, NaiveDateTime, Utc};
+use clap::{AppSettings, Clap};
+use clipboard::ClipboardContext;
+use clipboard::ClipboardProvider;
+use colors_transform::{Color, Rgb};
+use crossbeam_channel::{self, after, select, Select};
+use crossterm::event;
+use crossterm::event::Event as CEvent;
+use crossterm::event::{MouseEvent, MouseEventKind};
+use crossterm::{
+ event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyModifiers},
+ execute,
+ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
+};
+use http::StatusCode;
+use image;
+use image::GenericImageView;
+use lazy_static::lazy_static;
+use linkify::LinkFinder;
+use rand::distributions::Alphanumeric;
+use rand::{thread_rng, Rng};
+use regex::Regex;
+use reqwest::blocking::multipart;
+use reqwest::blocking::Client;
+use rodio::{source::Source, Decoder, OutputStream};
+use select::document::Document;
+use select::predicate::{And, Attr, Name};
+use serde_derive::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::error;
+use std::fs;
+use std::io::Cursor;
+use std::io::{self, Write};
+use std::process;
+use std::sync::Arc;
+use std::sync::Mutex;
+use std::thread;
+use std::time;
+use std::time::Duration;
+use std::time::Instant;
+use termage;
+use textwrap;
+use tui::layout::Rect;
+use tui::style::Color as tuiColor;
+use tui::{
+ backend::CrosstermBackend,
+ layout::{Constraint, Direction, Layout},
+ style::{Modifier, Style},
+ text::{Span, Spans, Text},
+ widgets::{Block, Borders, List, ListItem, Paragraph},
+ Frame, Terminal,
+};
+use unicode_width::UnicodeWidthStr;
+use util::StatefulList;
+
+const LANG: &str = "en";
+const SEND_TO_ALL: &str = "s *";
+const SEND_TO_MEMBERS: &str = "s ?";
+const SEND_TO_STAFFS: &str = "s %";
+const SEND_TO_ADMINS: &str = "s _";
+const SOUND1: &[u8] = include_bytes!("sound1.mp3");
+const DKF_URL: &str = "http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion";
+const BHCLI_BLOG_URL: &str =
+ "http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/bhcli";
+const BAN_IMPOSTERS: bool = true;
+const SERVER_DOWN_500_ERR: &str = "500 Internal Server Error, server down";
+const SERVER_DOWN_ERR: &str = "502 Bad Gateway, server down";
+const KICKED_ERR: &str = "You have been kicked";
+const REG_ERR: &str = "This nickname is a registered member";
+const NICKNAME_ERR: &str = "Invalid nickname";
+const CAPTCHA_WG_ERR: &str = "Wrong Captcha";
+const CAPTCHA_USED_ERR: &str = "Captcha already used or timed out";
+const UNKNOWN_ERR: &str = "Unknown error";
+const N0TR1V: &str = "n0tr1v";
+const DNMX_URL: &str = "http://hxuzjtocnzvv5g2rtg2bhwkcbupmk7rclb6lly3fo4tvqkk5oyrv3nid.onion";
+
+type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
+
+lazy_static! {
+ static ref SESSION_RGX: Regex = Regex::new(r#"session=([^&]+)"#).unwrap();
+ static ref COLOR_RGX: Regex = Regex::new(r#"color:\s*([#\w]+)\s*;"#).unwrap();
+ static ref COLOR1_RGX: Regex = Regex::new(r#"^#([0-9A-Fa-f]{6})$"#).unwrap();
+ static ref PM_RGX: Regex = Regex::new(r#"^/pm ([^\s]+) (.*)"#).unwrap();
+ static ref KICK_RGX: Regex = Regex::new(r#"^/(?:kick|k) ([^\s]+)\s?(.*)"#).unwrap();
+ static ref IGNORE_RGX: Regex = Regex::new(r#"^/ignore ([^\s]+)"#).unwrap();
+ static ref UNIGNORE_RGX: Regex = Regex::new(r#"^/unignore ([^\s]+)"#).unwrap();
+ static ref DLX_RGX: Regex = Regex::new(r#"^/dl([\d]+)$"#).unwrap();
+ static ref UPLOAD_RGX: Regex = Regex::new(r#"^/u\s([^\s]+)\s?(?:@([^\s]+)\s)?(.*)$"#).unwrap();
+ static ref FIND_RGX: Regex = Regex::new(r#"^/f\s(.*)$"#).unwrap();
+ static ref NEW_NICKNAME_RGX: Regex = Regex::new(r#"^/nick\s(.*)$"#).unwrap();
+ static ref NEW_COLOR_RGX: Regex = Regex::new(r#"^/color\s(.*)$"#).unwrap();
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+enum Typ {
+ BHC,
+ Custom,
+}
+
+impl Typ {
+ fn bhc() -> Self {
+ Typ::BHC
+ }
+}
+
+fn default_empty_str() -> String {
+ "".to_string()
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct Profile {
+ username: String,
+ password: String,
+ #[serde(default = "Typ::bhc")]
+ typ: Typ,
+ #[serde(default = "default_empty_str")]
+ url: String,
+ #[serde(default = "default_empty_str")]
+ date_format: String,
+ #[serde(default = "default_empty_str")]
+ page_php: String,
+ #[serde(default = "default_empty_str")]
+ members_tag: String,
+ #[serde(default = "default_empty_str")]
+ keepalive_send_to: String,
+}
+
+#[derive(Default, Debug, Serialize, Deserialize)]
+struct MyConfig {
+ dkf_api_key: Option<String>,
+ profiles: HashMap<String, Profile>,
+}
+
+#[derive(Clap)]
+#[clap(
+ name = "bhcli",
+ version = "0.0.1",
+ author = "n0tr1v <n0tr1v@protonmail.com>"
+)]
+#[clap(setting = AppSettings::ColoredHelp)]
+struct Opts {
+ #[clap(long, env = "DKF_API_KEY")]
+ dkf_api_key: Option<String>,
+ #[clap(short, long, env = "BHC_USERNAME")]
+ username: Option<String>,
+ #[clap(short, long, env = "BHC_PASSWORD")]
+ password: Option<String>,
+ #[clap(short, long, env = "BHC_MANUAL_CAPTCHA")]
+ manual_captcha: bool,
+ #[clap(short, long, env = "BHC_GUEST_COLOR")]
+ guest_color: Option<String>,
+ #[clap(short, long, env = "BHC_REFRESH_RATE", default_value = "5")]
+ refresh_rate: u64,
+ #[clap(long, env = "BHC_MAX_LOGIN_RETRY", default_value = "5")]
+ max_login_retry: isize,
+ #[clap(long)]
+ url: Option<String>,
+ #[clap(long)]
+ page_php: Option<String>,
+ #[clap(long)]
+ datetime_fmt: Option<String>,
+ #[clap(long)]
+ members_tag: Option<String>,
+ #[clap(short, long)]
+ dan: bool,
+ #[clap(
+ short,
+ long,
+ env = "BHC_PROXY_URL",
+ default_value = "socks5h://127.0.0.1:9050"
+ )]
+ socks_proxy_url: String,
+ #[clap(long, env = "DNMX_USERNAME")]
+ dnmx_username: Option<String>,
+ #[clap(long, env = "DNMX_PASSWORD")]
+ dnmx_password: Option<String>,
+ #[clap(short = 'c', long, default_value = "default")]
+ profile: String,
+}
+
+struct LeChatPHPConfig {
+ url: String,
+ datetime_fmt: String,
+ page_php: String,
+ keepalive_send_to: Option<String>,
+ members_tag: String,
+ staffs_tag: String,
+}
+
+impl LeChatPHPConfig {
+ fn new_black_hat_chat_config() -> Self {
+ Self {
+ url: "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion".to_owned(),
+ datetime_fmt: "%m-%d %H:%M:%S".to_owned(),
+ page_php: "index.php".to_owned(),
+ keepalive_send_to: Some("0".to_owned()),
+ members_tag: "[M] ".to_owned(),
+ staffs_tag: "[Staff] ".to_owned(),
+ }
+ }
+
+ fn new_dans_chat_config() -> Self {
+ Self {
+ url: "http://danschat356lctri3zavzh6fbxg2a7lo6z3etgkctzzpspewu7zdsaqd.onion".to_owned(),
+ datetime_fmt: "%d-%m %H:%M:%S".to_owned(),
+ page_php: "chat.php".to_owned(),
+ keepalive_send_to: None,
+ members_tag: "[Members] ".to_owned(),
+ staffs_tag: "[Staff] ".to_owned(),
+ }
+ }
+}
+
+struct BaseClient {
+ username: String,
+ password: String,
+}
+
+struct LeChatPHPClient<'a> {
+ base_client: BaseClient,
+ guest_color: String,
+ client: &'a Client,
+ session: String,
+ config: LeChatPHPConfig,
+ dkf_api_key: Option<String>,
+ manual_captcha: bool,
+ refresh_rate: u64,
+ max_login_retry: isize,
+
+ is_muted: Arc<Mutex<bool>>,
+ show_sys: bool,
+ display_guest_view: bool,
+ display_hidden_msgs: bool,
+ tx: crossbeam_channel::Sender<PostType>,
+ rx: Arc<Mutex<crossbeam_channel::Receiver<PostType>>>,
+
+ color_tx: crossbeam_channel::Sender<()>,
+ color_rx: Arc<Mutex<crossbeam_channel::Receiver<()>>>,
+}
+
+impl<'a> LeChatPHPClient<'a> {
+ fn run_forever(&mut self) {
+ let max_retry = self.max_login_retry;
+ let mut attempt = 0;
+ loop {
+ if let Err(e) = self.login() {
+ if e.to_string() == KICKED_ERR
+ || e.to_string() == REG_ERR
+ || e.to_string() == NICKNAME_ERR
+ || e.to_string() == UNKNOWN_ERR
+ {
+ eprintln!("{:?}", e.to_string());
+ break;
+ } else if e.to_string() == CAPTCHA_WG_ERR || e.to_string() == CAPTCHA_USED_ERR {
+ } else if e.to_string() == SERVER_DOWN_ERR || e.to_string() == SERVER_DOWN_500_ERR {
+ eprintln!("{}", e.to_string());
+ } else if let Some(err) = e.downcast_ref::<reqwest::Error>() {
+ if err.is_connect() {
+ eprintln!("{:?}\nIs tor proxy enabled ?", err.to_string());
+ break;
+ } else if err.is_timeout() {
+ eprintln!("timeout: {:?}", err.to_string());
+ } else {
+ eprintln!("{:?}", err.to_string());
+ }
+ } else {
+ eprintln!("unknown error: {:?}", e.to_string());
+ }
+ } else {
+ attempt = 0;
+ match self.get_msgs() {
+ Ok(ExitSignal::NeedLogin) => {}
+ Ok(ExitSignal::Terminate) => return,
+ Err(e) => eprintln!("{:?}", e),
+ }
+ }
+ attempt += 1;
+ if max_retry > 0 && attempt > max_retry {
+ break;
+ }
+ self.session = "".to_owned();
+ let retry_in = time::Duration::from_secs(2);
+ let mut msg = format!("retry login in {:?}, attempt: {}", retry_in, attempt);
+ if max_retry > 0 {
+ msg += &format!("/{}", max_retry);
+ }
+ println!("{}", msg);
+ thread::sleep(retry_in);
+ }
+ }
+
+ fn start_keepalive_thread(
+ &self,
+ exit_rx: crossbeam_channel::Receiver<ExitSignal>,
+ last_post_rx: crossbeam_channel::Receiver<bool>,
+ ) -> thread::JoinHandle<()> {
+ let tx = self.tx.clone();
+ let send_to = self.config.keepalive_send_to.clone();
+ thread::spawn(move || loop {
+ let timeout = after(time::Duration::from_secs(60 * 75));
+ select! {
+ // Whenever we send a message to chat server,
+ // we will receive a message on this channel
+ // and reset the timer for next keepalive.
+ recv(&last_post_rx) -> _ => {},
+ recv(&exit_rx) -> _ => return,
+ recv(&timeout) -> _ => {
+ tx.send(PostType::Post("<keepalive>".to_owned(), send_to.clone())).unwrap();
+ tx.send(PostType::DeleteLast).unwrap();
+ },
+ }
+ })
+ }
+
+ // Thread that POST to chat server
+ fn start_post_msg_thread(
+ &self,
+ exit_rx: crossbeam_channel::Receiver<ExitSignal>,
+ last_post_tx: crossbeam_channel::Sender<bool>,
+ ) -> thread::JoinHandle<()> {
+ let client = self.client.clone();
+ let rx = Arc::clone(&self.rx);
+ let full_url = format!("{}/{}", &self.config.url, &self.config.page_php);
+ let url = format!("{}?action=post&session={}", &full_url, self.session);
+ let session = self.session.clone();
+ thread::spawn(move || loop {
+ let rx = rx.lock().unwrap();
+
+ let mut sel = Select::new();
+ let oper1 = sel.recv(&rx);
+ let oper2 = sel.recv(&exit_rx);
+ let oper = sel.select();
+
+ if oper.index() == oper2 {
+ if let Ok(_) = oper.recv(&exit_rx) {
+ return;
+ }
+ } else if oper.index() == oper1 {
+ if let Ok(post_type_recv) = oper.recv(&rx) {
+ loop {
+ let post_type = post_type_recv.clone();
+ let resp = match client.get(url.clone()).send() {
+ Ok(r) => r,
+ Err(e) => {
+ eprintln!("failed to send request: {:?}", e);
+ continue;
+ }
+ };
+ let resp_text = resp.text().unwrap();
+ let doc = Document::from(resp_text.as_str());
+ let nc = doc.select(Attr("name", "nc")).next().unwrap();
+ let nc_value = nc.attr("value").unwrap().to_owned();
+ let postid = doc.select(Attr("name", "postid")).next().unwrap();
+ let postid_value = postid.attr("value").unwrap().to_owned();
+ let mut params: Vec<(&str, String)> = vec![
+ ("lang", LANG.to_owned()),
+ ("nc", nc_value.to_owned()),
+ ("session", session.clone()),
+ ];
+
+ if let PostType::Clean(date, text) = post_type {
+ if let Err(_) =
+ delete_message(&client, &full_url, &mut params, date, text)
+ {
+ continue;
+ }
+ break;
+ }
+
+ let mut req = client.post(&full_url);
+ let mut form: Option<reqwest::blocking::multipart::Form> = None;
+
+ match post_type {
+ PostType::Post(msg, send_to) => {
+ params.extend(vec![
+ ("action", "post".to_owned()),
+ ("postid", postid_value.to_owned()),
+ ("message", msg.clone()),
+ ("sendto", send_to.unwrap_or(SEND_TO_ALL.to_owned())),
+ ]);
+ }
+ PostType::NewNickname(new_nickname) => {
+ if let Err(e) =
+ set_profile_base_info(&client, &full_url, &mut params)
+ {
+ eprintln!("{:?}", e);
+ continue;
+ }
+ params.extend(vec![
+ ("do", "save".to_owned()),
+ ("timestamps", "on".to_owned()),
+ ("newnickname", new_nickname),
+ ]);
+ }
+ PostType::NewColor(new_color) => {
+ if let Err(e) =
+ set_profile_base_info(&client, &full_url, &mut params)
+ {
+ eprintln!("{:?}", e);
+ continue;
+ }
+ params.extend(vec![
+ ("do", "save".to_owned()),
+ ("timestamps", "on".to_owned()),
+ ("colour", new_color),
+ ]);
+ }
+ PostType::Ignore(username) => {
+ if let Err(e) =
+ set_profile_base_info(&client, &full_url, &mut params)
+ {
+ eprintln!("{:?}", e);
+ continue;
+ }
+ params.extend(vec![
+ ("do", "save".to_owned()),
+ ("timestamps", "on".to_owned()),
+ ("ignore", username),
+ ]);
+ }
+ PostType::Unignore(username) => {
+ if let Err(e) =
+ set_profile_base_info(&client, &full_url, &mut params)
+ {
+ eprintln!("{:?}", e);
+ continue;
+ }
+ params.extend(vec![
+ ("do", "save".to_owned()),
+ ("timestamps", "on".to_owned()),
+ ("unignore", username),
+ ]);
+ }
+ PostType::Profile(new_color, new_nickname) => {
+ if let Err(e) =
+ set_profile_base_info(&client, &full_url, &mut params)
+ {
+ eprintln!("{:?}", e);
+ continue;
+ }
+ params.extend(vec![
+ ("do", "save".to_owned()),
+ ("timestamps", "on".to_owned()),
+ ("colour", new_color),
+ ("newnickname", new_nickname),
+ ]);
+ }
+ PostType::Kick(msg, send_to) => {
+ params.extend(vec![
+ ("action", "post".to_owned()),
+ ("postid", postid_value.to_owned()),
+ ("message", msg),
+ ("sendto", send_to),
+ ("kick", "kick".to_owned()),
+ ("what", "purge".to_owned()),
+ ]);
+ }
+ PostType::DeleteLast | PostType::DeleteAll => {
+ params.extend(vec![("action", "delete".to_owned())]);
+ if let PostType::DeleteAll = post_type {
+ params.extend(vec![
+ ("sendto", SEND_TO_ALL.to_owned()),
+ ("confirm", "yes".to_owned()),
+ ("what", "all".to_owned()),
+ ]);
+ } else {
+ params.extend(vec![
+ ("sendto", "".to_owned()),
+ ("what", "last".to_owned()),
+ ]);
+ }
+ }
+ PostType::Upload(file_path, send_to, msg) => {
+ form = Some(
+ multipart::Form::new()
+ .text("lang", LANG.to_owned())
+ .text("nc", nc_value.to_owned())
+ .text("session", session.clone())
+ .text("action", "post".to_owned())
+ .text("postid", postid_value.to_owned())
+ .text("message", msg)
+ .text("sendto", send_to.to_owned())
+ .text("what", "purge".to_owned())
+ .file("file", file_path)
+ .unwrap(),
+ );
+ }
+ PostType::Clean(_, _) => {}
+ }
+
+ if let Some(form_content) = form {
+ req = req.multipart(form_content);
+ } else {
+ req = req.form(¶ms);
+ }
+ if let Err(err) = req.send() {
+ if err.is_timeout() {
+ eprintln!("{:?}", err.to_string());
+ continue;
+ } else {
+ eprintln!("{:?}", err.to_string());
+ }
+ }
+ break;
+ }
+ last_post_tx.send(true).unwrap();
+ }
+ }
+ })
+ }
+
+ // Thread that update messages every "refresh_rate"
+ fn start_get_msgs_thread(
+ &self,
+ sig: &Arc<Mutex<Sig>>,
+ messages: &Arc<Mutex<Vec<Message>>>,
+ users: &Arc<Mutex<Users>>,
+ messages_updated_tx: crossbeam_channel::Sender<bool>,
+ ) -> thread::JoinHandle<()> {
+ let client = self.client.clone();
+ let messages = Arc::clone(&messages);
+ let users = Arc::clone(&users);
+ let session = self.session.clone();
+ let username = self.base_client.username.clone();
+ let refresh_rate = self.refresh_rate.clone();
+ let base_url = self.config.url.clone();
+ let page_php = self.config.page_php.clone();
+ let datetime_fmt = self.config.datetime_fmt.clone();
+ let is_muted = Arc::clone(&self.is_muted);
+ let exit_rx = sig.lock().unwrap().clone();
+ let sig = Arc::clone(sig);
+ let tx = self.tx.clone();
+ let members_tag = self.config.members_tag.clone();
+ let h = thread::spawn(move || loop {
+ let (_stream, stream_handle) = OutputStream::try_default().unwrap();
+ let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap();
+
+ let url = format!(
+ "{}/{}?action=view&session={}&lang={}",
+ base_url, page_php, session, LANG
+ );
+ if let Ok(resp) = client.get(url).send() {
+ if let Ok(resp_text) = resp.text() {
+ let resp_text = resp_text.replace("<br>", "\n");
+ let doc = Document::from(resp_text.as_str());
+ let mut should_notify = false;
+ {
+ let mut messages = messages.lock().unwrap();
+ if let Ok(new_messages) = extract_messages(&doc) {
+ let parse_date = |date: &str| -> NaiveDateTime {
+ let now = chrono::offset::Utc::now();
+ let date_fmt = format!("%Y-{}", datetime_fmt);
+ NaiveDateTime::parse_from_str(
+ format!("{}-{}", now.year(), date).as_str(),
+ date_fmt.as_str(),
+ )
+ .unwrap()
+ };
+
+ if let Some(last_known_msg) = messages.get(0) {
+ let msg = last_known_msg;
+ let parsed_dt = parse_date(&msg.date);
+ for new_msg in &new_messages {
+ let new_parsed_dt = parse_date(&new_msg.date);
+
+ if parsed_dt > new_parsed_dt
+ || (new_msg.date == msg.date && msg.text == new_msg.text)
+ {
+ break;
+ }
+
+ if let Some((from, to_opt, msg)) =
+ get_message(&new_msg.text, &members_tag)
+ {
+ // Process new messages
+
+ // !bhcli filters
+ if msg == "!bhcli" && username == N0TR1V {
+ let msg = format!("@{} -> {}", from, BHCLI_BLOG_URL)
+ .to_owned();
+ tx.send(PostType::Post(msg, None)).unwrap();
+ } else if msg == "/logout"
+ && from == "STUXNET"
+ && username == N0TR1V
+ {
+ eprintln!("forced logout by {}", from);
+ sig.lock().unwrap().signal(ExitSignal::Terminate);
+ return;
+ }
+ // Notify when tagged
+ if msg.contains(format!("@{}", &username).as_str()) {
+ should_notify = true;
+ }
+ // Notify when PM is received
+ if let Some(to) = to_opt {
+ if to == username && msg != "!up" {
+ should_notify = true;
+ }
+ }
+ }
+ }
+ }
+
+ // Build messages vector. Tag deleted messages.
+ let mut msgs_repl = Vec::new();
+ let mut old_msg_ptr = 0;
+ let mut new_msg_ptr = 0;
+ let mut i = 0;
+ while old_msg_ptr < messages.len() || new_msg_ptr < new_messages.len() {
+ if let Some(old_msg) = messages.get(old_msg_ptr) {
+ if let Some(new_msg) = new_messages.get(new_msg_ptr) {
+ let new_parsed_dt = parse_date(&new_msg.date);
+ let parsed_dt = parse_date(&old_msg.date);
+ if new_parsed_dt > parsed_dt {
+ msgs_repl.push(new_msg.clone());
+ new_msg_ptr += 1;
+ } else if new_parsed_dt == parsed_dt {
+ if old_msg.text.text() == new_msg.text.text() {
+ msgs_repl.push(old_msg.clone());
+ } else {
+ msgs_repl.push(new_msg.clone());
+ }
+ new_msg_ptr += 1;
+ old_msg_ptr += 1;
+ } else {
+ let mut tmp = old_msg.clone();
+ tmp.deleted = true;
+ msgs_repl.push(tmp);
+ old_msg_ptr += 1;
+ }
+ } else {
+ msgs_repl.push(old_msg.clone());
+ old_msg_ptr += 1;
+ }
+ } else if let Some(new_msg) = new_messages.get(new_msg_ptr) {
+ msgs_repl.push(new_msg.clone());
+ new_msg_ptr += 1;
+ }
+ i += 1;
+ if i > 2000 {
+ break;
+ }
+ }
+
+ // Notify new messages has arrived.
+ // This ensure that we redraw the messages on the screen right away.
+ // Otherwise, the screen would not redraw until a keyboard event occurs.
+ messages_updated_tx.send(true).unwrap();
+ // Update "messages" with new value
+ *messages = msgs_repl;
+ } else {
+ // Failed to get messages, probably need relogin
+ sig.lock().unwrap().signal(ExitSignal::NeedLogin);
+ return;
+ }
+ }
+ let muted = { *is_muted.lock().unwrap() };
+ if should_notify && !muted {
+ if let Err(err) = stream_handle.play_raw(source.convert_samples()) {
+ eprintln!("{}", err);
+ }
+ }
+ {
+ let mut users = users.lock().unwrap();
+ ban_imposters(&tx, &username, &users);
+ *users = extract_users(&doc);
+ }
+ }
+ }
+
+ let timeout = after(time::Duration::from_secs(refresh_rate));
+ select! {
+ recv(&exit_rx) -> _ => return,
+ recv(&timeout) -> _ => {},
+ }
+ });
+ h
+ }
+
+ fn get_msgs(&mut self) -> Result<ExitSignal> {
+ let terminate_signal: ExitSignal;
+
+ let messages: Arc<Mutex<Vec<Message>>> = Arc::new(Mutex::new(Vec::new()));
+ let users: Arc<Mutex<Users>> = Arc::new(Mutex::new(Users::default()));
+
+ // Create default app state
+ let mut app = App::default();
+
+ // Each threads gets a clone of the receiver.
+ // When someone calls ".signal", all threads recieve it,
+ // and knows that they have to terminate.
+ let sig = Arc::new(Mutex::new(Sig::new()));
+
+ let (messages_updated_tx, messages_updated_rx) = crossbeam_channel::unbounded();
+ let (last_post_tx, last_post_rx) = crossbeam_channel::unbounded();
+
+ let h1 = self.start_keepalive_thread(sig.lock().unwrap().clone(), last_post_rx);
+ let h2 = self.start_post_msg_thread(sig.lock().unwrap().clone(), last_post_tx);
+ let h3 = self.start_get_msgs_thread(&sig, &messages, &users, messages_updated_tx);
+
+ // Terminal initialization
+ let mut stdout = io::stdout();
+ enable_raw_mode().unwrap();
+ execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
+ let backend = CrosstermBackend::new(stdout);
+ let mut terminal = Terminal::new(backend)?;
+
+ // Setup event handlers
+ let (events, h4) = Events::with_config(Config {
+ messages_updated_rx,
+ exit_rx: sig.lock().unwrap().clone(),
+ tick_rate: time::Duration::from_millis(250),
+ });
+
+ loop {
+ {
+ app.is_muted = *self.is_muted.lock().unwrap();
+ app.show_sys = self.show_sys;
+ app.display_guest_view = self.display_guest_view;
+ app.display_hidden_msgs = self.display_hidden_msgs;
+ app.members_tag = self.config.members_tag.clone();
+ app.staffs_tag = self.config.staffs_tag.clone();
+ }
+ // Draw UI
+ terminal.draw(|f| {
+ draw_terminal_frame(f, &mut app, &messages, &users);
+ })?;
+
+ // Handle input
+ match self.handle_input(&events, &mut app, &messages, &users) {
+ Err(ExitSignal::Terminate) => {
+ terminate_signal = ExitSignal::Terminate;
+ sig.lock().unwrap().signal(terminate_signal.clone());
+ break;
+ }
+ Err(ExitSignal::NeedLogin) => {
+ terminate_signal = ExitSignal::NeedLogin;
+ sig.lock().unwrap().signal(terminate_signal.clone());
+ break;
+ }
+ Ok(_) => continue,
+ };
+ }
+
+ // Cleanup before leaving
+ disable_raw_mode()?;
+ execute!(
+ terminal.backend_mut(),
+ LeaveAlternateScreen,
+ DisableMouseCapture
+ )?;
+ terminal.show_cursor()?;
+ terminal.clear()?;
+ terminal.set_cursor(0, 0)?;
+
+ h1.join().unwrap();
+ h2.join().unwrap();
+ h3.join().unwrap();
+ h4.join().unwrap();
+
+ Ok(terminate_signal)
+ }
+
+ fn post_msg(&self, post_type: PostType) -> Result<()> {
+ self.tx.send(post_type)?;
+ Ok(())
+ }
+
+ fn login(&mut self) -> Result<()> {
+ // If we provided a session, skip login process
+ if self.session != "" {
+ return Ok(());
+ }
+
+ // Get login page
+ let login_url = format!("{}/{}", &self.config.url, &self.config.page_php);
+ let resp = self.client.get(&login_url).send()?;
+ if resp.status() == StatusCode::BAD_GATEWAY {
+ return Err(SERVER_DOWN_ERR.into());
+ }
+ let resp = resp.text()?;
+ let doc = Document::from(resp.as_str());
+
+ // Post login form
+ let mut params = vec![
+ ("action", "login".to_owned()),
+ ("lang", LANG.to_owned()),
+ ("nick", self.base_client.username.clone()),
+ ("pass", self.base_client.password.clone()),
+ ("colour", self.guest_color.clone()),
+ ];
+
+ if let Some(captcha_value) = doc
+ .select(And(Name("input"), Attr("name", "challenge")))
+ .next()
+ {
+ let captcha_value = captcha_value.attr("value").unwrap();
+
+ let mut captcha_input = String::new();
+ if self.manual_captcha {
+ let captcha_img = doc.select(Name("img")).next().unwrap().attr("src").unwrap();
+
+ if let Some(dkf_api_key) = self.dkf_api_key.clone() {
+ // If we have the DKF_API_KEY, auto solve captcha using the api
+ let params = vec![("captcha", captcha_img)];
+ let resp = self
+ .client
+ .post(format!("{}/api/v1/captcha/solver", DKF_URL))
+ .header("DKF_API_KEY", dkf_api_key)
+ .form(¶ms)
+ .send()?;
+ let resp = resp.text()?;
+ let rgx = Regex::new(r#""answer": "([^"]+)""#)?
+ .captures(resp.as_str())
+ .unwrap();
+ let answer = rgx.get(1).unwrap().as_str();
+ captcha_input = answer.to_owned();
+ } else {
+ // Otherwise, save the captcha on disk and prompt user for answer
+ let img_decoded =
+ decode(captcha_img.strip_prefix("data:image/gif;base64,").unwrap())?;
+ let img = image::load_from_memory(&img_decoded).unwrap();
+ let img_buf = image::imageops::resize(
+ &img,
+ img.width() * 4,
+ img.height() * 4,
+ image::imageops::FilterType::Nearest,
+ );
+ // Save captcha as file on disk
+ img_buf.save("captcha.gif").unwrap();
+
+ termage::display_image("captcha.gif", img.width(), img.height());
+
+ // Enter captcha
+ print!("captcha: ");
+ io::stdout().flush().unwrap();
+ io::stdin().read_line(&mut captcha_input).unwrap();
+ trim_newline(&mut captcha_input);
+ }
+ } else {
+ // Captcha is not actually required for memebers (BHC)
+ captcha_input = "12345".to_owned();
+ }
+
+ params.extend(vec![
+ ("challenge", captcha_value.to_owned()),
+ ("captcha", captcha_input.clone()),
+ ]);
+ }
+
+ let resp = self.client.post(&login_url).form(¶ms).send()?;
+ match resp.status() {
+ StatusCode::BAD_GATEWAY => return Err(SERVER_DOWN_ERR.into()),
+ StatusCode::INTERNAL_SERVER_ERROR => return Err(SERVER_DOWN_500_ERR.into()),
+ _ => {}
+ }
+ let mut resp = resp.text()?;
+ if resp.contains(CAPTCHA_USED_ERR) {
+ return Err(CAPTCHA_USED_ERR.into());
+ } else if resp.contains(CAPTCHA_WG_ERR) {
+ return Err(CAPTCHA_WG_ERR.into());
+ } else if resp.contains(REG_ERR) {
+ return Err(REG_ERR.into());
+ } else if resp.contains(NICKNAME_ERR) {
+ return Err(NICKNAME_ERR.into());
+ } else if resp.contains(KICKED_ERR) {
+ return Err(KICKED_ERR.into());
+ }
+
+ let mut doc = Document::from(resp.as_str());
+ if let Some(body) = doc.select(Name("body")).next() {
+ if let Some(body_class) = body.attr("class") {
+ if body_class == "error" {
+ if let Some(h2) = doc.select(Name("h2")).next() {
+ eprintln!("{}", h2.text());
+ }
+ return Err(UNKNOWN_ERR.into());
+ } else if body_class == "failednotice" {
+ eprintln!("failed logins: {}", body.text());
+ let nc = doc.select(Attr("name", "nc")).next().unwrap();
+ let nc_value = nc.attr("value").unwrap().to_owned();
+ let params: Vec<(&str, String)> = vec![
+ ("lang", LANG.to_owned()),
+ ("nc", nc_value.to_owned()),
+ ("action", "login".to_owned()),
+ ];
+ resp = self.client.post(&login_url).form(¶ms).send()?.text()?;
+ doc = Document::from(resp.as_str());
+ }
+ }
+ }
+
+ let iframe = match doc.select(Attr("name", "view")).next() {
+ Some(view) => view,
+ None => {
+ fs::write("./dump_login_err.html", resp.as_str()).unwrap();
+ panic!("failed to get view iframe");
+ }
+ };
+ let iframe_src = iframe.attr("src").unwrap();
+
+ let session_captures = SESSION_RGX.captures(iframe_src).unwrap();
+ let session = session_captures.get(1).unwrap().as_str();
+
+ self.session = session.to_owned();
+ Ok(())
+ }
+
+ fn logout(&mut self) -> Result<()> {
+ let full_url = format!("{}/{}", &self.config.url, &self.config.page_php);
+ let params = [
+ ("action", "logout"),
+ ("session", &self.session),
+ ("lang", LANG),
+ ];
+ self.client.post(&full_url).form(¶ms).send()?;
+ self.session = "".to_owned();
+ Ok(())
+ }
+
+ fn start_cycle(&self, color_only: bool) {
+ let username = self.base_client.username.clone();
+ let tx = self.tx.clone();
+ let color_rx = Arc::clone(&self.color_rx);
+ thread::spawn(move || {
+ let mut idx = 0;
+ let colors = vec![
+ "#ff3366", "#ff6633", "#FFCC33", "#33FF66", "#33FFCC", "#33CCFF", "#3366FF",
+ "#6633FF", "#CC33FF", "#efefef",
+ ];
+ loop {
+ let color_rx = color_rx.lock().unwrap();
+ let timeout = after(time::Duration::from_millis(5200));
+ select! {
+ recv(&color_rx) -> _ => break,
+ recv(&timeout) -> _ => {}
+ }
+ idx = (idx + 1) % colors.len();
+ let color = colors[idx].to_owned();
+ if !color_only {
+ let name = format!("{}{}", username, random_string(14));
+ eprintln!("New name : {}", name);
+ tx.send(PostType::Profile(color, name)).unwrap();
+ } else {
+ tx.send(PostType::NewColor(color)).unwrap();
+ }
+ // tx.send(PostType::Post("!up".to_owned(), Some(username.clone())))
+ // .unwrap();
+ // tx.send(PostType::DeleteLast).unwrap();
+ }
+ let msg = PostType::Profile("#90ee90".to_owned(), username);
+ tx.send(msg).unwrap();
+ });
+ }
+
+ fn handle_input(
+ &mut self,
+ events: &Events,
+ app: &mut App,
+ messages: &Arc<Mutex<Vec<Message>>>,
+ users: &Arc<Mutex<Users>>,
+ ) -> std::result::Result<(), ExitSignal> {
+ match events.next() {
+ Ok(Event::NeedLogin) => return Err(ExitSignal::NeedLogin),
+ Ok(Event::Terminate) => return Err(ExitSignal::Terminate),
+ Ok(Event::Input(evt)) => self.handle_event(app, messages, users, evt),
+ _ => Ok(()),
+ }
+ }
+
+ fn handle_event(
+ &mut self,
+ app: &mut App,
+ messages: &Arc<Mutex<Vec<Message>>>,
+ users: &Arc<Mutex<Users>>,
+ event: crossterm::event::Event,
+ ) -> std::result::Result<(), ExitSignal> {
+ match event {
+ crossterm::event::Event::Resize(_cols, _rows) => Ok(()),
+ crossterm::event::Event::Key(key_event) => {
+ self.handle_key_event(app, messages, users, key_event)
+ }
+ crossterm::event::Event::Mouse(mouse_event) => {
+ self.handle_mouse_event(app, mouse_event)
+ }
+ }
+ }
+
+ fn handle_key_event(
+ &mut self,
+ app: &mut App,
+ messages: &Arc<Mutex<Vec<Message>>>,
+ users: &Arc<Mutex<Users>>,
+ key_event: crossterm::event::KeyEvent,
+ ) -> std::result::Result<(), ExitSignal> {
+ match app.input_mode {
+ InputMode::LongMessage => {
+ self.handle_long_message_mode_key_event(app, key_event, messages)
+ }
+ InputMode::Normal => self.handle_normal_mode_key_event(app, key_event, messages),
+ InputMode::Editing => self.handle_editing_mode_key_event(app, key_event, users),
+ }
+ }
+
+ fn handle_long_message_mode_key_event(
+ &mut self,
+ app: &mut App,
+ key_event: crossterm::event::KeyEvent,
+ messages: &Arc<Mutex<Vec<Message>>>,
+ ) -> std::result::Result<(), ExitSignal> {
+ match key_event {
+ KeyEvent {
+ code: KeyCode::Enter,
+ modifiers: KeyModifiers::NONE,
+ }
+ | KeyEvent {
+ code: KeyCode::Esc,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ app.long_message = None;
+ app.input_mode = InputMode::Normal;
+ }
+ KeyEvent {
+ code: KeyCode::Char('d'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(item) = app.items.items.get(idx) {
+ self.post_msg(PostType::Clean(item.date.to_owned(), item.text.text()))
+ .unwrap();
+ let mut messages = messages.lock().unwrap();
+ if let Some(pos) = messages
+ .iter()
+ .position(|m| m.date == item.date && m.text.text() == item.text.text())
+ {
+ messages[pos].hide = !messages[pos].hide;
+ }
+ app.long_message = None;
+ app.input_mode = InputMode::Normal;
+ }
+ }
+ }
+ _ => {}
+ }
+ Ok(())
+ }
+
+ fn handle_normal_mode_key_event(
+ &mut self,
+ app: &mut App,
+ key_event: crossterm::event::KeyEvent,
+ messages: &Arc<Mutex<Vec<Message>>>,
+ ) -> std::result::Result<(), ExitSignal> {
+ match key_event {
+ KeyEvent {
+ code: KeyCode::Char('/'),
+ modifiers: KeyModifiers::NONE,
+ } => {
+ app.items.unselect();
+ app.input = "/".to_owned();
+ app.input_idx = app.input.width();
+ app.input_mode = InputMode::Editing;
+ }
+ KeyEvent {
+ code: KeyCode::Char('j'),
+ modifiers: KeyModifiers::NONE,
+ }
+ | KeyEvent {
+ code: KeyCode::Down,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ app.items.next();
+ }
+ KeyEvent {
+ code: KeyCode::Char('k'),
+ modifiers: KeyModifiers::NONE,
+ }
+ | KeyEvent {
+ code: KeyCode::Up,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ app.items.previous();
+ }
+ KeyEvent {
+ code: KeyCode::Enter,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(item) = app.items.items.get(idx) {
+ app.long_message = Some(item.clone());
+ app.input_mode = InputMode::LongMessage;
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Backspace,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(item) = app.items.items.get(idx) {
+ let mut messages = messages.lock().unwrap();
+ if let Some(pos) = messages
+ .iter()
+ .position(|m| m.date == item.date && m.text.text() == item.text.text())
+ {
+ if item.deleted {
+ messages.remove(pos);
+ } else {
+ messages[pos].hide = !messages[pos].hide;
+ }
+ }
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('y'),
+ modifiers: KeyModifiers::NONE,
+ }
+ | KeyEvent {
+ code: KeyCode::Char('c'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(item) = app.items.items.get(idx) {
+ if let Some(upload_link) = &item.upload_link {
+ let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
+ let mut out = format!("{}{}", self.config.url, upload_link);
+ if let Some((_, _, msg)) =
+ get_message(&item.text, &self.config.members_tag)
+ {
+ out = format!("{} {}", msg, out);
+ }
+ ctx.set_contents(out).unwrap();
+ } else if let Some((_, _, msg)) =
+ get_message(&item.text, &self.config.members_tag)
+ {
+ let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
+ ctx.set_contents(msg).unwrap();
+ }
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('Y'),
+ modifiers: KeyModifiers::SHIFT,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(item) = app.items.items.get(idx) {
+ if let Some(upload_link) = &item.upload_link {
+ let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
+ let out = format!("{}{}", self.config.url, upload_link);
+ ctx.set_contents(out).unwrap();
+ } else if let Some((_, _, msg)) =
+ get_message(&item.text, &self.config.members_tag)
+ {
+ let finder = LinkFinder::new();
+ let links: Vec<_> = finder.links(msg.as_str()).collect();
+ if let Some(link) = links.get(0) {
+ let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
+ ctx.set_contents(link.as_str().to_owned()).unwrap();
+ }
+ }
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('d'),
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(item) = app.items.items.get(idx) {
+ eprintln!("{:?}", item.text.text());
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('D'),
+ modifiers: KeyModifiers::SHIFT,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(item) = app.items.items.get(idx) {
+ eprintln!("{:?} {:?}", item.text, item.upload_link);
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('m'),
+ modifiers: KeyModifiers::NONE,
+ } => {
+ let mut is_muted = self.is_muted.lock().unwrap();
+ *is_muted = !*is_muted;
+ }
+ KeyEvent {
+ code: KeyCode::Char('M'),
+ modifiers: KeyModifiers::SHIFT,
+ } => {
+ self.show_sys = !self.show_sys;
+ }
+ KeyEvent {
+ code: KeyCode::Char('G'),
+ modifiers: KeyModifiers::SHIFT,
+ } => {
+ self.display_guest_view = !self.display_guest_view;
+ }
+ KeyEvent {
+ code: KeyCode::Char('H'),
+ modifiers: KeyModifiers::SHIFT,
+ } => {
+ self.display_hidden_msgs = !self.display_hidden_msgs;
+ }
+ KeyEvent {
+ code: KeyCode::Char('i'),
+ modifiers: KeyModifiers::NONE,
+ } => {
+ app.input_mode = InputMode::Editing;
+ app.items.unselect();
+ }
+ KeyEvent {
+ code: KeyCode::Char('Q'),
+ modifiers: KeyModifiers::SHIFT,
+ } => {
+ self.logout().unwrap();
+ return Err(ExitSignal::Terminate);
+ }
+ KeyEvent {
+ code: KeyCode::Char('q'),
+ modifiers: KeyModifiers::NONE,
+ } => {
+ return Err(ExitSignal::Terminate);
+ }
+ KeyEvent {
+ code: KeyCode::Char('t'),
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(username) = get_username(
+ &self.base_client.username,
+ &app.items.items.get(idx).unwrap().text,
+ &self.config.members_tag,
+ ) {
+ app.input = format!("@{} ", username);
+ app.input_idx = app.input.width();
+ app.input_mode = InputMode::Editing;
+ app.items.unselect();
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('p'),
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(username) = get_username(
+ &self.base_client.username,
+ &app.items.items.get(idx).unwrap().text,
+ &self.config.members_tag,
+ ) {
+ app.input = format!("/pm {} ", username);
+ app.input_idx = app.input.width();
+ app.input_mode = InputMode::Editing;
+ app.items.unselect();
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('k'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ if let Some(username) = get_username(
+ &self.base_client.username,
+ &app.items.items.get(idx).unwrap().text,
+ &self.config.members_tag,
+ ) {
+ app.input = format!("/kick {} ", username);
+ app.input_idx = app.input.width();
+ app.input_mode = InputMode::Editing;
+ app.items.unselect();
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('u'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
+ code: KeyCode::PageUp,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ app.items.state.select(idx.checked_sub(10).or(Some(0)));
+ } else {
+ app.items.next();
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('d'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
+ code: KeyCode::PageDown,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if let Some(idx) = app.items.state.selected() {
+ let wanted_idx = idx + 10;
+ let max_idx = app.items.items.len() - 1;
+ let new_idx = std::cmp::min(wanted_idx, max_idx);
+ app.items.state.select(Some(new_idx));
+ } else {
+ app.items.next();
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Esc,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ app.items.unselect();
+ }
+ KeyEvent {
+ code: KeyCode::Char('u'),
+ modifiers: KeyModifiers::SHIFT,
+ } => {
+ app.items.state.select(Some(0));
+ }
+ _ => {}
+ }
+ Ok(())
+ }
+
+ fn handle_editing_mode_key_event(
+ &mut self,
+ app: &mut App,
+ key_event: crossterm::event::KeyEvent,
+ users: &Arc<Mutex<Users>>,
+ ) -> std::result::Result<(), ExitSignal> {
+ match key_event {
+ KeyEvent {
+ code: KeyCode::Enter,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if FIND_RGX.is_match(&app.input) {
+ return Ok(());
+ }
+
+ let input: String = app.input.drain(..).collect();
+ app.input_idx = 0;
+ if input == "/dl" {
+ // Delete last message
+ self.post_msg(PostType::DeleteLast).unwrap();
+ } else if let Some(captures) = DLX_RGX.captures(&input) {
+ // Delete the last X messages
+ let x: usize = captures.get(1).unwrap().as_str().parse().unwrap();
+ for _ in 0..x {
+ self.post_msg(PostType::DeleteLast).unwrap();
+ }
+ } else if input == "/dall" {
+ // Delete all messages
+ self.post_msg(PostType::DeleteAll).unwrap();
+ } else if input == "/cycles" {
+ self.color_tx.send(()).unwrap();
+ } else if input == "/cycle1" {
+ self.start_cycle(true);
+ } else if input == "/cycle2" {
+ self.start_cycle(false);
+ } else if input == "/kall" {
+ // Kick all guests
+ let username = "s _".to_owned();
+ let msg = "".to_owned();
+ self.post_msg(PostType::Kick(msg, username)).unwrap();
+ } else if input.starts_with("/m ") {
+ // Send message to "members" section
+ let msg = remove_prefix(&input, "/m ").to_owned();
+ let to = Some(SEND_TO_MEMBERS.to_owned());
+ self.post_msg(PostType::Post(msg, to)).unwrap();
+ app.input = "/m ".to_owned();
+ app.input_idx = app.input.width()
+ } else if input.starts_with("/a ") {
+ // Send message to "admin" section
+ let msg = remove_prefix(&input, "/a ").to_owned();
+ let to = Some(SEND_TO_ADMINS.to_owned());
+ self.post_msg(PostType::Post(msg, to)).unwrap();
+ app.input = "/a ".to_owned();
+ app.input_idx = app.input.width()
+ } else if input.starts_with("/s ") {
+ // Send message to "staff" section
+ let msg = remove_prefix(&input, "/s ").to_owned();
+ let to = Some(SEND_TO_STAFFS.to_owned());
+ self.post_msg(PostType::Post(msg, to)).unwrap();
+ app.input = "/s ".to_owned();
+ app.input_idx = app.input.width()
+ } else if let Some(captures) = PM_RGX.captures(&input) {
+ // Send PM message
+ let username = &captures[1];
+ let msg = captures[2].to_owned();
+ let to = Some(username.to_owned());
+ self.post_msg(PostType::Post(msg, to)).unwrap();
+ app.input = format!("/pm {} ", username);
+ app.input_idx = app.input.width()
+ } else if let Some(captures) = NEW_NICKNAME_RGX.captures(&input) {
+ // Change nickname
+ let new_nickname = captures[1].to_owned();
+ self.post_msg(PostType::NewNickname(new_nickname)).unwrap();
+ } else if let Some(captures) = NEW_COLOR_RGX.captures(&input) {
+ // Change color
+ let new_color = captures[1].to_owned();
+ self.post_msg(PostType::NewColor(new_color)).unwrap();
+ } else if let Some(captures) = KICK_RGX.captures(&input) {
+ // Kick a user
+ let username = captures[1].to_owned();
+ let msg = captures[2].to_owned();
+ self.post_msg(PostType::Kick(msg, username)).unwrap();
+ } else if let Some(captures) = IGNORE_RGX.captures(&input) {
+ // Ignore a user
+ let username = captures[1].to_owned();
+ self.post_msg(PostType::Ignore(username)).unwrap();
+ } else if let Some(captures) = UNIGNORE_RGX.captures(&input) {
+ // Unignore a user
+ let username = captures[1].to_owned();
+ self.post_msg(PostType::Unignore(username)).unwrap();
+ } else if let Some(captures) = UPLOAD_RGX.captures(&input) {
+ // Upload a file
+ let file_path = captures[1].to_owned();
+ let send_to = match captures.get(2) {
+ Some(to_match) => match to_match.as_str() {
+ "members" => SEND_TO_MEMBERS,
+ "staffs" => SEND_TO_STAFFS,
+ "admins" => SEND_TO_ADMINS,
+ _ => SEND_TO_ALL,
+ },
+ None => SEND_TO_ALL,
+ }
+ .to_owned();
+ let msg = match captures.get(3) {
+ Some(msg_match) => msg_match.as_str().to_owned(),
+ None => "".to_owned(),
+ };
+ self.post_msg(PostType::Upload(file_path, send_to, msg))
+ .unwrap();
+ } else {
+ // Send normal message
+ self.post_msg(PostType::Post(input, None)).unwrap();
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Tab,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ let (p1, p2) = app.input.split_at(app.input_idx);
+ if p2 == "" || p2.chars().nth(0) == Some(' ') {
+ let mut parts: Vec<&str> = p1.split(" ").collect();
+ if let Some(user_prefix) = parts.pop() {
+ let mut should_autocomplete = false;
+ let mut prefix = "";
+ if parts.len() == 1
+ && ((parts[0] == "/kick" || parts[0] == "/k" || parts[0] == "/pm")
+ || parts[0] == "/ignore"
+ || parts[0] == "/unignore")
+ {
+ should_autocomplete = true;
+ } else if user_prefix.starts_with("@") {
+ should_autocomplete = true;
+ prefix = "@";
+ }
+ if should_autocomplete {
+ let user_prefix_norm = remove_prefix(user_prefix, prefix);
+ let user_prefix_norm_len = user_prefix_norm.len();
+ if let Some(name) = autocomplete_username(users, user_prefix_norm) {
+ let complete_name = format!("{}{}", prefix, name);
+ parts.push(complete_name.as_str());
+ let p2 = p2.trim_start();
+ if p2 != "" {
+ parts.push(p2);
+ }
+ app.input = parts.join(" ");
+ app.input_idx += name.len() - user_prefix_norm_len;
+ }
+ }
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('c'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ app.clear_filter();
+ app.input = "".to_owned();
+ app.input_idx = 0;
+ app.input_mode = InputMode::Normal;
+ }
+ KeyEvent {
+ code: KeyCode::Char('a'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ app.input_idx = 0;
+ }
+ KeyEvent {
+ code: KeyCode::Char('e'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ app.input_idx = app.input.width();
+ }
+ KeyEvent {
+ code: KeyCode::Char('f'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ if let Some(idx) = app.input.chars().skip(app.input_idx).position(|c| c == ' ') {
+ app.input_idx = std::cmp::min(app.input_idx + idx + 1, app.input.width());
+ } else {
+ app.input_idx = app.input.width();
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('b'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ if let Some(idx) = app.input_idx.checked_sub(2) {
+ let tmp = app
+ .input
+ .chars()
+ .take(idx)
+ .collect::<String>()
+ .chars()
+ .rev()
+ .collect::<String>();
+ if let Some(idx) = tmp.chars().position(|c| c == ' ') {
+ app.input_idx = std::cmp::max(tmp.width() - idx, 0);
+ } else {
+ app.input_idx = 0;
+ }
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('v'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
+ if let Ok(clipboard) = ctx.get_contents() {
+ let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
+ app.input.insert_str(byte_position, &clipboard);
+ app.input_idx += clipboard.width();
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Left,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if app.input_idx > 0 {
+ app.input_idx -= 1;
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Right,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if app.input_idx < app.input.width() {
+ app.input_idx += 1;
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Down,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ app.input_mode = InputMode::Normal;
+ app.items.next();
+ }
+ KeyEvent {
+ code: KeyCode::Char(c),
+ modifiers: KeyModifiers::NONE,
+ }
+ | KeyEvent {
+ code: KeyCode::Char(c),
+ modifiers: KeyModifiers::SHIFT,
+ } => {
+ let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
+ app.input.insert(byte_position, c);
+
+ app.input_idx += 1;
+ app.update_filter();
+ }
+ KeyEvent {
+ code: KeyCode::Backspace,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if app.input_idx > 0 {
+ app.input_idx -= 1;
+ app.input = remove_at(&app.input, app.input_idx);
+ app.update_filter();
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Delete,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ if app.input_idx > 0 && app.input_idx == app.input.width() {
+ app.input_idx -= 1;
+ }
+ app.input = remove_at(&app.input, app.input_idx);
+ app.update_filter();
+ }
+ KeyEvent {
+ code: KeyCode::Esc,
+ modifiers: KeyModifiers::NONE,
+ } => {
+ app.input_mode = InputMode::Normal;
+ }
+ _ => {}
+ }
+ Ok(())
+ }
+
+ fn handle_mouse_event(
+ &mut self,
+ app: &mut App,
+ mouse_event: MouseEvent,
+ ) -> std::result::Result<(), ExitSignal> {
+ match mouse_event.kind {
+ MouseEventKind::ScrollDown => app.items.next(),
+ MouseEventKind::ScrollUp => app.items.previous(),
+ _ => {}
+ }
+ Ok(())
+ }
+}
+
+// Give a char index, return the byte position
+fn byte_pos(v: &str, idx: usize) -> Option<usize> {
+ let mut b = 0;
+ let mut chars = v.chars();
+ for _ in 0..idx {
+ if let Some(c) = chars.next() {
+ b += c.len_utf8();
+ } else {
+ return None;
+ }
+ }
+ Some(b)
+}
+
+// Remove the character at idx (utf-8 aware)
+fn remove_at(v: &str, idx: usize) -> String {
+ v.chars()
+ .enumerate()
+ .flat_map(|(i, c)| {
+ if i == idx {
+ return None;
+ }
+ Some(c)
+ })
+ .collect::<String>()
+}
+
+// Autocomplete any username
+fn autocomplete_username(users: &Arc<Mutex<Users>>, prefix: &str) -> Option<String> {
+ let users = users.lock().unwrap();
+ let all_users = users.all();
+ let mut filtered = all_users.iter().find(|(_, name)| name.starts_with(prefix));
+ if filtered.is_none() {
+ let prefix_lower = prefix.to_lowercase();
+ filtered = all_users
+ .iter()
+ .find(|(_, name)| name.to_lowercase().starts_with(&prefix_lower));
+ }
+ match filtered {
+ Some((_, name)) => Some(name.to_owned()),
+ None => None,
+ }
+}
+
+fn set_profile_base_info(
+ client: &Client,
+ full_url: &str,
+ params: &mut Vec<(&str, String)>,
+) -> Result<()> {
+ params.extend(vec![("action", "profile".to_owned())]);
+ let profile_resp = client.post(full_url).form(¶ms).send()?;
+ let profile_resp_txt = profile_resp.text().unwrap();
+ let doc = Document::from(profile_resp_txt.as_str());
+ let bold = doc.select(Attr("id", "bold")).next().unwrap();
+ let italic = doc.select(Attr("id", "italic")).next().unwrap();
+ let small = doc.select(Attr("id", "small")).next().unwrap();
+ if let Some(_) = bold.attr("checked") {
+ params.push(("bold", "on".to_owned()));
+ }
+ if let Some(_) = italic.attr("checked") {
+ params.push(("italic", "on".to_owned()));
+ }
+ if let Some(_) = small.attr("checked") {
+ params.push(("small", "on".to_owned()));
+ }
+ let font_select = doc.select(Attr("name", "font")).next().unwrap();
+ let font = font_select.select(Name("option")).find_map(|el| {
+ if let Some(_) = el.attr("selected") {
+ return Some(el.attr("value").unwrap());
+ }
+ None
+ });
+ params.push(("font", font.unwrap_or("").to_owned()));
+ Ok(())
+}
+
+fn delete_message(
+ client: &Client,
+ full_url: &str,
+ params: &mut Vec<(&str, String)>,
+ date: String,
+ text: String,
+) -> Result<()> {
+ params.extend(vec![
+ ("action", "admin".to_owned()),
+ ("do", "clean".to_owned()),
+ ("what", "choose".to_owned()),
+ ]);
+ let clean_resp = client.post(full_url).form(¶ms).send()?;
+ let clean_resp_txt = clean_resp.text().unwrap();
+ let doc = Document::from(clean_resp_txt.as_str());
+ let nc = doc.select(Attr("name", "nc")).next().unwrap();
+ let nc_value = nc.attr("value").unwrap().to_owned();
+ let msgs = extract_messages(&doc).unwrap();
+ if let Some(msg) = msgs
+ .iter()
+ .find(|m| m.date == date && m.text.text() == text)
+ {
+ params.extend(vec![
+ ("nc", nc_value.to_owned()),
+ ("what", "selected".to_owned()),
+ ("mid[]", format!("{}", msg.id.unwrap())),
+ ]);
+ client.post(full_url).form(¶ms).send()?;
+ }
+ Ok(())
+}
+
+fn ban_imposters(tx: &crossbeam_channel::Sender<PostType>, account_username: &str, users: &Users) {
+ if BAN_IMPOSTERS {
+ if users.admin.len() == 0 && (users.staff.len() == 0 || account_username == N0TR1V) {
+ let n0tr1v_rgx = Regex::new(r#"n[o|0]tr[1|i|l][v|y]"#).unwrap(); // o 0 | 1 i l | v y
+ let molester_rgx = Regex::new(r#"m[o|0][1|l][e|3][s|5|$]t[e|3]r"#).unwrap();
+ let rapist_rgx = Regex::new(r#"r[a|4]p[i|1|l]st"#).unwrap();
+ let hitler_rgx = Regex::new(r#"h[i|1|l]t[l|1]er"#).unwrap();
+ let himmler_rgx = Regex::new(r#"h[i|1]m+l[e|3]r"#).unwrap();
+ let goebbels_rgx = Regex::new(r#"g[o|0][e|3]b+[e|3]ls"#).unwrap();
+ let heydrich_rgx = Regex::new(r#"h[e|3]ydr[i|1]ch"#).unwrap();
+ let globocnik_rgx = Regex::new(r#"gl[o|0]b[o|0]cn[i|1|l]k"#).unwrap();
+ let dirlewanger_rgx = Regex::new(r#"d[i|1]rl[e|3]wang[e|3]r"#).unwrap();
+ let jeckeln_rgx = Regex::new(r#"j[e|3]ck[e|3]ln"#).unwrap();
+ let kramer_rgx = Regex::new(r#"kram[e|3]r"#).unwrap();
+ let blobel_rgx = Regex::new(r#"bl[o|0]b[e|3]l"#).unwrap();
+ let stangl_rgx = Regex::new(r#"stangl"#).unwrap();
+ for (_color, username) in &users.guests {
+ let lower_name = username.to_lowercase();
+ // Names that anyone using bhcli will ban
+ if n0tr1v_rgx.is_match(&lower_name) || lower_name.contains("pedo") {
+ let msg = "forbidden name".to_owned();
+ let username = username.to_owned();
+ tx.send(PostType::Kick(msg, username)).unwrap();
+ }
+ // Names that only "n0tr1v" will ban
+ if account_username == N0TR1V {
+ if lower_name.contains("fuck")
+ || lower_name.contains("nigger")
+ || lower_name.contains("nigga")
+ || lower_name.contains("chink")
+ || lower_name.contains("atomwaffen")
+ || lower_name.contains("altright")
+ || hitler_rgx.is_match(&lower_name)
+ || goebbels_rgx.is_match(&lower_name)
+ || himmler_rgx.is_match(&lower_name)
+ || heydrich_rgx.is_match(&lower_name)
+ || globocnik_rgx.is_match(&lower_name)
+ || dirlewanger_rgx.is_match(&lower_name)
+ || jeckeln_rgx.is_match(&lower_name)
+ || kramer_rgx.is_match(&lower_name)
+ || blobel_rgx.is_match(&lower_name)
+ || stangl_rgx.is_match(&lower_name)
+ || rapist_rgx.is_match(&lower_name)
+ || molester_rgx.is_match(&lower_name)
+ {
+ let msg = "forbidden name".to_owned();
+ let username = username.to_owned();
+ tx.send(PostType::Kick(msg, username)).unwrap();
+ }
+ }
+ }
+ }
+ }
+}
+
+struct CustomClient<'a> {
+ le_chat_php_client: LeChatPHPClient<'a>,
+}
+
+impl ChatClient for CustomClient<'_> {
+ fn run_forever(&mut self) {
+ self.le_chat_php_client.run_forever();
+ }
+}
+
+impl<'a> CustomClient<'a> {
+ fn new(params: Params<'a>) -> Self {
+ let mut c = new_default_le_chat_php_client(params.clone());
+ c.config.url = params.url.unwrap_or("".to_owned());
+ c.config.page_php = params.page_php.unwrap_or("chat.php".to_owned());
+ c.config.datetime_fmt = params.datetime_fmt.unwrap_or("%m-%d %H:%M:%S".to_owned());
+ c.config.members_tag = params.members_tag.unwrap_or("[M] ".to_owned());
+ c.config.keepalive_send_to = None;
+ Self {
+ le_chat_php_client: c,
+ }
+ }
+}
+
+struct BHClient<'a> {
+ le_chat_php_client: LeChatPHPClient<'a>,
+}
+
+impl ChatClient for BHClient<'_> {
+ fn run_forever(&mut self) {
+ self.le_chat_php_client.run_forever();
+ }
+}
+
+fn new_default_le_chat_php_client(params: Params) -> LeChatPHPClient {
+ let (color_tx, color_rx) = crossbeam_channel::unbounded();
+ let (tx, rx) = crossbeam_channel::unbounded();
+ LeChatPHPClient {
+ base_client: BaseClient {
+ username: params.username,
+ password: params.password,
+ },
+ max_login_retry: params.max_login_retry,
+ guest_color: params.guest_color,
+ session: "".to_owned(),
+ client: params.client,
+ dkf_api_key: params.dkf_api_key,
+ manual_captcha: params.manual_captcha,
+ refresh_rate: params.refresh_rate,
+ config: LeChatPHPConfig::new_black_hat_chat_config(),
+ is_muted: Arc::new(Mutex::new(false)),
+ show_sys: false,
+ display_guest_view: false,
+ display_hidden_msgs: false,
+ tx,
+ rx: Arc::new(Mutex::new(rx)),
+ color_tx,
+ color_rx: Arc::new(Mutex::new(color_rx)),
+ }
+}
+
+impl<'a> BHClient<'a> {
+ fn new(params: Params<'a>) -> Self {
+ let mut c = new_default_le_chat_php_client(params);
+ c.config = LeChatPHPConfig::new_black_hat_chat_config();
+ c.manual_captcha = true;
+ Self {
+ le_chat_php_client: c,
+ }
+ }
+}
+
+trait ChatClient {
+ fn run_forever(&mut self);
+}
+
+struct DanClient<'a> {
+ le_chat_php_client: LeChatPHPClient<'a>,
+}
+
+impl ChatClient for DanClient<'_> {
+ fn run_forever(&mut self) {
+ self.le_chat_php_client.run_forever();
+ }
+}
+
+impl<'a> DanClient<'a> {
+ fn new(params: Params<'a>) -> Self {
+ let mut c = new_default_le_chat_php_client(params);
+ c.config = LeChatPHPConfig::new_dans_chat_config();
+ c.manual_captcha = true;
+ Self {
+ le_chat_php_client: c,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+struct Params<'a> {
+ url: Option<String>,
+ page_php: Option<String>,
+ datetime_fmt: Option<String>,
+ members_tag: Option<String>,
+ username: String,
+ password: String,
+ guest_color: String,
+ client: &'a Client,
+ dkf_api_key: Option<String>,
+ manual_captcha: bool,
+ refresh_rate: u64,
+ max_login_retry: isize,
+}
+
+#[derive(Clone)]
+enum ExitSignal {
+ Terminate,
+ NeedLogin,
+}
+struct Sig {
+ tx: crossbeam_channel::Sender<ExitSignal>,
+ rx: crossbeam_channel::Receiver<ExitSignal>,
+ nb_rx: usize,
+}
+
+impl Sig {
+ fn new() -> Self {
+ let (tx, rx) = crossbeam_channel::unbounded();
+ let nb_rx = 0;
+ Self { tx, rx, nb_rx }
+ }
+
+ fn clone(&mut self) -> crossbeam_channel::Receiver<ExitSignal> {
+ self.nb_rx += 1;
+ self.rx.clone()
+ }
+
+ fn signal(&self, signal: ExitSignal) {
+ for _ in 0..self.nb_rx {
+ self.tx.send(signal.clone()).unwrap();
+ }
+ }
+}
+
+fn trim_newline(s: &mut String) {
+ if s.ends_with('\n') {
+ s.pop();
+ if s.ends_with('\r') {
+ s.pop();
+ }
+ }
+}
+
+fn get_guest_color(wanted: Option<String>) -> String {
+ match wanted.as_deref() {
+ Some("beige") => "F5F5DC",
+ Some("blue-violet") => "8A2BE2",
+ Some("brown") => "A52A2A",
+ Some("cyan") => "00FFFF",
+ Some("sky-blue") => "00BFFF",
+ Some("gold") => "FFD700",
+ Some("gray") => "808080",
+ Some("green") => "008000",
+ Some("hot-pink") => "FF69B4",
+ Some("light-blue") => "ADD8E6",
+ Some("light-green") => "90EE90",
+ Some("lime-green") => "32CD32",
+ Some("magenta") => "FF00FF",
+ Some("olive") => "808000",
+ Some("orange") => "FFA500",
+ Some("orange-red") => "FF4500",
+ Some("red") => "FF0000",
+ Some("royal-blue") => "4169E1",
+ Some("see-green") => "2E8B57",
+ Some("sienna") => "A0522D",
+ Some("silver") => "C0C0C0",
+ Some("tan") => "D2B48C",
+ Some("teal") => "008080",
+ Some("violet") => "EE82EE",
+ Some("white") => "FFFFFF",
+ Some("yellow") => "FFFF00",
+ Some("yellow-green") => "9ACD32",
+ Some(other) => COLOR1_RGX
+ .captures(other)
+ .map_or("", |captures| captures.get(1).map_or("", |m| m.as_str())),
+ None => "",
+ }
+ .to_owned()
+}
+
+fn get_tor_client(socks_proxy_url: &str) -> Client {
+ // Create client
+ let mut builder = reqwest::blocking::ClientBuilder::new()
+ .cookie_store(true)
+ .user_agent("Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0");
+
+ if socks_proxy_url != "" {
+ let proxy = match reqwest::Proxy::all(socks_proxy_url) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("{}", e);
+ process::exit(1);
+ }
+ };
+ builder = builder.proxy(proxy);
+ }
+
+ let client = match builder.build() {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("{}", e);
+ process::exit(1);
+ }
+ };
+ client
+}
+
+fn ask_username(username: Option<String>) -> String {
+ match username {
+ Some(u) => u,
+ None => {
+ print!("username: ");
+ let mut username_input = String::new();
+ io::stdout().flush().unwrap();
+ io::stdin().read_line(&mut username_input).unwrap();
+ trim_newline(&mut username_input);
+ username_input
+ }
+ }
+}
+
+fn ask_password(password: Option<String>) -> String {
+ match password {
+ Some(p) => p,
+ None => rpassword::prompt_password_stdout("Password: ").unwrap(),
+ }
+}
+
+enum ClientType {
+ BHC,
+ Dan,
+ Custom,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DkfNotifierResp {
+ #[serde(rename = "NewMessageSound")]
+ pub new_message_sound: bool,
+ #[serde(rename = "TaggedSound")]
+ pub tagged_sound: bool,
+ #[serde(rename = "PmSound")]
+ pub pm_sound: bool,
+ #[serde(rename = "InboxCount")]
+ pub inbox_count: i64,
+ #[serde(rename = "LastMessageCreatedAt")]
+ pub last_message_created_at: String,
+}
+
+fn start_dkf_notifier(client: &Client, dkf_api_key: &str) {
+ let client = client.clone();
+ let dkf_api_key = dkf_api_key.to_owned();
+ let mut last_known_date = chrono::offset::Utc::now();
+ thread::spawn(move || loop {
+ let (_stream, stream_handle) = OutputStream::try_default().unwrap();
+ let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap();
+
+ let params: Vec<(&str, String)> = vec![(
+ "last_known_date",
+ last_known_date.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
+ )];
+ let right_url = format!("{}/api/v1/chat/1/notifier", DKF_URL);
+ if let Ok(resp) = client
+ .post(right_url)
+ .form(¶ms)
+ .header("DKF_API_KEY", &dkf_api_key)
+ .send()
+ {
+ if let Ok(txt) = resp.text() {
+ if let Ok(v) = serde_json::from_str::<DkfNotifierResp>(&txt) {
+ if v.pm_sound || v.tagged_sound {
+ stream_handle.play_raw(source.convert_samples()).unwrap();
+ }
+ last_known_date = DateTime::parse_from_rfc3339(&v.last_message_created_at)
+ .unwrap()
+ .with_timezone(&Utc);
+ }
+ }
+ }
+ thread::sleep(time::Duration::from_secs(5));
+ });
+}
+
+// Start thread that looks for new emails on DNMX every minutes.
+fn start_dnmx_mail_notifier(client: &Client, username: &str, password: &str) {
+ let params: Vec<(&str, &str)> = vec![("login_username", username), ("secretkey", password)];
+ let login_url = format!("{}/src/redirect.php", DNMX_URL);
+ client.post(login_url).form(¶ms).send().unwrap();
+
+ let client_clone = client.clone();
+ thread::spawn(move || loop {
+ let (_stream, stream_handle) = OutputStream::try_default().unwrap();
+ let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap();
+
+ let right_url = format!("{}/src/right_main.php", DNMX_URL);
+ if let Ok(resp) = client_clone.get(right_url).send() {
+ let mut nb_mails = 0;
+ let doc = Document::from(resp.text().unwrap().as_str());
+ if let Some(table) = doc.select(Name("table")).nth(7) {
+ table.select(Name("tr")).skip(1).for_each(|n| {
+ if let Some(td) = n.select(Name("td")).nth(2) {
+ if let Some(_) = td.select(Name("b")).nth(0) {
+ nb_mails += 1;
+ }
+ }
+ });
+ }
+ if nb_mails > 0 {
+ eprintln!("{} new mails", nb_mails);
+ stream_handle.play_raw(source.convert_samples()).unwrap();
+ }
+ }
+ thread::sleep(time::Duration::from_secs(60));
+ });
+}
+
+fn main() -> Result<()> {
+ let mut opts: Opts = Opts::parse();
+
+ // Configs file
+ let cfg: MyConfig = confy::load("bhcli")?;
+ if opts.dkf_api_key.is_none() {
+ opts.dkf_api_key = cfg.dkf_api_key;
+ }
+ if let Some(default_profile) = cfg.profiles.get(&opts.profile) {
+ if opts.username.is_none() {
+ opts.username = Some(default_profile.username.clone());
+ opts.password = Some(default_profile.password.clone());
+ }
+ }
+
+ let client = get_tor_client(&opts.socks_proxy_url);
+
+ // If dnmx username is set, start mail notifier thread
+ if let Some(dnmx_username) = opts.dnmx_username {
+ start_dnmx_mail_notifier(&client, &dnmx_username, &opts.dnmx_password.unwrap())
+ }
+
+ if let Some(dkf_api_key) = &opts.dkf_api_key {
+ start_dkf_notifier(&client, dkf_api_key);
+ }
+
+ let guest_color = get_guest_color(opts.guest_color);
+ let username = ask_username(opts.username);
+ let password = ask_password(opts.password);
+ let params = Params {
+ url: opts.url,
+ page_php: opts.page_php,
+ datetime_fmt: opts.datetime_fmt,
+ members_tag: opts.members_tag,
+ username,
+ password,
+ guest_color,
+ client: &client,
+ dkf_api_key: opts.dkf_api_key,
+ manual_captcha: opts.manual_captcha,
+ refresh_rate: opts.refresh_rate,
+ max_login_retry: opts.max_login_retry,
+ };
+
+ let chat_type = if params.url.is_some() {
+ ClientType::Custom
+ } else if opts.dan {
+ ClientType::Dan
+ } else {
+ ClientType::BHC
+ };
+
+ let mut chat_client: Box<dyn ChatClient> = match chat_type {
+ ClientType::Custom => Box::new(CustomClient::new(params)),
+ ClientType::BHC => Box::new(BHClient::new(params)),
+ ClientType::Dan => Box::new(DanClient::new(params)),
+ };
+ chat_client.run_forever();
+
+ Ok(())
+}
+
+#[derive(Debug, Clone)]
+enum PostType {
+ Post(String, Option<String>), // Message, SendTo
+ Kick(String, String), // Message, Username
+ Upload(String, String, String), // FilePath, SendTo, Message
+ DeleteLast, // DeleteLast
+ DeleteAll, // DeleteAll
+ NewNickname(String), // NewUsername
+ NewColor(String), // NewColor
+ Profile(String, String), // NewColor, NewUsername
+ Ignore(String), // Username
+ Unignore(String), // Username
+ Clean(String, String), // Clean message
+}
+
+// Get username of other user (or ours if it's the only one)
+fn get_username(own_username: &str, root: &StyledText, members_tag: &str) -> Option<String> {
+ match get_message(root, members_tag) {
+ Some((from, Some(to), _)) => {
+ if from == own_username {
+ return Some(to);
+ }
+ return Some(from);
+ }
+ Some((from, None, _)) => {
+ return Some(from);
+ }
+ _ => return None,
+ }
+}
+
+// Extract "from"/"to"/"message content" from a "StyledText"
+fn get_message(root: &StyledText, members_tag: &str) -> Option<(String, Option<String>, String)> {
+ if let StyledText::Styled(_, children) = root {
+ let msg = match children.get(0) {
+ Some(el) => el.text(),
+ _ => return None,
+ };
+ if let Some(StyledText::Styled(_, children)) = children.get(children.len() - 1) {
+ let from = match children.get(children.len() - 1) {
+ Some(StyledText::Text(t)) => t.to_owned(),
+ _ => return None,
+ };
+ return Some((from, None, msg));
+ } else if let Some(StyledText::Text(t)) = children.get(children.len() - 1) {
+ if t == &members_tag {
+ let from = match children.get(children.len() - 2) {
+ Some(StyledText::Styled(_, children)) => match children.get(children.len() - 1)
+ {
+ Some(StyledText::Text(t)) => t.to_owned(),
+ _ => return None,
+ },
+ _ => return None,
+ };
+ return Some((from, None, msg));
+ } else if t == "[" {
+ let from = match children.get(children.len() - 2) {
+ Some(StyledText::Styled(_, children)) => match children.get(children.len() - 1)
+ {
+ Some(StyledText::Text(t)) => t.to_owned(),
+ _ => return None,
+ },
+ _ => return None,
+ };
+ let to = match children.get(2) {
+ Some(StyledText::Styled(_, children)) => match children.get(children.len() - 1)
+ {
+ Some(StyledText::Text(t)) => Some(t.to_owned()),
+ _ => return None,
+ },
+ _ => return None,
+ };
+ return Some((from, to, msg));
+ }
+ }
+ }
+ return None;
+}
+
+#[derive(Debug, PartialEq, Clone)]
+enum MessageType {
+ UserMsg,
+ SysMsg,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+struct Message {
+ id: Option<usize>,
+ typ: MessageType,
+ date: String,
+ upload_link: Option<String>,
+ text: StyledText,
+ deleted: bool, // Either or not a message was deleted on the chat
+ hide: bool, // Either ot not to hide a specific message
+}
+
+#[derive(Debug, PartialEq, Clone)]
+enum StyledText {
+ Styled(tuiColor, Vec<StyledText>),
+ Text(String),
+ None,
+}
+
+impl StyledText {
+ fn walk<F>(&self, mut clb: F)
+ where
+ F: FnMut(StyledText),
+ {
+ let mut v: Vec<&StyledText> = vec![self];
+ loop {
+ if let Some(e) = v.pop() {
+ clb(e.clone());
+ if let StyledText::Styled(_, children) = e {
+ v.extend(children);
+ }
+ continue;
+ }
+ break;
+ }
+ }
+
+ fn text(&self) -> String {
+ let mut s = String::new();
+ self.walk(|n| {
+ if let StyledText::Text(t) = n {
+ s += &t;
+ }
+ });
+ s
+ }
+
+ // Return a vector of each text parts & what color it should be
+ fn colored_text(&self) -> Vec<(tuiColor, String)> {
+ let mut out: Vec<(tuiColor, String)> = vec![];
+ let mut v: Vec<(tuiColor, &StyledText)> = vec![(tuiColor::White, self)];
+ loop {
+ if let Some((el_color, e)) = v.pop() {
+ match e {
+ StyledText::Styled(tui_color, children) => {
+ for child in children {
+ v.push((*tui_color, child));
+ }
+ }
+ StyledText::Text(t) => {
+ out.push((el_color, t.to_owned()));
+ }
+ StyledText::None => {}
+ }
+ continue;
+ }
+ break;
+ }
+ out
+ }
+}
+
+fn parse_color(color_str: &str) -> tuiColor {
+ let mut color = tuiColor::White;
+ if color_str == "red" {
+ return tuiColor::Red;
+ }
+ if let Ok(rgb) = Rgb::from_hex_str(color_str) {
+ color = tuiColor::Rgb(
+ rgb.get_red() as u8,
+ rgb.get_green() as u8,
+ rgb.get_blue() as u8,
+ );
+ }
+ color
+}
+
+fn process_node(e: select::node::Node, mut color: tuiColor) -> (StyledText, Option<String>) {
+ match e.data() {
+ select::node::Data::Element(_, _) => {
+ let mut upload_link: Option<String> = None;
+ if e.name() == Some("span") {
+ if let Some(style) = e.attr("style") {
+ if let Some(captures) = COLOR_RGX.captures(style) {
+ let color_match = captures.get(1).unwrap().as_str();
+ color = parse_color(color_match);
+ }
+ }
+ } else if e.name() == Some("font") {
+ if let Some(color_str) = e.attr("color") {
+ color = parse_color(color_str);
+ }
+ } else if e.name() == Some("a") {
+ color = tuiColor::White;
+ if let Some(class) = e.attr("class") {
+ if class == "attachement" {
+ if let Some(ahref) = e.attr("href") {
+ upload_link = Some(ahref.to_owned());
+ }
+ }
+ }
+ }
+ let mut children_texts: Vec<StyledText> = vec![];
+ let children = e.children();
+ for child in children {
+ let (st, ul) = process_node(child, color);
+ if let Some(_) = &ul {
+ upload_link = ul;
+ }
+ children_texts.push(st);
+ }
+ children_texts.reverse();
+ (StyledText::Styled(color, children_texts), upload_link)
+ }
+ select::node::Data::Text(t) => (StyledText::Text(t.to_string()), None),
+ select::node::Data::Comment(_) => (StyledText::None, None),
+ }
+}
+
+struct Users {
+ admin: Vec<(tuiColor, String)>,
+ staff: Vec<(tuiColor, String)>,
+ members: Vec<(tuiColor, String)>,
+ guests: Vec<(tuiColor, String)>,
+}
+
+impl Default for Users {
+ fn default() -> Self {
+ Self {
+ admin: Default::default(),
+ staff: Default::default(),
+ members: Default::default(),
+ guests: Default::default(),
+ }
+ }
+}
+
+impl Users {
+ fn all(&self) -> Vec<&(tuiColor, String)> {
+ let mut out = Vec::new();
+ out.extend(&self.admin);
+ out.extend(&self.staff);
+ out.extend(&self.members);
+ out.extend(&self.guests);
+ out
+ }
+}
+
+fn extract_users(doc: &Document) -> Users {
+ let mut admin = Vec::new();
+ let mut staff = Vec::new();
+ let mut members = Vec::new();
+ let mut guests = Vec::new();
+
+ if let Some(chatters) = doc.select(Attr("id", "chatters")).next() {
+ if let Some(tr) = chatters.select(Name("tr")).next() {
+ let mut th_count = 0;
+ for e in tr.children() {
+ if let select::node::Data::Element(_, _) = e.data() {
+ if e.name() == Some("th") {
+ th_count += 1;
+ continue;
+ }
+ for user_span in e.select(Name("span")) {
+ if let Some(user_style) = user_span.attr("style") {
+ if let Some(captures) = COLOR_RGX.captures(user_style) {
+ if let Some(color_match) = captures.get(1) {
+ let color = color_match.as_str().to_owned();
+ let tui_color = parse_color(&color);
+ let username = user_span.text();
+ match th_count {
+ 1 => admin.push((tui_color, username)),
+ 2 => staff.push((tui_color, username)),
+ 3 => members.push((tui_color, username)),
+ 4 => guests.push((tui_color, username)),
+ _ => {}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ Users {
+ admin,
+ staff,
+ members,
+ guests,
+ }
+}
+
+fn remove_suffix<'a>(s: &'a str, suffix: &str) -> &'a str {
+ match s.strip_suffix(suffix) {
+ Some(s) => s,
+ None => s,
+ }
+}
+
+fn remove_prefix<'a>(s: &'a str, prefix: &str) -> &'a str {
+ match s.strip_prefix(prefix) {
+ Some(s) => s,
+ None => s,
+ }
+}
+
+fn extract_messages(doc: &Document) -> Result<Vec<Message>> {
+ let msgs = doc
+ .select(Attr("id", "messages"))
+ .next()
+ .ok_or("failed to get messages div")?
+ .select(Attr("class", "msg"))
+ .filter_map(|tag| {
+ let mut id: Option<usize> = None;
+ if let Some(checkbox) = tag.select(Name("input")).next() {
+ let id_value: usize = checkbox.attr("value").unwrap().parse().unwrap();
+ id = Some(id_value);
+ }
+ if let Some(date_node) = tag.select(Name("small")).next() {
+ if let Some(msg_span) = tag.select(Name("span")).next() {
+ let date = remove_suffix(&date_node.text(), " - ").to_owned();
+ let typ = match msg_span.attr("class") {
+ Some("usermsg") => MessageType::UserMsg,
+ Some("sysmsg") => MessageType::SysMsg,
+ _ => return None,
+ };
+ let (text, upload_link) = process_node(msg_span, tuiColor::White);
+ return Some(Message {
+ id,
+ typ,
+ date,
+ upload_link,
+ text,
+ deleted: false,
+ hide: false,
+ });
+ }
+ }
+ None
+ })
+ .collect::<Vec<_>>();
+ Ok(msgs)
+}
+
+fn draw_terminal_frame(
+ f: &mut Frame<CrosstermBackend<io::Stdout>>,
+ app: &mut App,
+ messages: &Arc<Mutex<Vec<Message>>>,
+ users: &Arc<Mutex<Users>>,
+) {
+ if app.long_message.is_none() {
+ let hchunks = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints([Constraint::Min(1), Constraint::Length(25)].as_ref())
+ .split(f.size());
+
+ {
+ let chunks = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints(
+ [
+ Constraint::Length(1),
+ Constraint::Length(3),
+ Constraint::Min(1),
+ ]
+ .as_ref(),
+ )
+ .split(hchunks[0]);
+
+ render_help_txt(f, app, chunks[0]);
+ render_textbox(f, app, chunks[1]);
+ render_messages(f, app, chunks[2], messages);
+ render_users(f, hchunks[1], users);
+ }
+ } else {
+ let hchunks = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints([Constraint::Min(1)])
+ .split(f.size());
+ {
+ render_long_message(f, app, hchunks[0]);
+ }
+ }
+}
+
+fn gen_lines(msg_txt: &StyledText, w: usize, line_prefix: String) -> Vec<Vec<(tuiColor, String)>> {
+ let txt = msg_txt.text();
+ let wrapped = textwrap::fill(&txt, w);
+ let splits = wrapped.split("\n").collect::<Vec<&str>>();
+ let mut new_lines: Vec<Vec<(tuiColor, String)>> = Vec::new();
+ let mut ctxt = msg_txt.colored_text();
+ ctxt.reverse();
+ let mut ptr = 0;
+ let mut split_idx = 0;
+ let mut line: Vec<(tuiColor, String)> = Vec::new();
+ let mut first_in_line = true;
+ loop {
+ if let Some((color, mut txt)) = ctxt.pop() {
+ txt = txt.replace("\n", "");
+ if let Some(split) = splits.get(split_idx) {
+ if let Some(chr) = txt.chars().next() {
+ if chr == ' ' && first_in_line {
+ let skipped: String = txt.chars().skip(1).collect();
+ txt = skipped;
+ }
+ }
+
+ let txt = txt.as_str();
+
+ let remain = split.len() - ptr;
+ if txt.len() <= remain {
+ ptr += txt.len();
+ line.push((color, txt.to_owned()));
+ first_in_line = false;
+ } else {
+ line.push((color, txt[0..remain].to_owned()));
+ new_lines.push(line.clone());
+ line.clear();
+ line.push((tuiColor::White, line_prefix.clone()));
+ ctxt.push((color, txt[(remain)..].to_owned()));
+ ptr = 0;
+ split_idx += 1;
+ first_in_line = true;
+ }
+ }
+ } else {
+ new_lines.push(line.clone());
+ break;
+ }
+ }
+ new_lines
+}
+
+fn render_long_message(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
+ if let Some(m) = &app.long_message {
+ let new_lines = gen_lines(&m.text, (r.width - 2) as usize, "".to_owned());
+
+ let mut rows = vec![];
+ let mut spans_vec = vec![];
+ for line in new_lines.into_iter() {
+ for (color, txt) in line {
+ spans_vec.push(Span::styled(txt, Style::default().fg(color)));
+ }
+ rows.push(Spans::from(spans_vec.clone()));
+ spans_vec.clear();
+ }
+
+ let messages_list_items = vec![ListItem::new(rows)];
+
+ let messages_list = List::new(messages_list_items)
+ .block(Block::default().borders(Borders::ALL).title(""))
+ .highlight_style(
+ Style::default()
+ .bg(tuiColor::Rgb(50, 50, 50))
+ .add_modifier(Modifier::BOLD),
+ );
+
+ f.render_widget(messages_list, r);
+ }
+}
+
+fn render_help_txt(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
+ let (mut msg, style) = match app.input_mode {
+ InputMode::Normal => (
+ vec![
+ Span::raw("Press "),
+ Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
+ Span::raw(" to exit, "),
+ Span::styled("i", Style::default().add_modifier(Modifier::BOLD)),
+ Span::raw(" to start editing."),
+ ],
+ Style::default(),
+ ),
+ InputMode::Editing => (
+ vec![
+ Span::raw("Press "),
+ Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
+ Span::raw(" to stop editing, "),
+ Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
+ Span::raw(" to record the message"),
+ ],
+ Style::default(),
+ ),
+ InputMode::LongMessage => (vec![], Style::default()),
+ };
+ if app.is_muted {
+ let fg = tuiColor::Red;
+ let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
+ msg.extend(vec![Span::raw(" | "), Span::styled("muted", style)]);
+ } else {
+ let fg = tuiColor::LightGreen;
+ let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
+ msg.extend(vec![Span::raw(" | "), Span::styled("not muted", style)]);
+ }
+ if app.display_guest_view {
+ let fg = tuiColor::LightGreen;
+ let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
+ msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]);
+ } else {
+ let fg = tuiColor::Gray;
+ let style = Style::default().fg(fg);
+ msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]);
+ }
+ if app.display_hidden_msgs {
+ let fg = tuiColor::LightGreen;
+ let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
+ msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]);
+ } else {
+ let fg = tuiColor::Gray;
+ let style = Style::default().fg(fg);
+ msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]);
+ }
+ let mut text = Text::from(Spans::from(msg));
+ text.patch_style(style);
+ let help_message = Paragraph::new(text);
+ f.render_widget(help_message, r);
+}
+
+fn render_textbox(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
+ let w = (r.width - 3) as usize;
+ let str = app.input.clone();
+ let mut input_str = str.as_str();
+ let mut overflow = 0;
+ if app.input_idx >= w {
+ overflow = std::cmp::max(app.input.width() - w, 0);
+ input_str = &str[overflow..];
+ }
+ let input = Paragraph::new(input_str)
+ .style(match app.input_mode {
+ InputMode::LongMessage => Style::default(),
+ InputMode::Normal => Style::default(),
+ InputMode::Editing => Style::default().fg(tuiColor::Yellow),
+ })
+ .block(Block::default().borders(Borders::ALL).title("Input"));
+ f.render_widget(input, r);
+ match app.input_mode {
+ InputMode::LongMessage => {}
+ InputMode::Normal =>
+ // Hide the cursor. `Frame` does this by default, so we don't need to do anything here
+ {}
+
+ InputMode::Editing => {
+ // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
+ f.set_cursor(
+ // Put cursor past the end of the input text
+ r.x + app.input_idx as u16 - overflow as u16 + 1,
+ // Move one line down, from the border to the input line
+ r.y + 1,
+ )
+ }
+ }
+}
+
+fn render_messages(
+ f: &mut Frame<CrosstermBackend<io::Stdout>>,
+ app: &mut App,
+ r: Rect,
+ messages: &Arc<Mutex<Vec<Message>>>,
+) {
+ // Messages
+ app.items.items.clear();
+ let messages = messages.lock().unwrap();
+ let messages_list_items: Vec<ListItem> = messages
+ .iter()
+ .filter_map(|m| {
+ if !app.display_hidden_msgs && m.hide {
+ return None;
+ }
+ // Simulate a guest view (remove "PMs" and "Members chat" messages)
+ if app.display_guest_view {
+ // TODO: this is not efficient at all
+ if m.text.text().starts_with(&app.members_tag)
+ || m.text.text().starts_with(&app.staffs_tag)
+ {
+ return None;
+ }
+ if let Some((_, Some(_), _)) = get_message(&m.text, &app.members_tag) {
+ return None;
+ }
+ }
+
+ if app.filter != "" {
+ if !m
+ .text
+ .text()
+ .to_lowercase()
+ .contains(&app.filter.to_lowercase())
+ {
+ return None;
+ }
+ }
+
+ app.items.items.push(m.clone());
+
+ let new_lines = gen_lines(&m.text, (r.width - 20) as usize, " ".repeat(17));
+
+ let mut rows = vec![];
+ let date_style = match (m.deleted, m.hide) {
+ (false, true) => Style::default().fg(tuiColor::Gray),
+ (false, _) => Style::default().fg(tuiColor::DarkGray),
+ (true, _) => Style::default().fg(tuiColor::Red),
+ };
+ let mut spans_vec = vec![Span::styled(m.date.clone(), date_style)];
+ let show_sys_sep = app.show_sys && m.typ == MessageType::SysMsg;
+ let sep = if show_sys_sep { " * " } else { " - " };
+ spans_vec.push(Span::raw(sep));
+ for (idx, line) in new_lines.into_iter().enumerate() {
+ // Spams can take your whole screen, so we limit to 5 lines.
+ if idx >= 5 {
+ spans_vec.push(Span::styled(
+ " […]",
+ Style::default().fg(tuiColor::White),
+ ));
+ rows.push(Spans::from(spans_vec.clone()));
+ break;
+ }
+ for (color, txt) in line {
+ spans_vec.push(Span::styled(txt, Style::default().fg(color)));
+ }
+ rows.push(Spans::from(spans_vec.clone()));
+ spans_vec.clear();
+ }
+
+ let mut list_item = ListItem::new(rows);
+ if m.deleted {
+ list_item = list_item.style(Style::default().bg(tuiColor::Rgb(30, 0, 0)));
+ } else if m.hide {
+ list_item = list_item.style(Style::default().bg(tuiColor::Rgb(20, 20, 20)));
+ }
+
+ Some(list_item)
+ })
+ .collect();
+
+ let messages_list = List::new(messages_list_items)
+ .block(Block::default().borders(Borders::ALL).title("Messages"))
+ .highlight_style(
+ Style::default()
+ .bg(tuiColor::Rgb(50, 50, 50))
+ .add_modifier(Modifier::BOLD),
+ );
+ f.render_stateful_widget(messages_list, r, &mut app.items.state)
+}
+
+fn render_users(f: &mut Frame<CrosstermBackend<io::Stdout>>, r: Rect, users: &Arc<Mutex<Users>>) {
+ // Users lists
+ let users = users.lock().unwrap();
+ let mut users_list: Vec<ListItem> = vec![];
+ let mut users_types: Vec<&Vec<(tuiColor, String)>> = Vec::new();
+ users_types.push(&users.admin);
+ users_types.push(&users.staff);
+ users_types.push(&users.members);
+ users_types.push(&users.guests);
+ for (i, users_type) in users_types.iter().enumerate() {
+ match i {
+ 0 => users_list.push(ListItem::new(Span::raw("-- Admin --"))),
+ 1 => users_list.push(ListItem::new(Span::raw("-- Staff --"))),
+ 2 => users_list.push(ListItem::new(Span::raw("-- Members --"))),
+ 3 => users_list.push(ListItem::new(Span::raw("-- Guests --"))),
+ _ => {}
+ }
+ for (tui_color, username) in users_type.iter() {
+ let span = Span::styled(username, Style::default().fg(*tui_color));
+ users_list.push(ListItem::new(span));
+ }
+ }
+ let users = List::new(users_list).block(Block::default().borders(Borders::ALL).title("Users"));
+ f.render_widget(users, r);
+}
+
+fn random_string(n: usize) -> String {
+ let s: Vec<u8> = thread_rng().sample_iter(&Alphanumeric).take(n).collect();
+ std::str::from_utf8(&s).unwrap().to_owned()
+}
+
+enum InputMode {
+ LongMessage,
+ Normal,
+ Editing,
+}
+
+/// App holds the state of the application
+struct App {
+ /// Current value of the input box
+ input: String,
+ input_idx: usize,
+ /// Current input mode
+ input_mode: InputMode,
+ is_muted: bool,
+ show_sys: bool,
+ display_guest_view: bool,
+ display_hidden_msgs: bool,
+ items: StatefulList<Message>,
+ filter: String,
+ members_tag: String,
+ staffs_tag: String,
+ long_message: Option<Message>,
+}
+
+impl Default for App {
+ fn default() -> App {
+ App {
+ input: String::new(),
+ input_idx: 0,
+ input_mode: InputMode::Normal,
+ is_muted: false,
+ show_sys: false,
+ display_guest_view: false,
+ display_hidden_msgs: false,
+ items: StatefulList::new(),
+ filter: "".to_owned(),
+ members_tag: "".to_owned(),
+ staffs_tag: "".to_owned(),
+ long_message: None,
+ }
+ }
+}
+
+impl App {
+ fn update_filter(&mut self) {
+ if let Some(captures) = FIND_RGX.captures(&self.input) {
+ // Find
+ self.filter = captures.get(1).map_or("", |m| m.as_str()).to_owned();
+ }
+ }
+
+ fn clear_filter(&mut self) {
+ if FIND_RGX.is_match(&self.input) {
+ self.filter = "".to_owned();
+ self.input = "".to_owned();
+ self.input_idx = 0;
+ }
+ }
+}
+
+pub enum Event<I> {
+ Input(I),
+ Tick,
+ Terminate,
+ NeedLogin,
+}
+
+/// A small event handler that wrap termion input and tick events. Each event
+/// type is handled in its own thread and returned to a common `Receiver`
+struct Events {
+ messages_updated_rx: crossbeam_channel::Receiver<bool>,
+ exit_rx: crossbeam_channel::Receiver<ExitSignal>,
+ rx: crossbeam_channel::Receiver<Event<CEvent>>,
+}
+
+#[derive(Debug, Clone)]
+struct Config {
+ pub exit_rx: crossbeam_channel::Receiver<ExitSignal>,
+ pub messages_updated_rx: crossbeam_channel::Receiver<bool>,
+ pub tick_rate: Duration,
+}
+
+impl Events {
+ fn with_config(config: Config) -> (Events, thread::JoinHandle<()>) {
+ let (tx, rx) = crossbeam_channel::unbounded();
+ let tick_rate = config.tick_rate;
+ let exit_rx = config.exit_rx;
+ let messages_updated_rx = config.messages_updated_rx;
+ let exit_rx1 = exit_rx.clone();
+ let h = thread::spawn(move || {
+ let mut last_tick = Instant::now();
+ loop {
+ // poll for tick rate duration, if no events, sent tick event.
+ let timeout = tick_rate
+ .checked_sub(last_tick.elapsed())
+ .unwrap_or_else(|| Duration::from_secs(0));
+ if event::poll(timeout).unwrap() {
+ let evt = event::read().unwrap();
+ match evt {
+ CEvent::Resize(_, _) => tx.send(Event::Input(evt)).unwrap(),
+ CEvent::Key(_) => tx.send(Event::Input(evt)).unwrap(),
+ CEvent::Mouse(mouse_event) => {
+ match mouse_event.kind {
+ event::MouseEventKind::ScrollDown
+ | event::MouseEventKind::ScrollUp
+ | event::MouseEventKind::Down(_) => {
+ tx.send(Event::Input(evt)).unwrap();
+ }
+ _ => {}
+ };
+ }
+ };
+ }
+ if last_tick.elapsed() >= tick_rate {
+ select! {
+ recv(&exit_rx1) -> _ => break,
+ default => {},
+ }
+ last_tick = Instant::now();
+ }
+ }
+ });
+ (
+ Events {
+ rx,
+ exit_rx,
+ messages_updated_rx,
+ },
+ h,
+ )
+ }
+
+ fn next(&self) -> std::result::Result<Event<CEvent>, crossbeam_channel::RecvError> {
+ select! {
+ recv(&self.rx) -> evt => evt,
+ recv(&self.messages_updated_rx) -> _ => Ok(Event::Tick),
+ recv(&self.exit_rx) -> v => match v {
+ Ok(ExitSignal::Terminate) => Ok(Event::Terminate),
+ Ok(ExitSignal::NeedLogin) => Ok(Event::NeedLogin),
+ Err(_) => Ok(Event::Terminate),
+ },
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn gen_lines_test() {
+ let txt = StyledText::Styled(
+ tuiColor::White,
+ vec![
+ StyledText::Styled(
+ tuiColor::Rgb(255, 255, 255),
+ vec![
+ StyledText::Text(" prmdbba pwuv💓".to_owned()),
+ StyledText::Styled(
+ tuiColor::Rgb(255, 255, 255),
+ vec![StyledText::Styled(
+ tuiColor::Rgb(0, 255, 0),
+ vec![StyledText::Text("PMW".to_owned())],
+ )],
+ ),
+ StyledText::Styled(
+ tuiColor::Rgb(255, 255, 255),
+ vec![StyledText::Styled(
+ tuiColor::Rgb(255, 255, 255),
+ vec![StyledText::Text("A".to_owned())],
+ )],
+ ),
+ StyledText::Styled(
+ tuiColor::Rgb(255, 255, 255),
+ vec![StyledText::Styled(
+ tuiColor::Rgb(0, 255, 0),
+ vec![StyledText::Text("XOS".to_owned())],
+ )],
+ ),
+ StyledText::Text(
+ "pqb a mavx pkj fhsoeycg oruzb asd lk ruyaq re lheot mbnrw ".to_owned(),
+ ),
+ ],
+ ),
+ StyledText::Text(" - ".to_owned()),
+ StyledText::Styled(
+ tuiColor::Rgb(255, 255, 255),
+ vec![StyledText::Text("rytxvgs".to_owned())],
+ ),
+ ],
+ );
+ let lines = gen_lines(&txt, 71, "".to_owned());
+ assert_eq!(lines.len(), 2);
+ }
+}
diff --git a/src/sound1.mp3 b/src/sound1.mp3
Binary files differ.
diff --git a/src/util/event.rs b/src/util/event.rs
@@ -0,0 +1 @@
+
diff --git a/src/util/mod.rs b/src/util/mod.rs
@@ -0,0 +1,55 @@
+pub mod event;
+
+use tui::widgets::ListState;
+
+pub struct StatefulList<T> {
+ pub state: ListState,
+ pub items: Vec<T>,
+}
+
+impl<T> StatefulList<T> {
+ pub fn new() -> StatefulList<T> {
+ StatefulList {
+ state: ListState::default(),
+ items: Vec::new(),
+ }
+ }
+
+ pub fn next(&mut self) {
+ if self.items.len() == 0 {
+ return;
+ }
+ let i = match self.state.selected() {
+ Some(i) => {
+ if i >= self.items.len() - 1 {
+ 0
+ } else {
+ i + 1
+ }
+ }
+ None => 0,
+ };
+ self.state.select(Some(i));
+ }
+
+ pub fn previous(&mut self) {
+ if self.items.len() == 0 {
+ return;
+ }
+ let i = match self.state.selected() {
+ Some(i) => {
+ if i == 0 {
+ self.items.len() - 1
+ } else {
+ i - 1
+ }
+ }
+ None => 0,
+ };
+ self.state.select(Some(i));
+ }
+
+ pub fn unselect(&mut self) {
+ self.state.select(None);
+ }
+}