bhcli

A TUI for chatting on LE PHP Chats
git clone https://git.dasho.dev/bhcli.git
Log | Files | Refs | README

commit d36b7456588cd8f363fee081148bcf98ac226b13
Author: Dasho <git@dasho.dev>
Date:   Tue, 15 Jul 2025 14:14:34 +0100

init(bhcli): Existing code copied from http://git.dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/Strange/bhcli

Diffstat:
A.gitignore | 7+++++++
A.vscode/settings.json | 4++++
ACargo.lock | 3538+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACargo.toml | 41+++++++++++++++++++++++++++++++++++++++++
ADockerfile | 3+++
AMakefile | 43+++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascreenshot.png | 0
Asrc/bhc/mod.rs | 0
Asrc/lechatphp/captcha.rs | 314+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lechatphp/mod.rs | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main.rs | 3380+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/sound1.mp3 | 0
Asrc/util/event.rs | 1+
Asrc/util/mod.rs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astrange_bhcli.jpg | 0
16 files changed, 7772 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,7 @@ +captcha.gif +samples +target +dist +*.log +*.svg +*.env diff --git a/.vscode/settings.json b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "makefile.configureOnOpen": true +} +\ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock @@ -0,0 +1,3538 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.6.0", + "cfg-if", + "libc", +] + +[[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 = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.4", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bhcli" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "bresenham", + "chrono", + "clap 4.5.17", + "clipboard", + "colors-transform", + "confy", + "crossbeam", + "crossbeam-channel", + "crossterm 0.26.1", + "http", + "image 0.24.9", + "lazy_static", + "linkify", + "log", + "log4rs", + "rand", + "regex", + "reqwest", + "rodio", + "rpassword", + "select", + "serde", + "serde_derive", + "serde_json", + "termage", + "textwrap 0.16.1", + "toml 0.7.8", + "tui", + "unicode-width", +] + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2 1.0.86", + "quote 1.0.37", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.77", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +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 = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bresenham" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc1116225f66d2ea341a26503f83a6b1205070a6f7199ce1f1550ead91f6fd7" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cc" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[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.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "4.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +dependencies = [ + "heck", + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[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 = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "confy" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" +dependencies = [ + "directories", + "serde", + "thiserror", + "toml 0.5.11", +] + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "deflate" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 1.0.109", +] + +[[package]] +name = "destructure_traitobject" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" + +[[package]] +name = "directories" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide 0.7.4", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + +[[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.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +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 = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "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 0.1.22", + "lzw", + "num-derive 0.2.5", + "num-iter", + "num-rational", + "num-traits", + "png 0.12.0", + "scoped_threadpool", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif 0.13.1", + "jpeg-decoder 0.3.1", + "num-traits", + "png 0.17.13", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inflate" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +dependencies = [ + "adler32", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +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 = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[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.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "linkify" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96dd5884008358112bc66093362197c7248ece00d46624e2cf71e50029f8cff5" +dependencies = [ + "memchr", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "serde", +] + +[[package]] +name = "log-mdc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" + +[[package]] +name = "log4rs" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" +dependencies = [ + "anyhow", + "arc-swap", + "chrono", + "derivative", + "fnv", + "humantime", + "libc", + "log", + "log-mdc", + "once_cell", + "parking_lot", + "rand", + "serde", + "serde-value", + "serde_json", + "serde_yaml", + "thiserror", + "thread-id", + "typemap-ors", + "winapi", +] + +[[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 = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + +[[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 = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive 0.4.2", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +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.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54b9600d584d3b8a739e1662a595fab051329eff43f20e7d8cc22872962145b" +dependencies = [ + "bitflags 1.3.2", + "deflate", + "inflate", + "num-iter", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.4", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[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 = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.20", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[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.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2 1.0.86", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-socks", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rodio" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1bb7b48ee48471f55da122c0044fcc7600cfcc85db88240b89cb832935e611" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "symphonia", +] + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "select" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9da09dc3f4dfdb6374cbffff7a2cffcec316874d4429899eefdc97b3b94dcd" +dependencies = [ + "bit-set", + "html5ever", + "markup5ever_rcdom", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2 1.0.86", + "quote 1.0.37", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[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", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +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.34.0", + "image 0.19.0", + "terminal_graphics", + "terminal_size", +] + +[[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.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + +[[package]] +name = "thread-id" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder 0.3.1", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio 1.0.2", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.6.18", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +dependencies = [ + "bitflags 1.3.2", + "cassowary", + "crossterm 0.25.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "typemap-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" +dependencies = [ + "unsafe-any-ors", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unsafe-any-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" +dependencies = [ + "destructure_traitobject", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote 1.0.37", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[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 = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[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.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" +dependencies = [ + "log", + "mac", + "markup5ever", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "bhcli" +version = "0.1.0" +edition = "2018" + +[profile.dev] +debug = true +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.70" +base64 = "0.21.0" +bresenham = "0.1.1" +chrono = "0.4.19" +clap = { version = "4.1.14", features = ["derive", "env"] } +clipboard = "0.5.0" +colors-transform = "0.2.4" +confy = "0.5.1" +crossbeam = "0.8.1" +crossbeam-channel = "0.5.1" +crossterm = { version = "0.26.1" } +http = "0.2.4" +image = "0.24.6" +lazy_static = "1.4.0" +linkify = "0.9.0" +log = "0.4.17" +log4rs = "1.2.0" +rand = "0.8.4" +regex = "1.5.4" +reqwest = { version = "0.11.4", features = ["blocking", "cookies", "socks", "multipart"] } +rodio = "0.17.1" +rpassword = "7.2.0" +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.16.0" +toml = "0.7.3" +tui = { version = "0.19.0", features = ["crossterm"], default-features = false } +unicode-width = "0.1.8" diff --git a/Dockerfile b/Dockerfile @@ -0,0 +1,3 @@ +FROM rust:1.68 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,43 @@ +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/ dkf:/home/dkf/dist/downloads-bhcli + +deploy: build-darwin cross-compile-windows build-linux rsync + +linux: + cargo build --release + @echo "Copying binary to /opt requires sudo privileges." + @echo "Ensure /opt is added to your \$$PATH variable to use bhcli globally from any path." + sudo cp target/release/bhcli /opt + +.PHONY: build-darwin process-windows cross-compile-windows rsync linux diff --git a/README.md b/README.md @@ -0,0 +1,123 @@ +# BHCLI + +![screenshot](strange_bhcli.jpg "strange_bhcli") + +## Description + +This is a CLI client for any of [le-chat-php](https://github.com/DanWin/le-chat-php) +Officially supported chats are [Black Hat Chat](http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion) + +Tested working on [ --url ] : +- [PopPooB's Chat](http://vfdvqflzfgwnejh6rrzjnuxvbnpgjr4ursv4moombwyauot5c2z6ebid.onion/chat.php) + +## Pre-built binaries + +Pre-buit binaries can be found on the [official website](http://git.dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/Strange/bhcli/releases) + +## 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` +- Toggle a "members" view, by filtering out PMs and "Guest chat" `shift+M` +- 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 +- `ctrl + w` or !warn username to send a pre-kick warning message to a user + [ Only for members+ users ] + > This is your warning @username, will be kicked next !rules +- Can hide messages with `backspace`, hidden messages can be viewed by toggling + `ctrl+ H`. + > - Hidden messages are just hidden from the view, they are not deleted + > - Deleted messages once hidden can't be viewed again +- Download an embedded file into cwd with `d` +- Download an embedded file and open it with xdg-open into cwd with `D` +- `shift + T` for translating text to english. [ must have translate-shell installed on arch or debain ] + > pacman -S translate-shell +- Custom personal command creation for members+ [ read Command Creation ] + +### 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 down 5 message `J(CAPS)` +- Going up 1 message `k` | `up arrow` +- Going up 5 message `K(CAPS)` +- Jump to Top Message `gg` + +## 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` +- The manual way +> - Compile with `cargo build --release` +> - Run with `./target/release/bhcli` +> - You can move the binary to `/opt` to make it available system wide [ given that u have /opt in $PATH ] +- The MAKEFILE way +> - Compile with `make linux` +> - Run with bhcli [ given that u have /opt in $PATH ] +- The bhcli.log file will be created in the same directory as the pwd you run + the binary from + +## Cross compile + +`cargo build --release --target x86_64-pc-windows-gnu` + + +## Profiles + +To automatically login when starting the application, you can put the following content in your config file `/path/to/rs.bhcli/default-config.toml` + +```toml +[profiles] + +[profiles.default] +username = "username" +password = "password" +``` +## Custom Commands + +U can create ur own custom personal commands using the format below.<br> +The commands are not created on the server but rather edited on clien tand sen +tot server.<br> +Comands must start from "!" in the textbox, but "!" are not required in config. + +```toml +[commands] + +command1 = "This is the mesage that will be posted" +hello = "hello everyone !" +``` diff --git a/screenshot.png b/screenshot.png Binary files differ. diff --git a/src/bhc/mod.rs b/src/bhc/mod.rs diff --git a/src/lechatphp/captcha.rs b/src/lechatphp/captcha.rs @@ -0,0 +1,313 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::hash::Hash; +use base64::{engine::general_purpose, Engine as _}; +use bresenham::Bresenham; +use image::{DynamicImage, GenericImageView, Rgba}; +use lazy_static::lazy_static; + +const B64_PREFIX: &'static str = "R0lGODlhCAAOAIAAAAAAAAAAACH5BAgAAAAALAAAAAAIAA4AgAQCBPz+/AI"; +// list of letters that contains other letters: (h, n) (I, l) (y, u) (Q, O) (B, 3) (E, L) (R, P) +// So our alphabet needs to have "I" before "l" since "l" is contained by "I". +const ALPHABET1: &'static str = "abdcefgh1ijkImnpoqrstyQuvwxzABCDEGJKMNHLORPFSTlUVWXYZ023456789"; +const LETTER_WIDTH: u32 = 8; +const LETTER_HEIGHT: u32 = 14; +const NB_CHARS: u32 = 5; +const LEFT_PADDING: u32 = 5; // left padding for difficulty 1 and 2 +const TOP_PADDING: u32 = 7; // top padding for difficulty 1 and 2 + +lazy_static! { + static ref B64_MAP: HashMap<char, &'static str> = HashMap::from([ + ('0', "VhI8Qkbv3FIvGMeiQ1fPSzSXiSAIFADs="), + ('1', "UhI8QkcvnHlpJSXgNbdnO2FViVQAAOw=="), + ('2', "UhI+hcWruGkMgSmrfvGnrtVDiKBYAOw=="), + ('3', "UhH+hatyBEkTuzVjpldWtHIUiUgAAOw=="), + ('4', "VhI8XkcvqIFiGTmbvdRFl2TzJSCYFADs="), + ('5', "WhG+hG6CYGnwrygedRIw3jGlhRpZGAQA7"), + ('6', "XhI+hcWruGoiJrRcha5fPTS0bQpamUQAAOw=="), + ('7', "ThG+hq5jhQEPz1OeuhJT3CIZiAQA7"), + ('8', "XhI+hcWruGosRLPYwxLnaqXEXQpYmUgAAOw=="), + ('9', "VhI+hcWru2kosTjAjxduydyHiSBoFADs="), + ('A', "VhI8Qkbv3FIvGMeiQ3RlT+X3JSBoFADs="), + ('B', "VhG+hm3EK4GMtLimtntlmeHXISJYFADs="), + ('C', "VhI+hatyLAIwuhSgv1edWt1TYSAIFADs="), + ('D', "WhG+hm3EK4GNTNhvpdZXnvHjISJZAAQA7"), + ('E', "UhG+hG6CYGnyTSYrw0RE6K3niaBQAOw=="), + ('F', "ThG+hy5jhgIpsSugs0oe/CIZiAQA7"), + ('G', "VhI+hatyLXgQuhbMqhfhWl13TSB4FADs="), + ('H', "UhG8RqMr93Gm0xjVhlkl3BIaiUQAAOw=="), + ('I', "UhH+hi4rmXmzhgZmq1JQuboUiUAAAOw=="), + ('J', "UhI8QG6mdlpMRInqhpRI/64TiUQAAOw=="), + ('K', "XhG8RqHruQIQrNXbfirLO2oUaQpamUQAAOw=="), + ('L', "UhG8RmMvKAHwOTVvP1bmpH4UiUgAAOw=="), + ('M', "WhG8RqJ0NI1DTUVgrPZg7vz3ISJZGAQA7"), + ('N', "WhG8RqH3tAFTJUSgXzpvTGlXISJZGAQA7"), + ('O', "VhI+hcWru2kpTxlfxBZBx/23ISJYFADs="), + ('P', "ThG+hG6DI4JJsPuQavhJnD4ZiAQA7"), + ('Q', "WhI+hcWru2kpTxjdhXSxeDjDISJZAAQA7"), + ('R', "WhG+hG6DI4JJs1vNc07jlNHXISJZGAQA7"), + ('S', "UhH+hC4obkHGywVjpw1tbC1XiiBQAOw=="), + ('T', "UhG+hq9cIHpIuwGghTXn2eoXiVQAAOw=="), + ('U', "UhG8RqMr93GlrQivT1UBmioTiiBQAOw=="), + ('V', "VhG8RqMr9gJOLpjon1bsefiHiSIoFADs="), + ('W', "UhG8RqMr93GnLUWBhplzyh4TiaBQAOw=="), + ('X', "UhG8RqMrrQJQNUXvfjG5S72GiWAAAOw=="), + ('Y', "UhG8RqMrrQJQNUXvtPDst/WHiiBQAOw=="), + ('Z', "UhG+hG5jtVIRHIlvpcwBnpnHiWAAAOw=="), + ('a', "ThI+pq+FuYAyNvitnfuB2yoRKAQA7"), + ('b', "WhG8RmMvKAFSONukaPDRji4VgRJZHAQA7"), + ('c', "RhI+pq+FuYHwt1CWBfJn5VAAAOw=="), + ('d', "VhI8YkbD93JtMrmoutpvmeEnNSB4FADs="), + ('e', "RhI+pq+FxnJEyvntXBRWzzxQAOw=="), + ('f', "VhI8QG7f2VJNwoliZpQm7XSXiSBoFADs="), + ('g', "VhI+pm+EO3nnQwBqDpXvRq03aFy4FADs="), + ('h', "VhG8RmMvKAFSONukaPDTjrXlgRJYFADs="), + ('i', "ThI8Qkcrd1kMrzlNTpldKCIZAAQA7"), + ('j', "UhI8XkbANF0uPTlTxVSw/P0kZVAAAOw=="), + ('k', "VhH8RaMrgWJwrQrUSRs7Sll3PSB4FADs="), + ('l', "RhI+haMueAgPw1CZfjvrODxYAOw=="), + ('m', "ThI+piwHh4ItUWkjn1Rl3x4RKAQA7"), + ('n', "UhI+pixHgHnSG0hNljY1jvzHiUgAAOw=="), + ('o', "ThI+pq+FxnJEyPvSgBbXyy4RMAQA7"), + ('p', "VhI+pixHgHnSG0hNljS3vOUlXxygFADs="), + ('q', "VhI+pq+HwHnTS0IBuxpLaiCXVMSIFADs="), + ('r', "ShI+pixHgnInzyXTbw1uzDxoFADs="), + ('s', "RhI+pm+EPHHphUanorLeyvxQAOw=="), + ('t', "UhI95EcrIYlsTVuqueYD3qIQiUgAAOw=="), + ('u', "ThI+pixHt3onSUOggyJvHzoRLAQA7"), + ('v', "ThI+pixHt3nGAVmnt1VtOz4RLAQA7"), + ('w', "UhI+pixHt3onPAXprzJliyzHiUgAAOw=="), + ('x', "ThI+pixH9nJHTPZjsBVTSyoRLAQA7"), + ('y', "VhI+pixHt3onSUOggyJvHXlkPxS0FADs="), + ('z', "PhI+pm+GvXAuzIjkfZXwVADs="), + ]); + static ref RED_COLOR: Rgba<u8> = Rgba::from([204, 2, 4, 255]); + static ref ON_COLOR: Rgba<u8> = Rgba::from([252, 254, 252, 255]); +} + +fn get_letter_img(letter: char) -> DynamicImage { + let b64_suffix = B64_MAP.get(&letter).expect(format!("letter image not found for {}", letter).as_str()); + let img_dec = general_purpose::STANDARD.decode(format!("{}{}", B64_PREFIX, b64_suffix)).unwrap(); + image::load_from_memory(&img_dec).unwrap() +} + +pub fn solve_b64(b64_str: &str) -> Option<String> { + let img_dec = general_purpose::STANDARD.decode(b64_str.strip_prefix("data:image/gif;base64,")?).ok()?; + let img = image::load_from_memory(&img_dec).ok()?; + if img.width() > 60 { + return match solve_difficulty3(&img) { + Ok(answer) => Some(answer), + Err(e) => { + println!("{:?}", e); + None + }, + }; + } + solve_difficulty2(&img) +} + +// This function can solve both difficulty 1 and 2. +fn solve_difficulty2(img: &DynamicImage) -> Option<String> { + let mut answer = String::new(); + for i in 0..NB_CHARS { + let sub_img = img.crop_imm(LEFT_PADDING + ((LETTER_WIDTH +1)*i), TOP_PADDING, LETTER_WIDTH, LETTER_HEIGHT); + for c in ALPHABET1.chars() { + if img_contains_letter(&sub_img, c) { + answer.push(c); + break; + } + } + } + Some(answer) +} + +#[derive(Debug, PartialEq, Eq, Hash)] +struct Letter { + offset: Point, + character: char, +} + +impl Letter { + fn new(offset: Point, character: char) -> Self { + Self { offset, character } + } + + fn offset(&self) -> Point { + self.offset.clone() + } + + fn center(&self) -> Point { + let offset = self.offset(); + Point::new(offset.x + LETTER_WIDTH/2, offset.y + LETTER_HEIGHT/2 - 1) + } +} + +#[derive(Debug)] +struct CaptchaErr(String); + +impl Display for CaptchaErr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for CaptchaErr {} + +// SolveDifficulty3 solve captcha for difficulty 3 +// For each pixel, verify if a match is found. If we do have a match, +// verify that we have some "red" in it. +// +// Red circle is 17x17 (initial point) +fn solve_difficulty3(img: &DynamicImage) -> Result<String, CaptchaErr> { + //img.save(format!("captcha.gif")).unwrap(); + + // Step1: Find all letters with red on the center + let letters_set = find_letters(&img)?; + + // Step2: Find the starting letter + let starting = get_starting_letter(&img, &letters_set) + .ok_or(CaptchaErr("could not find starting letter".to_owned()))?; + + // Step3: Solve path + let answer = solve_path(starting, &letters_set, &img); + Ok(answer) +} + +// Bresenham algorithm will return an iterator of all the pixels that makes a line in between two points. +// From the starting letter, we trace a line to all other letters and count how many red pixels were on the line. +// The next letter will be the one that had the most red pixels. +// Repeat until we find the whole path. +fn solve_path(starting: &Letter, letters_set: &HashSet<Letter>, img: &DynamicImage) -> String { + let mut answer = String::new(); + let mut remaining: HashSet<_> = letters_set.iter().collect(); + let mut letter = remaining.take(&starting).unwrap(); + for _ in 0..NB_CHARS { + answer.push(letter.character); + let mut dest_count = HashMap::<&Letter, usize>::new(); + for dest in remaining.iter() { + let red = Bresenham::new(letter.center().into(), dest.center().into()) + .filter(|(x, y)| is_red(img.get_pixel(*x as u32, *y as u32))) + .count(); + dest_count.insert(dest, red); + } + if let Some((dest_max, _)) = dest_count.into_iter().max_by_key(|e| e.1) { + letter = remaining.take(dest_max).unwrap(); + } + } + answer +} + +fn find_letters(img: &DynamicImage) -> Result<HashSet<Letter>, CaptchaErr> { + const IMAGE_WIDTH: u32 = 150; + const IMAGE_HEIGHT: u32 = 200; + const MIN_PX_FOR_LETTER: usize = 21; + let mut letters_set = HashSet::new(); + for y in 0..IMAGE_HEIGHT-LETTER_HEIGHT { + for x in 0..IMAGE_WIDTH-LETTER_WIDTH { + let letter_img = img.crop_imm(x, y, LETTER_WIDTH, LETTER_HEIGHT); + // We know that minimum amount of pixels on to form a letter is 21 + // We can skip squares that do not have this prerequisite + // Check middle pixels for red, if no red pixels, we can ignore that square + if count_px_on(&letter_img) < MIN_PX_FOR_LETTER || !has_red_in_center_area(&letter_img) { + continue; + } + 'alphabet_loop: for c in ALPHABET1.chars() { + if !img_contains_letter(&letter_img, c) { + continue; + } + // "w" fits in "W". So if we find "W" 1 px bellow, discard "w" + for (a, b, x, y) in vec![('w', 'W', x, y+1), ('k', 'K', x+1, y+1)] { + if c == a { + let one_px_down_img = img.crop_imm(x, y, LETTER_WIDTH, LETTER_HEIGHT); + if img_contains_letter(&one_px_down_img, b) { + continue 'alphabet_loop; + } + } + } + letters_set.insert(Letter::new(Point::new(x, y), c)); + break; + } + } + } + if letters_set.len() != NB_CHARS as usize { + return Err(CaptchaErr(format!("did not find exactly 5 letters {}", letters_set.len()))); + } + Ok(letters_set) +} + +fn get_starting_letter<'a>(img: &DynamicImage, letters_set: &'a HashSet<Letter>) -> Option<&'a Letter> { + const MIN_STARTING_PT_RED_PX: usize = 50; + for letter in letters_set.iter() { + let square = img.crop_imm(letter.offset.x-5, letter.offset.y-3, LETTER_WIDTH+5+6, LETTER_HEIGHT+3+2); + let count_red = count_red_px(&square); + if count_red > MIN_STARTING_PT_RED_PX { + return Some(letter); + } + } + None +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct Point { + x: u32, + y: u32, +} + +impl Point { + fn new(x: u32, y: u32) -> Self { + Self{x, y} + } +} + +impl From<Point> for bresenham::Point { + fn from(value: Point) -> Self { + (value.x as isize, value.y as isize) + } +} + +// give an image and a valid letter image, return either or not the letter is in that image. +fn img_contains_letter(img: &DynamicImage, c: char) -> bool { + let letter_img = get_letter_img(c); + if letter_img.dimensions() != img.dimensions() { + return false; + } + for y in 0..LETTER_HEIGHT { + for x in 0..LETTER_WIDTH { + let good_letter_color = letter_img.get_pixel(x, y); + let letter_img_color = img.get_pixel(x, y); + // If we find an Off pixel where it's supposed to be On, skip that letter + if is_on(good_letter_color) && !is_on(letter_img_color) { + return false; + } + } + } + true +} + +fn is_on(c: Rgba<u8>) -> bool { + c == *ON_COLOR || c == *RED_COLOR +} + +fn is_red(c: Rgba<u8>) -> bool { + c == *RED_COLOR +} + +fn has_red_in_center_area(letter_img: &DynamicImage) -> bool { + letter_img.view(LETTER_WIDTH/2 - 1, LETTER_HEIGHT/2 - 1, 2, 2) + .pixels() + .any(|(_, _, c)| is_red(c)) +} + +// Count pixels that are On (either white or red) +fn count_px_on(img: &DynamicImage) -> usize { + img.pixels() + .filter(|(_, _, c)| is_on(*c)) + .count() +} + +// Count pixels that are red +fn count_red_px(img: &DynamicImage) -> usize { + img.pixels() + .filter(|(_, _, c)| is_red(*c)) + .count() +} +\ No newline at end of file diff --git a/src/lechatphp/mod.rs b/src/lechatphp/mod.rs @@ -0,0 +1,256 @@ +use crate::{ + trim_newline, CAPTCHA_FAILED_SOLVE_ERR, CAPTCHA_USED_ERR, CAPTCHA_WG_ERR, KICKED_ERR, LANG, + NICKNAME_ERR, REG_ERR, SERVER_DOWN_500_ERR, SERVER_DOWN_ERR, SESSION_RGX, UNKNOWN_ERR, +}; +use base64::engine::general_purpose; +use base64::Engine; +use http::StatusCode; +use regex::Regex; +use reqwest::blocking::Client; +use select::document::Document; +use select::predicate::{And, Attr, Name}; +use std::fmt::{Display, Formatter}; +use std::io::Write; +use std::process::{Command, Stdio}; +use std::time::Duration; +use std::{error, fs, io, thread}; + +pub mod captcha; + +#[derive(Debug)] +pub enum LoginErr { + ServerDownErr, + ServerDown500Err, + CaptchaFailedSolveErr, // When auto-solver failed to solve the lechatphp built-in captcha + CaptchaUsedErr, + CaptchaWgErr, + RegErr, + NicknameErr, + KickedErr, + UnknownErr, + Reqwest(reqwest::Error), +} + +impl From<reqwest::Error> for LoginErr { + fn from(value: reqwest::Error) -> Self { + LoginErr::Reqwest(value) + } +} + +impl Display for LoginErr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + LoginErr::ServerDownErr => SERVER_DOWN_ERR.to_owned(), + LoginErr::ServerDown500Err => SERVER_DOWN_500_ERR.to_owned(), + LoginErr::CaptchaFailedSolveErr => CAPTCHA_FAILED_SOLVE_ERR.to_owned(), + LoginErr::CaptchaUsedErr => CAPTCHA_USED_ERR.to_owned(), + LoginErr::CaptchaWgErr => CAPTCHA_WG_ERR.to_owned(), + LoginErr::RegErr => REG_ERR.to_owned(), + LoginErr::NicknameErr => NICKNAME_ERR.to_owned(), + LoginErr::KickedErr => KICKED_ERR.to_owned(), + LoginErr::UnknownErr => UNKNOWN_ERR.to_owned(), + LoginErr::Reqwest(e) => e.to_string(), + }; + write!(f, "{}", s) + } +} + +impl error::Error for LoginErr {} + +pub fn login( + client: &Client, + base_url: &str, + page_php: &str, + username: &str, + password: &str, + color: &str, + manual_captcha: bool, + sxiv: bool, +) -> Result<String, LoginErr> { + // Get login page + let login_url = format!("{}/{}", &base_url, &page_php); + let resp = client.get(&login_url).send()?; + if resp.status() == StatusCode::BAD_GATEWAY { + return Err(LoginErr::ServerDownErr); + } + 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", username.to_owned()), + ("pass", password.to_owned()), + ("colour", color.to_owned()), + ]; + + if let Some(captcha_node) = doc + .find(And(Name("input"), Attr("name", "challenge"))) + .next() + { + let captcha_value = captcha_node.attr("value").unwrap(); + let captcha_img = doc.find(Name("img")).next().unwrap().attr("src").unwrap(); + + let mut captcha_input = String::new(); + if manual_captcha { + // Otherwise, save the captcha on disk and prompt user for answer + // println!("Captcha image source: {}", captcha_img); + // let img_decoded = general_purpose::STANDARD.decode(captcha_img.strip_prefix("data:image/gif;base64,").unwrap()).unwrap(); + // + // Attempt to strip the appropriate prefix based on the MIME type + let base64_str = + if let Some(base64) = captcha_img.strip_prefix("data:image/png;base64,") { + base64 + } else if let Some(base64) = captcha_img.strip_prefix("data:image/gif;base64,") { + base64 + } else { + panic!("Unexpected captcha image format. Expected PNG or GIF."); + }; + + // Decode the base64 string into binary image data + let img_decoded = general_purpose::STANDARD.decode(base64_str).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(); + + if sxiv { + let mut sxiv_process = Command::new("sxiv") + .arg("captcha.gif") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to open image with sxiv"); + + // Prompt the user to enter the CAPTCHA + print!("Please enter the CAPTCHA: "); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut captcha_input).unwrap(); + trim_newline(&mut captcha_input); + + // Close the sxiv window + sxiv_process.kill().expect("Failed to close sxiv"); + + println!("Captcha input: {}", captcha_input); + } else { + 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_input = + captcha::solve_b64(captcha_img).ok_or(LoginErr::CaptchaFailedSolveErr)?; + } + + params.extend(vec![ + ("challenge", captcha_value.to_owned()), + ("captcha", captcha_input.clone()), + ]); + } + + let mut resp = client.post(&login_url).form(&params).send()?; + match resp.status() { + StatusCode::BAD_GATEWAY => return Err(LoginErr::ServerDownErr), + StatusCode::INTERNAL_SERVER_ERROR => return Err(LoginErr::ServerDown500Err), + _ => {} + } + + let mut refresh_header = resp + .headers() + .get("refresh") + .map(|v| v.to_str().unwrap()) + .unwrap_or(""); + while refresh_header != "" { + let rgx = Regex::new(r#"URL=(.+)"#).unwrap(); + let refresh_url = format!( + "{}{}", + base_url, + rgx.captures(&refresh_header) + .unwrap() + .get(1) + .unwrap() + .as_str() + ); + println!("waitroom enabled, wait 10sec"); + thread::sleep(Duration::from_secs(10)); + resp = client.get(refresh_url.clone()).send()?; + refresh_header = resp + .headers() + .get("refresh") + .map(|v| v.to_str().unwrap()) + .unwrap_or(""); + } + + let mut resp = resp.text()?; + if resp.contains(CAPTCHA_USED_ERR) { + return Err(LoginErr::CaptchaUsedErr); + } else if resp.contains(CAPTCHA_WG_ERR) { + return Err(LoginErr::CaptchaWgErr); + } else if resp.contains(REG_ERR) { + return Err(LoginErr::RegErr); + } else if resp.contains(NICKNAME_ERR) { + return Err(LoginErr::NicknameErr); + } else if resp.contains(KICKED_ERR) { + return Err(LoginErr::KickedErr); + } + + let mut doc = Document::from(resp.as_str()); + if let Some(body) = doc.find(Name("body")).next() { + if let Some(body_class) = body.attr("class") { + if body_class == "error" { + if let Some(h2) = doc.find(Name("h2")).next() { + log::error!("{}", h2.text()); + } + return Err(LoginErr::UnknownErr); + } else if body_class == "failednotice" { + log::error!("failed logins: {}", body.text()); + let nc = doc.find(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 = client.post(&login_url).form(&params).send()?.text()?; + doc = Document::from(resp.as_str()); + } + } + } + + let iframe = match doc.find(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(); + Ok(session.to_owned()) +} + +pub fn logout( + client: &Client, + base_url: &str, + page_php: &str, + session: &str, +) -> anyhow::Result<()> { + let full_url = format!("{}/{}", &base_url, &page_php); + let params = [("action", "logout"), ("session", &session), ("lang", LANG)]; + client.post(&full_url).form(&params).send()?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs @@ -0,0 +1,3380 @@ +mod bhc; +mod lechatphp; +mod util; + +use crate::lechatphp::LoginErr; +use anyhow::{anyhow, Context}; +use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; +use clap::Parser; +use clipboard::ClipboardContext; +use clipboard::ClipboardProvider; +use colors_transform::{Color, Rgb}; +use crossbeam_channel::{self, after, 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 lazy_static::lazy_static; +use linkify::LinkFinder; +use log::LevelFilter; +use log4rs::append::file::FileAppender; +use log4rs::encode::pattern::PatternEncoder; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use regex::Regex; +use reqwest::blocking::multipart; +use reqwest::blocking::Client; +use reqwest::redirect::Policy; +use rodio::{source::Source, Decoder, OutputStream}; +use select::document::Document; +use select::predicate::{Attr, Name}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::io::Cursor; +use std::io::{self, Write}; +use std::process::Command; +use std::sync::Mutex; +use std::sync::{Arc, MutexGuard}; +use std::thread; +use std::time::Duration; +use std::time::Instant; +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 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_FAILED_SOLVE_ERR: &str = "Failed solve captcha"; +const CAPTCHA_USED_ERR: &str = "Captcha already used or timed out"; +const UNKNOWN_ERR: &str = "Unknown error"; +const DNMX_URL: &str = "http://hxuzjtocnzvv5g2rtg2bhwkcbupmk7rclb6lly3fo4tvqkk5oyrv3nid.onion"; + +lazy_static! { + static ref META_REFRESH_RGX: Regex = Regex::new(r#"url='([^']+)'"#).unwrap(); + 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(); +} + +fn default_empty_str() -> String { + "".to_string() +} + +#[derive(Debug, Serialize, Deserialize)] +struct Profile { + username: String, + password: String, + #[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(Parser)] +#[command(name = "bhcli")] +#[command(author = "Dasho <o_o@dasho.dev>")] +#[command(version = "0.1.0")] + +struct Opts { + #[arg(long, env = "DKF_API_KEY")] + dkf_api_key: Option<String>, + #[arg(short, long, env = "BHC_USERNAME")] + username: Option<String>, + #[arg(short, long, env = "BHC_PASSWORD")] + password: Option<String>, + #[arg(short, long, env = "BHC_MANUAL_CAPTCHA")] + manual_captcha: bool, + #[arg(short, long, env = "BHC_GUEST_COLOR")] + guest_color: Option<String>, + #[arg(short, long, env = "BHC_REFRESH_RATE", default_value = "5")] + refresh_rate: u64, + #[arg(long, env = "BHC_MAX_LOGIN_RETRY", default_value = "5")] + max_login_retry: isize, + #[arg(long)] + url: Option<String>, + #[arg(long)] + page_php: Option<String>, + #[arg(long)] + datetime_fmt: Option<String>, + #[arg(long)] + members_tag: Option<String>, + #[arg(short, long)] + dan: bool, + #[arg( + short, + long, + env = "BHC_PROXY_URL", + default_value = "socks5h://127.0.0.1:9050" + )] + socks_proxy_url: String, + #[arg(long)] + no_proxy: bool, + #[arg(long, env = "DNMX_USERNAME")] + dnmx_username: Option<String>, + #[arg(long, env = "DNMX_PASSWORD")] + dnmx_password: Option<String>, + #[arg(short = 'c', long, default_value = "default")] + profile: String, + + //Strange + #[arg(long,default_value = "0")] + keepalive_send_to: Option<String>, + + #[arg(long)] + session: Option<String>, + + #[arg(long)] + sxiv: bool, +} + +struct LeChatPHPConfig { + url: String, + datetime_fmt: String, + page_php: String, + keepalive_send_to: 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: "chat.php".to_owned(), + keepalive_send_to: "0".to_owned(), + members_tag: "[M] ".to_owned(), + staffs_tag: "[Staff] ".to_owned(), + } + } +} + +struct BaseClient { + username: String, + password: String, +} + +struct LeChatPHPClient { + base_client: BaseClient, + guest_color: String, + client: Client, + session: Option<String>, + config: LeChatPHPConfig, + last_key_event: Option<KeyCode>, + manual_captcha: bool, + sxiv: bool, + refresh_rate: u64, + max_login_retry: isize, + + is_muted: Arc<Mutex<bool>>, + show_sys: bool, + display_guest_view: bool, + display_member_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 LeChatPHPClient { + fn run_forever(&mut self) { + let max_retry = self.max_login_retry; + let mut attempt = 0; + loop { + match self.login() { + Err(e) => match e { + LoginErr::KickedErr + | LoginErr::RegErr + | LoginErr::NicknameErr + | LoginErr::UnknownErr => { + log::error!("{}", e); + println!("Login error: {}", e); // Print error message + break; + } + LoginErr::CaptchaFailedSolveErr => { + log::error!("{}", e); + println!("Captcha failed to solve: {}", e); // Print error message + continue; + } + LoginErr::CaptchaWgErr | LoginErr::CaptchaUsedErr => {} + LoginErr::ServerDownErr | LoginErr::ServerDown500Err => { + log::error!("{}", e); + println!("Server is down: {}", e); // Print error message + } + LoginErr::Reqwest(err) => { + if err.is_connect() { + log::error!("{}\nIs tor proxy enabled ?", err); + println!("Connection error: {}\nIs tor proxy enabled ?", err); // Print error message + break; + } else if err.is_timeout() { + log::error!("timeout: {}", err); + println!("Timeout error: {}", err); // Print error message + } else { + log::error!("{}", err); + println!("Reqwest error: {}", err); // Print error message + } + } + }, + + Ok(()) => { + attempt = 0; + match self.get_msgs() { + Ok(ExitSignal::NeedLogin) => {} + Ok(ExitSignal::Terminate) => return, + Err(e) => log::error!("{:?}", e), + } + } + } + attempt += 1; + if max_retry > 0 && attempt > max_retry { + break; + } + self.session = None; + let retry_in = 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<()>, + ) -> thread::JoinHandle<()> { + let tx = self.tx.clone(); + let send_to = self.config.keepalive_send_to.clone(); + thread::spawn(move || loop { + let clb = || { + tx.send(PostType::Post("<keepalive>".to_owned(), Some(send_to.clone()))) + .unwrap(); + tx.send(PostType::DeleteLast).unwrap(); + }; + let timeout = after(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) -> _ => clb(), + } + }) + } + + // Thread that POST to chat server + fn start_post_msg_thread( + &self, + exit_rx: crossbeam_channel::Receiver<ExitSignal>, + last_post_tx: crossbeam_channel::Sender<()>, + ) -> 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 session = self.session.clone().unwrap(); + let url = format!("{}?action=post&session={}", &full_url, &session); + thread::spawn(move || loop { + // select! macro fucks all the LSP, therefore the code gymnastic here + let clb = |v: Result<PostType, crossbeam_channel::RecvError>| match v { + Ok(post_type_recv) => post_msg( + &client, + post_type_recv, + &full_url, + session.clone(), + &url, + &last_post_tx, + ), + Err(_) => return, + }; + let rx = rx.lock().unwrap(); + select! { + recv(&exit_rx) -> _ => return, + recv(&rx) -> v => clb(v), + } + }) + } + + // 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<()>, + ) -> thread::JoinHandle<()> { + let client = self.client.clone(); + let messages = Arc::clone(messages); + let users = Arc::clone(users); + let session = self.session.clone().unwrap(); + let username = self.base_client.username.clone(); + let refresh_rate = self.refresh_rate; + 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 members_tag = self.config.members_tag.clone(); + thread::spawn(move || loop { + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); + let mut should_notify = false; + + if let Err(err) = get_msgs( + &client, + &base_url, + &page_php, + &session, + &username, + &users, + &sig, + &messages_updated_tx, + &members_tag, + &datetime_fmt, + &messages, + &mut should_notify, + ) { + log::error!("{}", err); + }; + + let muted = { *is_muted.lock().unwrap() }; + if should_notify && !muted { + if let Err(err) = stream_handle.play_raw(source.convert_samples()) { + log::error!("{}", err); + } + } + + let timeout = after(Duration::from_secs(refresh_rate)); + select! { + recv(&exit_rx) -> _ => return, + recv(&timeout) -> _ => {}, + } + }) + } + + fn get_msgs(&mut self) -> anyhow::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 receive 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: 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_member_view = self.display_member_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(); + + // process() + // Draw UI + terminal.draw(|f| { + draw_terminal_frame(f, &mut app, &messages, &users, &self.base_client.username); + })?; + + // Handle input + match self.handle_input(&events, &mut app, &messages, &users) { + Err(ExitSignal::Terminate) => { + terminate_signal = ExitSignal::Terminate; + sig.lock().unwrap().signal(&terminate_signal); + break; + } + Err(ExitSignal::NeedLogin) => { + terminate_signal = ExitSignal::NeedLogin; + sig.lock().unwrap().signal(&terminate_signal); + 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) -> anyhow::Result<()> { + self.tx.send(post_type)?; + Ok(()) + } + + fn login(&mut self) -> Result<(), LoginErr> { + // If we provided a session, skip login process + if self.session.is_some() { + // println!("Session in params: {:?}", self.session); + return Ok(()); + } + // println!("self.session is not Some"); + // println!("self.sxiv = {:?}", self.sxiv); + self.session = Some(lechatphp::login( + &self.client, + &self.config.url, + &self.config.page_php, + &self.base_client.username, + &self.base_client.password, + &self.guest_color, + self.manual_captcha, + self.sxiv, + )?); + Ok(()) + } + + fn logout(&mut self) -> anyhow::Result<()> { + if let Some(session) = &self.session { + lechatphp::logout( + &self.client, + &self.config.url, + &self.config.page_php, + session, + )?; + self.session = None; + } + 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 = [ + "#ff3366", "#ff6633", "#FFCC33", "#33FF66", "#33FFCC", "#33CCFF", "#3366FF", + "#6633FF", "#CC33FF", "#efefef", + ]; + loop { + let color_rx = color_rx.lock().unwrap(); + let timeout = after(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)); + log::error!("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>>, + ) -> 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: event::Event, + ) -> Result<(), ExitSignal> { + match event { + event::Event::Resize(_cols, _rows) => Ok(()), + event::Event::FocusGained => Ok(()), + event::Event::FocusLost => Ok(()), + event::Event::Paste(_) => Ok(()), + event::Event::Key(key_event) => self.handle_key_event(app, messages, users, key_event), + 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: KeyEvent, + ) -> Result<(), ExitSignal> { + if app.input_mode != InputMode::Normal { + self.last_key_event = None; + } + 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 | InputMode::EditingErr => { + self.handle_editing_mode_key_event(app, key_event, users) + } + } + } + + fn handle_long_message_mode_key_event( + &mut self, + app: &mut App, + key_event: KeyEvent, + messages: &Arc<Mutex<Vec<Message>>>, + ) -> Result<(), ExitSignal> { + match key_event { + KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + .. + } + | KeyEvent { + code: KeyCode::Esc, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_long_message_mode_key_event_esc(app), + KeyEvent { + code: KeyCode::Char('d'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_long_message_mode_key_event_ctrl_d(app, messages), + _ => {} + } + Ok(()) + } + + fn handle_normal_mode_key_event( + &mut self, + app: &mut App, + key_event: KeyEvent, + messages: &Arc<Mutex<Vec<Message>>>, + ) -> Result<(), ExitSignal> { + match key_event { + KeyEvent { + code: KeyCode::Char('/'), + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_slash(app), + KeyEvent { + code: KeyCode::Char('j'), + modifiers: KeyModifiers::NONE, + .. + } + | KeyEvent { + code: KeyCode::Down, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_down(app), + KeyEvent { + code: KeyCode::Char('J'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_j(app,5), + KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::NONE, + .. + } + | KeyEvent { + code: KeyCode::Up, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_up(app), + KeyEvent { + code: KeyCode::Char('K'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_k(app,5), + KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_enter(app, messages), + KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_backspace(app, messages), + KeyEvent { + code: KeyCode::Char('y'), + modifiers: KeyModifiers::NONE, + .. + } + | KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_normal_mode_key_event_yank(app), + KeyEvent { + code: KeyCode::Char('Y'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_yank_link(app), + + //Strange + KeyEvent { + code: KeyCode::Char('D'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_download_link(app), + + //Strange + KeyEvent { + code: KeyCode::Char('d'), + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_download_and_view(app), + + // KeyEvent { + // code: KeyCode::Char('d'), + // modifiers: KeyModifiers::NONE, + // .. + // } => self.handle_normal_mode_key_event_debug(app), + // KeyEvent { + // code: KeyCode::Char('D'), + // modifiers: KeyModifiers::SHIFT, + // .. + // } => self.handle_normal_mode_key_event_debug2(app), + KeyEvent { + code: KeyCode::Char('m'), + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_toggle_mute(), + KeyEvent { + code: KeyCode::Char('S'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_toggle_sys(), + KeyEvent { + code: KeyCode::Char('M'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_toggle_member_view(), + KeyEvent { + code: KeyCode::Char('G'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_toggle_guest_view(), + KeyEvent { + code: KeyCode::Char('H'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_toggle_hidden(), + KeyEvent { + code: KeyCode::Char('i'), + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_input_mode(app), + KeyEvent { + code: KeyCode::Char('Q'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_logout()?, + KeyEvent { + code: KeyCode::Char('q'), + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_exit()?, + KeyEvent { + code: KeyCode::Char('t'), + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_tag(app), + KeyEvent { + code: KeyCode::Char('p'), + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_pm(app), + KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_normal_mode_key_event_kick(app), + KeyEvent { + code: KeyCode::Char('w'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_normal_mode_key_event_warn(app), + KeyEvent { + code: KeyCode::Char('T'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_translate(app, messages), + KeyEvent { + code: KeyCode::Char('u'), + modifiers: KeyModifiers::CONTROL, + .. + } + | KeyEvent { + code: KeyCode::PageUp, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_page_up(app), + KeyEvent { + code: KeyCode::Char('d'), + modifiers: KeyModifiers::CONTROL, + .. + } + | KeyEvent { + code: KeyCode::PageDown, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_page_down(app), + KeyEvent { + code: KeyCode::Esc, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_esc(app), + KeyEvent { + code: KeyCode::Char('u'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_shift_u(app), + KeyEvent { + code: KeyCode::Char('g'), + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_g(app), + _ => {} + } + self.last_key_event = Some(key_event.code); + Ok(()) + } + + fn handle_editing_mode_key_event( + &mut self, + app: &mut App, + key_event: KeyEvent, + users: &Arc<Mutex<Users>>, + ) -> Result<(), ExitSignal> { + app.input_mode = InputMode::Editing; + match key_event { + KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_editing_mode_key_event_enter(app)?, + KeyEvent { + code: KeyCode::Tab, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_editing_mode_key_event_tab(app, users), + KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_editing_mode_key_event_ctrl_c(app), + KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_editing_mode_key_event_ctrl_a(app), + KeyEvent { + code: KeyCode::Char('e'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_editing_mode_key_event_ctrl_e(app), + KeyEvent { + code: KeyCode::Char('f'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_editing_mode_key_event_ctrl_f(app), + KeyEvent { + code: KeyCode::Char('b'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_editing_mode_key_event_ctrl_b(app), + KeyEvent { + code: KeyCode::Char('v'), + modifiers: KeyModifiers::CONTROL, + .. + } => self.handle_editing_mode_key_event_ctrl_v(app), + KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_editing_mode_key_event_left(app), + KeyEvent { + code: KeyCode::Right, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_editing_mode_key_event_right(app), + KeyEvent { + code: KeyCode::Down, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_editing_mode_key_event_down(app), + KeyEvent { + code: KeyCode::Char(c), + modifiers: KeyModifiers::NONE, + .. + } + | KeyEvent { + code: KeyCode::Char(c), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_editing_mode_key_event_shift_c(app, c), + KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_editing_mode_key_event_backspace(app), + KeyEvent { + code: KeyCode::Delete, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_editing_mode_key_event_delete(app), + KeyEvent { + code: KeyCode::Esc, + modifiers: KeyModifiers::NONE, + .. + } => self.handle_editing_mode_key_event_esc(app), + _ => {} + } + Ok(()) + } + + fn handle_long_message_mode_key_event_esc(&mut self, app: &mut App) { + app.long_message = None; + app.input_mode = InputMode::Normal; + } + + fn handle_long_message_mode_key_event_ctrl_d( + &mut self, + app: &mut App, + messages: &Arc<Mutex<Vec<Message>>>, + ) { + 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 == item.text) + { + messages[pos].hide = !messages[pos].hide; + } + app.long_message = None; + app.input_mode = InputMode::Normal; + } + } + } + + fn handle_normal_mode_key_event_up(&mut self, app: &mut App) { + app.items.previous() + } + + fn handle_normal_mode_key_event_down(&mut self, app: &mut App) { + app.items.next() + } + + fn handle_normal_mode_key_event_j(&mut self, app: &mut App, lines: usize) { + for _ in 0..lines { + app.items.next(); // Move to the next item + } + } + + fn handle_normal_mode_key_event_k(&mut self, app: &mut App, lines: usize) { + for _ in 0..lines { + app.items.previous(); // Move to the next item + } + } + + fn handle_normal_mode_key_event_slash(&mut self, app: &mut App) { + app.items.unselect(); + app.input = "/".to_owned(); + app.input_idx = app.input.width(); + app.input_mode = InputMode::Editing; + } + + fn handle_normal_mode_key_event_enter( + &mut self, + app: &mut App, + messages: &Arc<Mutex<Vec<Message>>>, + ) { + if let Some(idx) = app.items.state.selected() { + if let Some(item) = app.items.items.get(idx) { + // If we have a filter, <enter> will "jump" to the message + if !app.filter.is_empty() { + let idx = messages + .lock() + .unwrap() + .iter() + .enumerate() + .find(|(_, e)| e.date == item.date) + .map(|(i, _)| i); + app.clear_filter(); + app.items.state.select(idx); + return; + } + app.long_message = Some(item.clone()); + app.input_mode = InputMode::LongMessage; + } + } + } + + fn handle_normal_mode_key_event_backspace( + &mut self, + app: &mut App, + messages: &Arc<Mutex<Vec<Message>>>, + ) { + 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 == item.text) + { + if item.deleted { + messages.remove(pos); + } else { + messages[pos].hide = !messages[pos].hide; + } + } + } + } + } + + fn handle_normal_mode_key_event_yank(&mut self, app: &mut App) { + 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(); + } + } + } + } + + fn handle_normal_mode_key_event_yank_link(&mut self, app: &mut App) { + 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(); + } + } + } + } + } + + //Strange + fn handle_normal_mode_key_event_download_link(&mut self, app: &mut App) { + 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 url = format!("{}{}", self.config.url, upload_link); + let _ = Command::new("curl") + .args([ + "--socks5", + "localhost:9050", + "--socks5-hostname", + "localhost:9050", + &url, + ]) + .arg("-o") + .arg("download.img") + .output() + .expect("Failed to execute curl command"); + } 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.first() { + let url = link.as_str(); + let _ = Command::new("curl") + .args([ + "--socks5", + "localhost:9050", + "--socks5-hostname", + "localhost:9050", + url, + ]) + .arg("-o") + .arg("download.img") + .output() + .expect("Failed to execute curl command"); + } + } + } + } + } + + //strageEdit + fn handle_normal_mode_key_event_download_and_view(&mut self, app: &mut App) { + 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 url = format!("{}{}", self.config.url, upload_link); + let _ = Command::new("curl") + .args([ + "--socks5", + "localhost:9050", + "--socks5-hostname", + "localhost:9050", + &url, + ]) + .arg("-o") + .arg("download.img") + .output() + .expect("Failed to execute curl command"); + + let _ = Command::new("xdg-open") + .arg("./download.img") + .output() + .expect("Failed to execute sxiv command"); + } 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.first() { + let url = link.as_str(); + let _ = Command::new("curl") + .args([ + "--socks5", + "localhost:9050", + "--socks5-hostname", + "localhost:9050", + url, + ]) + .arg("-o") + .arg("download.img") + .output() + .expect("Failed to execute curl command"); + + let _ = Command::new("sxiv") + .arg("./download.img") + .output() + .expect("Failed to execute sxiv command"); + } + } + } + } + } + + fn handle_normal_mode_key_event_toggle_mute(&mut self) { + let mut is_muted = self.is_muted.lock().unwrap(); + *is_muted = !*is_muted; + } + + fn handle_normal_mode_key_event_toggle_sys(&mut self) { + self.show_sys = !self.show_sys; + } + + fn handle_normal_mode_key_event_toggle_guest_view(&mut self) { + self.display_guest_view = !self.display_guest_view; + } + + fn handle_normal_mode_key_event_toggle_member_view(&mut self) { + self.display_member_view = !self.display_member_view; + } + + fn handle_normal_mode_key_event_g(&mut self, app: &mut App) { + // Handle "gg" key combination + if self.last_key_event == Some(KeyCode::Char('g')) { + app.items.select_top(); + self.last_key_event = None; + } + } + + fn handle_normal_mode_key_event_toggle_hidden(&mut self) { + self.display_hidden_msgs = !self.display_hidden_msgs; + } + + fn handle_normal_mode_key_event_input_mode(&mut self, app: &mut App) { + app.input_mode = InputMode::Editing; + app.items.unselect(); + } + + fn handle_normal_mode_key_event_logout(&mut self) -> Result<(), ExitSignal> { + self.logout().unwrap(); + return Err(ExitSignal::Terminate); + } + + fn handle_normal_mode_key_event_exit(&mut self) -> Result<(), ExitSignal> { + return Err(ExitSignal::Terminate); + } + + fn handle_normal_mode_key_event_tag(&mut self, app: &mut App) { + if let Some(idx) = app.items.state.selected() { + let text = &app.items.items.get(idx).unwrap().text; + if let Some(username) = + get_username(&self.base_client.username, &text, &self.config.members_tag) + { + if text.text().starts_with(&app.members_tag) { + app.input = format!("/m @{} ", username); + } else { + app.input = format!("@{} ", username); + } + app.input_idx = app.input.width(); + app.input_mode = InputMode::Editing; + app.items.unselect(); + } + } + } + + fn handle_normal_mode_key_event_pm(&mut self, app: &mut App) { + 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(); + } + } + } + + fn handle_normal_mode_key_event_kick(&mut self, app: &mut App) { + 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(); + } + } + } + + //Strange + fn handle_normal_mode_key_event_translate( + &mut self, + app: &mut App, + messages: &Arc<Mutex<Vec<Message>>>, + ) { + log::error!("translate running"); + if let Some(idx) = app.items.state.selected() { + log::error!("1353"); + let mut message_lock = messages.lock().unwrap(); + if let Some(message) = message_lock.get_mut(idx) { + log::error!("1356"); + let original_text = &mut message.text; + let output = Command::new("trans") + .arg("-b") + .arg(&original_text.text()) + .output() + .expect("Failed to execute translation command"); + + if output.status.success() { + if let Ok(new_text) = String::from_utf8(output.stdout) { + *original_text = StyledText::Text(new_text.trim().to_owned()); + log::error!("Translation successful: {}", new_text); + } else { + log::error!("Failed to decode translation output as UTF-8"); + } + } else { + log::error!("Translation command failed with error: {:?}", output.status); + } + } + } + } + + //Strange + fn handle_normal_mode_key_event_warn(&mut self, app: &mut App) { + 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!("!warn @{} ", username); + app.input_idx = app.input.width(); + app.input_mode = InputMode::Editing; + app.items.unselect(); + } + } + } + fn handle_normal_mode_key_event_page_up(&mut self, app: &mut App) { + if let Some(idx) = app.items.state.selected() { + app.items.state.select(idx.checked_sub(10).or(Some(0))); + } else { + app.items.next(); + } + } + + fn handle_normal_mode_key_event_page_down(&mut self, app: &mut App) { + 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(); + } + } + + fn handle_normal_mode_key_event_esc(&mut self, app: &mut App) { + app.items.unselect(); + } + + fn handle_normal_mode_key_event_shift_u(&mut self, app: &mut App) { + app.items.state.select(Some(0)); + } + + fn handle_editing_mode_key_event_enter(&mut self, app: &mut App) -> Result<(), ExitSignal> { + if FIND_RGX.is_match(&app.input) { + return Ok(()); + } + + let input: String = app.input.drain(..).collect(); + app.input_idx = 0; + + // Iterate over commands and execute associated actions + for (command, action) in &app.commands.commands { + // log::error!("command :{} action :{}", command, action); + let expected_input = format!("!{}", command); + if input == expected_input { + // Execute the action by posting a message + self.post_msg(PostType::Post(action.clone(), None)).unwrap(); + // Return Ok(()) if the action is executed successfully + return Ok(()); + } + } + + 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 if input.starts_with("!warn") { + // Strange + let msg: String = input + .find('@') + .map(|index| input[index..].to_string()) + .unwrap_or_else(String::new); + + let end_msg = format!( + "This is your warning - {}, will be kicked next !rules", + msg + ); + // log::error!("The Strange end_msg is :{}", end_msg); + self.post_msg(PostType::Post(end_msg, None)).unwrap(); + } else { + if input.starts_with("/") && !input.starts_with("/me ") { + app.input_idx = input.len(); + app.input = input; + app.input_mode = InputMode::EditingErr; + } else { + // Send normal message + self.post_msg(PostType::Post(input, None)).unwrap(); + } + } + Ok(()) + } + + fn handle_editing_mode_key_event_tab(&mut self, app: &mut App, users: &Arc<Mutex<Users>>) { + 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; + } + } + } + } + } + + fn handle_editing_mode_key_event_ctrl_c(&mut self, app: &mut App) { + app.clear_filter(); + app.input = "".to_owned(); + app.input_idx = 0; + app.input_mode = InputMode::Normal; + } + + fn handle_editing_mode_key_event_ctrl_a(&mut self, app: &mut App) { + app.input_idx = 0; + } + + fn handle_editing_mode_key_event_ctrl_e(&mut self, app: &mut App) { + app.input_idx = app.input.width(); + } + + fn handle_editing_mode_key_event_ctrl_f(&mut self, app: &mut App) { + 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(); + } + } + + fn handle_editing_mode_key_event_ctrl_b(&mut self, app: &mut App) { + 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; + } + } + } + + fn handle_editing_mode_key_event_ctrl_v(&mut self, app: &mut App) { + 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(); + } + } + + fn handle_editing_mode_key_event_left(&mut self, app: &mut App) { + if app.input_idx > 0 { + app.input_idx -= 1; + } + } + + fn handle_editing_mode_key_event_right(&mut self, app: &mut App) { + if app.input_idx < app.input.width() { + app.input_idx += 1; + } + } + + fn handle_editing_mode_key_event_down(&mut self, app: &mut App) { + app.input_mode = InputMode::Normal; + app.items.next(); + } + + fn handle_editing_mode_key_event_shift_c(&mut self, app: &mut App, c: char) { + let byte_position = byte_pos(&app.input, app.input_idx).unwrap(); + app.input.insert(byte_position, c); + + app.input_idx += 1; + app.update_filter(); + } + + fn handle_editing_mode_key_event_backspace(&mut self, app: &mut App) { + if app.input_idx > 0 { + app.input_idx -= 1; + app.input = remove_at(&app.input, app.input_idx); + app.update_filter(); + } + } + + fn handle_editing_mode_key_event_delete(&mut self, app: &mut App) { + 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(); + } + + fn handle_editing_mode_key_event_esc(&mut self, app: &mut App) { + app.input_mode = InputMode::Normal; + } + + fn handle_mouse_event( + &mut self, + app: &mut App, + mouse_event: MouseEvent, + ) -> 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 prefix_lower = prefix.to_lowercase(); + let filtered = all_users + .iter() + .find(|(_, name)| name.to_lowercase().starts_with(&prefix_lower)); + Some(filtered?.1.to_owned()) +} + +fn set_profile_base_info( + client: &Client, + full_url: &str, + params: &mut Vec<(&str, String)>, +) -> anyhow::Result<()> { + params.extend(vec![("action", "profile".to_owned())]); + let profile_resp = client.post(full_url).form(&params).send()?; + let profile_resp_txt = profile_resp.text().unwrap(); + let doc = Document::from(profile_resp_txt.as_str()); + let bold = doc.find(Attr("id", "bold")).next().unwrap(); + let italic = doc.find(Attr("id", "italic")).next().unwrap(); + let small = doc.find(Attr("id", "small")).next().unwrap(); + if bold.attr("checked").is_some() { + params.push(("bold", "on".to_owned())); + } + if italic.attr("checked").is_some() { + params.push(("italic", "on".to_owned())); + } + if small.attr("checked").is_some() { + params.push(("small", "on".to_owned())); + } + let font_select = doc.find(Attr("name", "font")).next().unwrap(); + let font = font_select.find(Name("option")).find_map(|el| { + if el.attr("selected").is_some() { + return Some(el.attr("value").unwrap()); + } + None + }); + params.push(("font", font.unwrap_or("").to_owned())); + Ok(()) +} + +enum RetryErr { + Retry, + Exit, +} + +fn retry_fn<F>(mut clb: F) +where + F: FnMut() -> anyhow::Result<RetryErr>, +{ + loop { + match clb() { + Ok(RetryErr::Retry) => continue, + Ok(RetryErr::Exit) => return, + Err(err) => { + log::error!("{}", err); + continue; + } + } + } +} + +fn post_msg( + client: &Client, + post_type_recv: PostType, + full_url: &str, + session: String, + url: &str, + last_post_tx: &crossbeam_channel::Sender<()>, +) { + let mut should_reset_keepalive_timer = false; + retry_fn(|| -> anyhow::Result<RetryErr> { + let post_type = post_type_recv.clone(); + let resp_text = client.get(url).send()?.text()?; + let doc = Document::from(resp_text.as_str()); + let nc = doc + .find(Attr("name", "nc")) + .next() + .context("nc not found")?; + let nc_value = nc.attr("value").context("nc value not found")?.to_owned(); + let postid = doc + .find(Attr("name", "postid")) + .next() + .context("failed to get postid")?; + let postid_value = postid + .attr("value") + .context("failed to get postid value")? + .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(e) = delete_message(&client, full_url, &mut params, date, text) { + log::error!("failed to delete message: {:?}", e); + return Ok(RetryErr::Retry); + } + return Ok(RetryErr::Exit); + } + + let mut req = client.post(full_url); + let mut form: Option<multipart::Form> = None; + + match post_type { + PostType::Post(msg, send_to) => { + should_reset_keepalive_timer = true; + params.extend(vec![ + ("action", "post".to_owned()), + ("postid", postid_value.to_owned()), + ("multi", "on".to_owned()), + ("message", msg), + ("sendto", send_to.unwrap_or(SEND_TO_ALL.to_owned())), + ]); + } + PostType::NewNickname(new_nickname) => { + set_profile_base_info(client, full_url, &mut params)?; + params.extend(vec![ + ("do", "save".to_owned()), + ("timestamps", "on".to_owned()), + ("newnickname", new_nickname), + ]); + } + PostType::NewColor(new_color) => { + set_profile_base_info(client, full_url, &mut params)?; + params.extend(vec![ + ("do", "save".to_owned()), + ("timestamps", "on".to_owned()), + ("colour", new_color), + ]); + } + PostType::Ignore(username) => { + set_profile_base_info(client, full_url, &mut params)?; + params.extend(vec![ + ("do", "save".to_owned()), + ("timestamps", "on".to_owned()), + ("ignore", username), + ]); + } + PostType::Unignore(username) => { + set_profile_base_info(client, full_url, &mut params)?; + params.extend(vec![ + ("do", "save".to_owned()), + ("timestamps", "on".to_owned()), + ("unignore", username), + ]); + } + PostType::Profile(new_color, new_nickname) => { + set_profile_base_info(client, full_url, &mut params)?; + 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( + match 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) + { + Ok(f) => f, + Err(e) => { + log::error!("{:?}", e); + return Ok(RetryErr::Exit); + } + }, + ); + } + PostType::Clean(_, _) => {} + } + + if let Some(form_content) = form { + req = req.multipart(form_content); + } else { + req = req.form(&params); + } + if let Err(err) = req.send() { + log::error!("{:?}", err.to_string()); + if err.is_timeout() { + return Ok(RetryErr::Retry); + } + } + return Ok(RetryErr::Exit); + }); + if should_reset_keepalive_timer { + last_post_tx.send(()).unwrap(); + } +} + +fn parse_date(date: &str, datetime_fmt: &str) -> NaiveDateTime { + let now = Utc::now(); + let date_fmt = format!("%Y-{}", datetime_fmt); + NaiveDateTime::parse_from_str( + format!("{}-{}", now.year(), date).as_str(), + date_fmt.as_str(), + ) + .unwrap() +} + +fn get_msgs( + client: &Client, + base_url: &str, + page_php: &str, + session: &str, + username: &str, + users: &Arc<Mutex<Users>>, + sig: &Arc<Mutex<Sig>>, + messages_updated_tx: &crossbeam_channel::Sender<()>, + members_tag: &str, + datetime_fmt: &str, + messages: &Arc<Mutex<Vec<Message>>>, + should_notify: &mut bool, +) -> anyhow::Result<()> { + let url = format!( + "{}/{}?action=view&session={}&lang={}", + base_url, page_php, session, LANG + ); + let resp_text = client.get(url).send()?.text()?; + let resp_text = resp_text.replace("<br>", "\n"); + let doc = Document::from(resp_text.as_str()); + let new_messages = match extract_messages(&doc) { + Ok(messages) => messages, + Err(_) => { + // Failed to get messages, probably need re-login + sig.lock().unwrap().signal(&ExitSignal::NeedLogin); + return Ok(()); + } + }; + { + let messages = messages.lock().unwrap(); + process_new_messages( + &new_messages, + &messages, + datetime_fmt, + members_tag, + username, + should_notify, + ); + // Build messages vector. Tag deleted messages. + update_messages(new_messages, messages, datetime_fmt); + // 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(()).unwrap(); + } + { + let mut users = users.lock().unwrap(); + *users = extract_users(&doc); + } + Ok(()) +} + +fn process_new_messages( + new_messages: &Vec<Message>, + messages: &MutexGuard<Vec<Message>>, + datetime_fmt: &str, + members_tag: &str, + username: &str, + should_notify: &mut bool, +) { + if let Some(last_known_msg) = messages.first() { + let last_known_msg_parsed_dt = parse_date(&last_known_msg.date, datetime_fmt); + let filtered = new_messages.iter().filter(|new_msg| { + last_known_msg_parsed_dt <= parse_date(&new_msg.date, datetime_fmt) + && !(new_msg.date == last_known_msg.date && last_known_msg.text == new_msg.text) + }); + for new_msg in filtered { + if let Some((_, to_opt, msg)) = get_message(&new_msg.text, members_tag) { + // Process new messages + + // 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; + } + } + } + } + } +} + +fn update_messages( + new_messages: Vec<Message>, + mut messages: MutexGuard<Vec<Message>>, + datetime_fmt: &str, +) { + let mut old_msg_ptr = 0; + for new_msg in new_messages.into_iter() { + loop { + if let Some(old_msg) = messages.get_mut(old_msg_ptr) { + let new_parsed_dt = parse_date(&new_msg.date, datetime_fmt); + let parsed_dt = parse_date(&old_msg.date, datetime_fmt); + if new_parsed_dt < parsed_dt { + old_msg.deleted = true; + old_msg_ptr += 1; + continue; + } + if new_parsed_dt == parsed_dt { + if old_msg.text != new_msg.text { + let mut found = false; + let mut x = 0; + loop { + x += 1; + if let Some(old_msg) = messages.get(old_msg_ptr + x) { + let parsed_dt = parse_date(&old_msg.date, datetime_fmt); + if new_parsed_dt == parsed_dt { + if old_msg.text == new_msg.text { + found = true; + break; + } + continue; + } + } + break; + } + if !found { + messages.insert(old_msg_ptr, new_msg); + old_msg_ptr += 1; + } + } + old_msg_ptr += 1; + break; + } + } + messages.insert(old_msg_ptr, new_msg); + old_msg_ptr += 1; + break; + } + } + messages.truncate(1000); +} + +fn delete_message( + client: &Client, + full_url: &str, + params: &mut Vec<(&str, String)>, + date: String, + text: String, +) -> anyhow::Result<()> { + params.extend(vec![ + ("action", "admin".to_owned()), + ("do", "clean".to_owned()), + ("what", "choose".to_owned()), + ]); + let clean_resp_txt = client.post(full_url).form(&params).send()?.text()?; + let doc = Document::from(clean_resp_txt.as_str()); + let nc = doc + .find(Attr("name", "nc")) + .next() + .context("nc not found")?; + let nc_value = nc.attr("value").context("nc value not found")?.to_owned(); + let msgs = extract_messages(&doc)?; + if let Some(msg) = msgs + .iter() + .find(|m| m.date == date && m.text.text() == text) + { + let msg_id = msg.id.context("msg id not found")?; + params.extend(vec![ + ("nc", nc_value.to_owned()), + ("what", "selected".to_owned()), + ("mid[]", format!("{}", msg_id)), + ]); + client.post(full_url).form(&params).send()?; + } + Ok(()) +} + +impl ChatClient { + fn new(params: Params) -> Self { + // println!("session[2026] : {:?}",params.session); + let mut c = new_default_le_chat_php_client(params.clone()); + c.config.url = params.url.unwrap_or( + "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion/index.php" + .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 = params.keepalive_send_to.unwrap_or("0".to_owned()); + // c.session = params.session; + Self { + le_chat_php_client: c, + } + } + + 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(); + let session = params.session.clone(); + // println!("session[2050] : {:?}",params.session); + LeChatPHPClient { + base_client: BaseClient { + username: params.username, + password: params.password, + }, + max_login_retry: params.max_login_retry, + guest_color: params.guest_color, + // session: params.session, + session, + last_key_event: None, + client: params.client, + manual_captcha: params.manual_captcha, + sxiv: params.sxiv, + 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_member_view: false, + display_hidden_msgs: false, + tx, + rx: Arc::new(Mutex::new(rx)), + color_tx, + color_rx: Arc::new(Mutex::new(color_rx)), + } +} + +struct ChatClient { + le_chat_php_client: LeChatPHPClient, +} + +#[derive(Debug, Clone)] +struct Params { + url: Option<String>, + page_php: Option<String>, + datetime_fmt: Option<String>, + members_tag: Option<String>, + username: String, + password: String, + guest_color: String, + client: Client, + manual_captcha: bool, + sxiv: bool, + refresh_rate: u64, + max_login_retry: isize, + keepalive_send_to: Option<String>, + session: Option<String>, +} + +#[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, no_proxy: bool) -> Client { + let ua = "Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0"; + let mut builder = reqwest::blocking::ClientBuilder::new() + .redirect(Policy::none()) + .cookie_store(true) + .user_agent(ua); + if !no_proxy { + let proxy = reqwest::Proxy::all(socks_proxy_url).unwrap(); + builder = builder.proxy(proxy); + } + builder.build().unwrap() +} + +fn ask_username(username: Option<String>) -> String { + username.unwrap_or_else(|| { + 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 { + password.unwrap_or_else(|| rpassword::prompt_password("Password: ").unwrap()) +} + +#[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 = 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(&params) + .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(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(&params).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.find(Name("table")).nth(7) { + table.find(Name("tr")).skip(1).for_each(|n| { + if let Some(td) = n.find(Name("td")).nth(2) { + if td.find(Name("b")).nth(0).is_some() { + nb_mails += 1; + } + } + }); + } + if nb_mails > 0 { + log::error!("{} new mails", nb_mails); + stream_handle.play_raw(source.convert_samples()).unwrap(); + } + } + thread::sleep(Duration::from_secs(60)); + }); +} + +//Strange +#[derive(Debug, Deserialize)] +struct Commands { + commands: HashMap<String, String>, +} + +impl Default for Commands { + fn default() -> Self { + Commands { + commands: HashMap::new(), // Initialize commands with empty HashMap + } + } +} + +// Strange +// Function to read the configuration file and parse it +fn read_commands_file(file_path: &str) -> Result<Commands, Box<dyn std::error::Error>> { + // Read the contents of the file + let commands_content = std::fs::read_to_string(file_path)?; + // log::error!("Read file contents: {}", commands_content); + // Deserialize the contents into a Commands struct + let commands: Commands = toml::from_str(&commands_content)?; + // log::error!( + // "Deserialized file contents into Commands struct: {:?}", + // commands + // ); + + Ok(commands) +} + +fn main() -> anyhow::Result<()> { + let mut opts: Opts = Opts::parse(); + // println!("Parsed Session: {:?}", opts.session); + + + // Configs file + if let Ok(config_path) = confy::get_configuration_file_path("bhcli", None) { + println!("Config path: {:?}", config_path); + } + if let Ok(cfg) = confy::load::<MyConfig>("bhcli", None) { + 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 logfile = FileAppender::builder() + .encoder(Box::new(PatternEncoder::new("{d} {l} {t} - {m}{n}"))) + .build("bhcli.log")?; + + let config = log4rs::config::Config::builder() + .appender(log4rs::config::Appender::builder().build("logfile", Box::new(logfile))) + .build( + log4rs::config::Root::builder() + .appender("logfile") + .build(LevelFilter::Error), + )?; + + log4rs::init_config(config)?; + + let client = get_tor_client(&opts.socks_proxy_url, opts.no_proxy); + + // 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.clone(), + manual_captcha: opts.manual_captcha, + sxiv: opts.sxiv, + refresh_rate: opts.refresh_rate, + max_login_retry: opts.max_login_retry, + keepalive_send_to: opts.keepalive_send_to, + session: opts.session.clone(), + }; + // println!("Session[2378]: {:?}", opts.session); + + + ChatClient::new(params).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 = children.get(0)?.text(); + match children.get(children.len() - 1)? { + StyledText::Styled(_, children) => { + let from = match children.get(children.len() - 1)? { + StyledText::Text(t) => t.to_owned(), + _ => return None, + }; + return Some((from, None, msg)); + } + StyledText::Text(t) => { + if t == &members_tag { + let from = match children.get(children.len() - 2)? { + StyledText::Styled(_, children) => { + match children.get(children.len() - 1)? { + 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)? { + StyledText::Styled(_, children) => { + match children.get(children.len() - 1)? { + StyledText::Text(t) => t.to_owned(), + _ => return None, + } + } + _ => return None, + }; + let to = match children.get(2)? { + StyledText::Styled(_, children) => { + match children.get(children.len() - 1)? { + StyledText::Text(t) => Some(t.to_owned()), + _ => return None, + } + } + _ => return None, + }; + return Some((from, to, msg)); + } + } + _ => return None, + } + } + 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 +} + +impl Message { + fn new( + id: Option<usize>, + typ: MessageType, + date: String, + upload_link: Option<String>, + text: StyledText, + ) -> Self { + Self { + id, + typ, + date, + upload_link, + text, + deleted: false, + hide: false, + } + } +} + +#[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); + 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; + match 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); + } + } + } + Some("font") => { + if let Some(color_str) = e.attr("color") { + color = parse_color(color_str); + } + } + Some("a") => { + color = tuiColor::White; + if let (Some("attachement"), Some(href)) = (e.attr("class"), e.attr("href")) { + upload_link = Some(href.to_owned()); + } + } + Some("style") => { + return (StyledText::None, None); + } + _ => {} + } + let mut children_texts: Vec<StyledText> = vec![]; + let children = e.children(); + for child in children { + let (st, ul) = process_node(child, color); + if ul.is_some() { + 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 is_guest(&self, name: &str) -> bool { + // self.guests.iter().find(|(_, username)| username == name).is_some() + // } +} + +fn extract_users(doc: &Document) -> Users { + let mut users = Users::default(); + + if let Some(chatters) = doc.find(Attr("id", "chatters")).next() { + if let Some(tr) = chatters.find(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.find(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 => users.admin.push((tui_color, username)), + 2 => users.staff.push((tui_color, username)), + 3 => users.members.push((tui_color, username)), + 4 => users.guests.push((tui_color, username)), + _ => {} + } + } + } + } + } + } + } + } + } + users +} + +fn remove_suffix<'a>(s: &'a str, suffix: &str) -> &'a str { + s.strip_suffix(suffix).unwrap_or(s) +} + +fn remove_prefix<'a>(s: &'a str, prefix: &str) -> &'a str { + s.strip_prefix(prefix).unwrap_or(s) +} + +fn extract_messages(doc: &Document) -> anyhow::Result<Vec<Message>> { + let msgs = doc + .find(Attr("id", "messages")) + .next() + .ok_or(anyhow!("failed to get messages div"))? + .find(Attr("class", "msg")) + .filter_map(|tag| { + let mut id: Option<usize> = None; + if let Some(checkbox) = tag.find(Name("input")).next() { + let id_value: usize = checkbox.attr("value").unwrap().parse().unwrap(); + id = Some(id_value); + } + if let Some(date_node) = tag.find(Name("small")).next() { + if let Some(msg_span) = tag.find(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::new(id, typ, date, upload_link, text)); + } + } + 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>>, + username: &str, +) { + 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], username); + 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: &str) -> 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 remain = split.len() - ptr; + if txt.len() <= remain { + ptr += txt.len(); + line.push((color, txt)); + first_in_line = false; + } else { + //line.push((color, txt[0..remain].to_owned())); + if let Some(valid_slice) = txt.get(0..remain) { + line.push((color, valid_slice.to_owned())); + } else { + let valid_remain = txt.char_indices() + .take_while(|&(i, _)| i < remain) + .last() + .map(|(i, _)| i) + .unwrap_or(txt.len()); + + line.push((color, txt[..valid_remain].to_owned())); + } + + new_lines.push(line.clone()); + line.clear(); + line.push((tuiColor::White, line_prefix.to_owned())); + //ctxt.push((color, txt[(remain)..].to_owned())); + if let Some(valid_slice) = txt.get(remain..) { + ctxt.push((color, valid_slice.to_owned())); + } else { + let valid_remain = txt.char_indices() + .skip_while(|&(i, _)| i < remain) // Find first valid boundary after remain + .map(|(i, _)| i) + .next() + .unwrap_or(txt.len()); + + ctxt.push((color, txt[valid_remain..].to_owned())); + } + + ptr = 0; + split_idx += 1; + first_in_line = true; + } + } + } else { + new_lines.push(line); + 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, ""); + + let mut rows = vec![]; + for line in new_lines.into_iter() { + let spans_vec: Vec<Span> = line + .into_iter() + .map(|(color, txt)| Span::styled(txt, Style::default().fg(color))) + .collect(); + rows.push(Spans::from(spans_vec)); + } + + 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, + curr_user: &str, +) { + 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("Q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to logout, "), + Span::styled("i", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to start editing."), + ], + Style::default(), + ), + InputMode::Editing | InputMode::EditingErr => ( + 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()), + }; + msg.extend(vec![Span::raw(format!(" | {}", curr_user))]); + 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)]); + } + + //Strange + 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)]); + } + + //Strange + if app.display_member_view { + let fg = tuiColor::LightGreen; + let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); + msg.extend(vec![Span::raw(" | "), Span::styled("M", style)]); + } else { + let fg = tuiColor::Gray; + let style = Style::default().fg(fg); + msg.extend(vec![Span::raw(" | "), Span::styled("M", 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), + InputMode::EditingErr => Style::default().fg(tuiColor::Red), + }) + .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 | InputMode::EditingErr => { + // 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 + let text = m.text.text(); + if text.starts_with(&app.members_tag) || text.starts_with(&app.staffs_tag) { + return None; + } + if let Some((_, Some(_), _)) = get_message(&m.text, &app.members_tag) { + return None; + } + } + + // Strange + // Display only messages from members and staff + if app.display_member_view { + // In members mode, include only messages from members and staff + let text = m.text.text(); + if !text.starts_with(&app.members_tag) && !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).as_str()); + + 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)); + 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 style = match (m.deleted, m.hide) { + (true, _) => Style::default().bg(tuiColor::Rgb(30, 0, 0)), + (_, true) => Style::default().bg(tuiColor::Rgb(20, 20, 20)), + _ => Style::default(), + }; + Some(ListItem::new(rows).style(style)) + }) + .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)>, &str)> = Vec::new(); + users_types.push((&users.admin, "-- Admin --")); + users_types.push((&users.staff, "-- Staff --")); + users_types.push((&users.members, "-- Members --")); + users_types.push((&users.guests, "-- Guests --")); + for (users, label) in users_types.into_iter() { + users_list.push(ListItem::new(Span::raw(label))); + for (tui_color, username) in users.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() +} + +#[derive(PartialEq)] +enum InputMode { + LongMessage, + Normal, + Editing, + EditingErr, +} + +/// 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_member_view: bool, + display_hidden_msgs: bool, + items: StatefulList<Message>, + filter: String, + members_tag: String, + staffs_tag: String, + long_message: Option<Message>, + commands: Commands, +} + +impl Default for App { + fn default() -> App { + // Read commands from the file and set them as default values + let commands = if let Ok(config_path) = confy::get_configuration_file_path("bhcli", None) { + if let Some(config_path_str) = config_path.to_str() { + match read_commands_file(config_path_str) { + Ok(commands) => commands, + Err(err) => { + log::error!( + "Failed to read commands from config file - {} : +{}", + config_path_str, + err + ); + Commands { + commands: HashMap::new(), + } + } + } + } else { + log::error!("Failed to convert configuration file path to string."); + Commands { + commands: HashMap::new(), + } + } + } else { + log::error!("Failed to get configuration file path."); + Commands { + commands: HashMap::new(), + } + }; + + App { + input: String::new(), + input_idx: 0, + input_mode: InputMode::Normal, + is_muted: false, + show_sys: false, + display_guest_view: false, + display_member_view: false, + display_hidden_msgs: false, + items: StatefulList::new(), + filter: "".to_owned(), + members_tag: "".to_owned(), + staffs_tag: "".to_owned(), + long_message: None, + commands, + } + } +} + +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<()>, + 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<()>, + 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 thread_handle = 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::FocusGained => {} + CEvent::FocusLost => {} + CEvent::Paste(_) => {} + CEvent::Resize(_, _) => tx.send(Event::Input(evt)).unwrap(), + CEvent::Key(_) => tx.send(Event::Input(evt)).unwrap(), + CEvent::Mouse(mouse_event) => { + match mouse_event.kind { + MouseEventKind::ScrollDown + | MouseEventKind::ScrollUp + | 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, + }, + thread_handle, + ) + } + + fn next(&self) -> 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, ""); + 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,62 @@ +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); + } + + pub fn select_top(&mut self) { + if self.items.is_empty() { + return; + } + self.state.select(Some(0)); + } +} diff --git a/strange_bhcli.jpg b/strange_bhcli.jpg Binary files differ.