commit 05e2355615950efe32afccf6ac7e4b49f860171a Author: y1lm0z Date: Sat Apr 4 01:33:50 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..df99c69 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9d405f1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: rust + +jobs: + allow_failures: + - os: osx + name: macOS 10.15 + + include: + - stage: Lint + os: linux + before_script: + - rustup component add rustfmt + - rustup component add clippy + script: + - cargo fmt --all -- --check + - cargo clippy -- -D warnings + + - stage: Build + os: linux + + - os: osx + + - os: osx + name: macOS 10.15 + osx_image: xcode12 + + +script: + - cargo build diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3f7a0dd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4916 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accessory" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87537f9ae7cfa78d5b8ebd1a1db25959f5e737126be4d8eb44a5452fc4b63cde" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.3", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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 = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "aquamarine" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" +dependencies = [ + "include_dir", + "itertools 0.10.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "archery" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae2ed21cd55021f05707a807a5fc85695dafb98832921f6cfa06db67ca5b869" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] + +[[package]] +name = "as_variant" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a" + +[[package]] +name = "assign" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backon" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if 1.0.3", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.9.4", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.87", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "bitpacking" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" +dependencies = [ + "crunchy", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 1.0.3", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bon" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2529c31017402be841eb45892278a6c21a000c0a17643af326c73a73f83f0fb" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82020dadcb845a345591863adb65d74fa8dc5c18a0b6d408470e13b7adc7005" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.87", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" + +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytesize" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + +[[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 = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if 1.0.3", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10612c0ec0e0a1ff0e97980647cb058a6e7aedb913d01d009c406b8b7d0b26ee" +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", + "unicode-width", + "vec_map", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_panic" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013b6c2c3a14d678f38cd23994b02da3a1a1b6a5d1eedddfe63a5a5f11b13a81" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if 1.0.3", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +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-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if 1.0.3", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if 1.0.3", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "date_header" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c03c416ed1a30fbb027ef484ba6ab6f80e1eada675e1a2b92fd673c045a1f1d" + +[[package]] +name = "deadpool" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "deadpool-sqlite" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8510000b26f632483a35120c2ce280c29e1e14c2dcb27b5055dbdac276f63f58" +dependencies = [ + "deadpool", + "deadpool-sync", + "rusqlite", +] + +[[package]] +name = "deadpool-sync" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" +dependencies = [ + "deadpool-runtime", +] + +[[package]] +name = "decancer" +version = "3.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9244323129647178bf41ac861a2cdb9d9c81b9b09d3d0d1de9cd302b33b8a1d" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "eyeball" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93bd0ebf93d61d6332d3c09a96e97975968a44e19a64c947bde06e6baff383f" +dependencies = [ + "futures-core", + "readlock", + "readlock-tokio", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "eyeball-im" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e8e9d31591be508826b875d8fe6056aebcaec3281ac0e45434ff303686c566" +dependencies = [ + "futures-core", + "imbl", + "tokio", + "tracing", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fancy_constructor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b19d0e43eae2bfbafe4931b5e79c73fb1a849ca15cd41a761a7b8587f9a1a2" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "fastdivide" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[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" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if 1.0.3", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if 1.0.3", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "growable-bloom-filter" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d174ccb4ba660d431329e7f0797870d0a4281e36353ec4b4a3c5eab6c2cfb6f1" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "xxhash-rust", +] + +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.0", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[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 = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-auth" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" +dependencies = [ + "memchr", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.9", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "hyperloglogplus" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3" +dependencies = [ + "serde", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "once_cell", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "imbl" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4308a675e4cfc1920f36a8f4d8fb62d5533b7da106844bd1ec51c6f1fa94a0c" +dependencies = [ + "archery", + "bitmaps", + "imbl-sized-chunks", + "rand_core 0.9.3", + "rand_xoshiro", + "serde", + "version_check", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" +dependencies = [ + "bitmaps", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexed_db_futures" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43315957678a70eb21fb0d2384fe86dde0d6c859a01e24ce127eb65a0143d28c" +dependencies = [ + "accessory", + "cfg-if 1.0.3", + "delegate-display", + "fancy_constructor", + "js-sys", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if 1.0.3", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "js_int" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a" +dependencies = [ + "serde", +] + +[[package]] +name = "js_option" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c" +dependencies = [ + "serde", +] + +[[package]] +name = "konst" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" +dependencies = [ + "const_panic", + "konst_kernel", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" +dependencies = [ + "typewit", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.3", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[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.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.0", +] + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if 1.0.3", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matrix-pickle" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2551de3bba2cc65b52dc6b268df6114011fe118ac24870fbcf1b35537bd721" +dependencies = [ + "matrix-pickle-derive", + "thiserror 1.0.64", +] + +[[package]] +name = "matrix-pickle-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75de44c3120d78e978adbcf6d453b20ba011f3c46363e52d1dbbc72f545e9fb" +dependencies = [ + "proc-macro-crate", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "matrix-sdk" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfa71339f867dcada2e7f1130f858fd8892088b1a4c123dd50a99ed2399ab22" +dependencies = [ + "anymap2", + "aquamarine", + "as_variant", + "async-channel", + "async-stream", + "async-trait", + "backon", + "bytes", + "bytesize", + "cfg-if 1.0.3", + "event-listener", + "eyeball", + "eyeball-im", + "futures-core", + "futures-util", + "gloo-timers", + "http", + "imbl", + "indexmap", + "itertools 0.14.0", + "js_int", + "language-tags", + "matrix-sdk-base", + "matrix-sdk-common", + "matrix-sdk-indexeddb", + "matrix-sdk-search", + "matrix-sdk-sqlite", + "mime", + "mime2ext", + "oauth2", + "once_cell", + "percent-encoding", + "pin-project-lite", + "reqwest", + "ruma", + "serde", + "serde_html_form", + "serde_json", + "sha2", + "tempfile", + "thiserror 2.0.16", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "urlencoding", + "vodozemac", + "zeroize", +] + +[[package]] +name = "matrix-sdk-base" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14659a7e902ea8a821ec217f36b168fb4c79020d91b912175ac188b6c364225" +dependencies = [ + "as_variant", + "async-trait", + "bitflags 2.9.4", + "decancer", + "eyeball", + "eyeball-im", + "futures-util", + "growable-bloom-filter", + "matrix-sdk-common", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "once_cell", + "regex", + "ruma", + "serde", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tracing", + "unicode-normalization", +] + +[[package]] +name = "matrix-sdk-common" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb33a986495135e217f28cfe0918bf7d01a9800e42f4ef88afbd48e23b8cc53" +dependencies = [ + "eyeball-im", + "futures-core", + "futures-executor", + "futures-util", + "gloo-timers", + "imbl", + "ruma", + "serde", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tracing", + "tracing-subscriber", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "matrix-sdk-crypto" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61bf6c3195de301c98339283413a4e9d9d63c4f214ef8955147643caab161256" +dependencies = [ + "aes", + "aquamarine", + "as_variant", + "async-trait", + "bs58", + "byteorder", + "cfg-if 1.0.3", + "ctr", + "eyeball", + "futures-core", + "futures-util", + "hkdf", + "hmac", + "itertools 0.14.0", + "js_option", + "matrix-sdk-common", + "matrix-sdk-qrcode", + "pbkdf2", + "rand 0.8.5", + "rmp-serde", + "ruma", + "serde", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.16", + "time", + "tokio", + "tokio-stream", + "tracing", + "ulid", + "url", + "vodozemac", + "zeroize", +] + +[[package]] +name = "matrix-sdk-indexeddb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2752015e69b6b56a8df72e52f91a834771259f755eb1c65232b77348ffb16b" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "getrandom 0.2.15", + "gloo-utils", + "hkdf", + "indexed_db_futures", + "js-sys", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "rmp-serde", + "ruma", + "serde", + "serde-wasm-bindgen", + "serde_json", + "sha2", + "thiserror 2.0.16", + "tokio", + "tracing", + "wasm-bindgen", + "web-sys", + "zeroize", +] + +[[package]] +name = "matrix-sdk-qrcode" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e46baa0a7fd0e6e648887c0edfeb739682f7e12242986ffdf4300791cdbe199" +dependencies = [ + "byteorder", + "qrcode", + "ruma", + "thiserror 2.0.16", + "vodozemac", +] + +[[package]] +name = "matrix-sdk-search" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7474bd9045b6ce28fbe5c903a1cef2ba43264e51386c1d07f61699a178f3fec0" +dependencies = [ + "ruma", + "tantivy", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "matrix-sdk-sqlite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662a128004b553196365d88ea46917047d7350a24dfe7ce829e7b78636673604" +dependencies = [ + "as_variant", + "async-trait", + "deadpool-sqlite", + "itertools 0.14.0", + "matrix-sdk-base", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "num_cpus", + "rmp-serde", + "ruma", + "rusqlite", + "serde", + "serde_json", + "serde_path_to_error", + "thiserror 2.0.16", + "tokio", + "tracing", + "vodozemac", +] + +[[package]] +name = "matrix-sdk-store-encryption" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0aac550e685306fbbd57faae8f98af9812f19bbf0f83da7559d67cf4789679" +dependencies = [ + "base64", + "blake3", + "chacha20poly1305", + "hmac", + "pbkdf2", + "rand 0.8.5", + "rmp-serde", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.16", + "zeroize", +] + +[[package]] +name = "measure_time" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" +dependencies = [ + "log", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime2ext" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc" + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[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 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "murmurhash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[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 = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.2", + "libc", +] + +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64", + "chrono", + "getrandom 0.2.15", + "http", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.64", + "url", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oneshot" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags 2.9.4", + "cfg-if 1.0.3", + "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", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ownedbytes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[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 1.0.3", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pipe-channel" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0239fd4927d7ca8f368e41af249e611d875eb42216b6f26753b5525ddfbedb1f" +dependencies = [ + "libc", + "nix", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn 2.0.87", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.9.4", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "readlock" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "072cfe5b1d2dcd38d20e18f85e9c9978b6cc08f0b373e9f1fff1541335622974" + +[[package]] +name = "readlock-tokio" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b1800712c0d75de4b0bda5483d46eaf8df757b81df5ca2bde53d5ac2e2c5b2" +dependencies = [ + "tokio", +] + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "async-compression", + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if 1.0.3", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "ruma" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b698b728bc3747f564a9115c83b4f2e229b52377f6a1cca2e6add9cf4a13be" +dependencies = [ + "assign", + "js_int", + "js_option", + "ruma-client-api", + "ruma-common", + "ruma-events", + "ruma-federation-api", + "ruma-html", + "ruma-signatures", + "web-time", +] + +[[package]] +name = "ruma-client-api" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54e56c591f9ad686defb0bacbebba5c8882eb0c9f8734f6a080345b4e3dd941" +dependencies = [ + "as_variant", + "assign", + "bytes", + "date_header", + "http", + "js_int", + "js_option", + "maplit", + "ruma-common", + "ruma-events", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.16", + "url", + "web-time", +] + +[[package]] +name = "ruma-common" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac7f59b9f7639667d0d6ae3ae242c8912e9ed061cea1fbaf72710a402e83b53e" +dependencies = [ + "as_variant", + "base64", + "bytes", + "form_urlencoded", + "getrandom 0.2.15", + "http", + "indexmap", + "js-sys", + "js_int", + "konst", + "percent-encoding", + "rand 0.8.5", + "regex", + "ruma-identifiers-validation", + "ruma-macros", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.16", + "time", + "tracing", + "url", + "uuid", + "web-time", + "wildmatch", + "zeroize", +] + +[[package]] +name = "ruma-events" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34fa815769ed4fe1ef5b50aa0ba6f350317c13b5a9f1e008b014f4a3ddf14204" +dependencies = [ + "as_variant", + "indexmap", + "js_int", + "js_option", + "percent-encoding", + "pulldown-cmark", + "regex", + "ruma-common", + "ruma-identifiers-validation", + "ruma-macros", + "serde", + "serde_json", + "thiserror 2.0.16", + "tracing", + "url", + "web-time", + "wildmatch", + "zeroize", +] + +[[package]] +name = "ruma-federation-api" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecbc887ba1292e48e6363b29e0dec4571b52d2b5102ebf60068105efadaa6e0a" +dependencies = [ + "headers", + "http", + "http-auth", + "httparse", + "js_int", + "memchr", + "mime", + "ruma-common", + "ruma-events", + "serde", + "serde_json", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "ruma-html" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6124d74847ea788601477c89a44485894432a806824cae93885c5825a8ae9dbc" +dependencies = [ + "as_variant", + "html5ever", + "tracing", + "wildmatch", +] + +[[package]] +name = "ruma-identifiers-validation" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a7b93ac1e571c585f8fa5cef09c07bb8a15529775fd56b9a3eac4f9233dff2" +dependencies = [ + "js_int", + "thiserror 2.0.16", +] + +[[package]] +name = "ruma-macros" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9911c7188517f28505d2d513339511d00e0f50cec5c2dde820cd0ec7e6a833" +dependencies = [ + "cfg-if 1.0.3", + "proc-macro-crate", + "proc-macro2", + "quote", + "ruma-identifiers-validation", + "serde", + "syn 2.0.87", + "toml", +] + +[[package]] +name = "ruma-signatures" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47cd146d56ae6e7a4a8d912a30dfe57c70e5bf18806fdf617527d4d4f2dd2a4" +dependencies = [ + "base64", + "ed25519-dalek", + "pkcs8", + "rand 0.8.5", + "ruma-common", + "serde_json", + "sha2", + "thiserror 2.0.16", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.9.4", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + +[[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 = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_html_form" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if 1.0.3", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if 1.0.3", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[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 = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.99", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tantivy" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502915c7381c5cb2d2781503962610cb880ad8f1a0ca95df1bae645d5ebf2545" +dependencies = [ + "aho-corasick", + "arc-swap", + "base64", + "bitpacking", + "bon", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fastdivide", + "fnv", + "fs4", + "htmlescape", + "hyperloglogplus", + "itertools 0.14.0", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror 2.0.16", + "time", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b04eed5108d8283607da6710fe17a7663523440eaf7ea5a1a440d19a1448b6" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b628488ae936c83e92b5c4056833054ca56f76c0e616aee8339e24ac89119cd" +dependencies = [ + "downcast-rs", + "fastdivide", + "itertools 0.14.0", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f880aa7cab0c063a47b62596d10991cdd0b6e0e0575d9c5eeb298b307a25de55" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" +dependencies = [ + "byteorder", + "regex-syntax", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768fccdc84d60d86235d42d7e4c33acf43c418258ff5952abf07bd7837fcd26b" +dependencies = [ + "nom", + "serde", + "serde_json", +] + +[[package]] +name = "tantivy-sstable" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8292095d1a8a2c2b36380ec455f910ab52dde516af36321af332c93f20ab7d5" +dependencies = [ + "futures-util", + "itertools 0.14.0", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-fst", + "zstd", +] + +[[package]] +name = "tantivy-stacker" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d38a379411169f0b3002c9cba61cdfe315f757e9d4f239c00c282497a0749d" +dependencies = [ + "murmurhash32", + "rand_distr", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23024f6aeb25ceb1a0e27740c84bdb0fae52626737b7e9a9de6ad5aa25c7b038" +dependencies = [ + "serde", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "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 = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl 1.0.64", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[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.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "typewit" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.1", + "web-time", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom 0.2.15", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[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 = "vodozemac" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c022a277687e4e8685d72b95a7ca3ccfec907daa946678e715f8badaa650883d" +dependencies = [ + "aes", + "arrayvec", + "base64", + "base64ct", + "cbc", + "chacha20poly1305", + "curve25519-dalek", + "ed25519-dalek", + "getrandom 0.2.15", + "hkdf", + "hmac", + "matrix-pickle", + "prost", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.16", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if 1.0.3", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if 1.0.3", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weechat" +version = "0.4.0" +source = "git+https://github.com/poljar/rust-weechat#b4baca13c0808495d1b363266222d571287c4847" +dependencies = [ + "async-task", + "async-trait", + "backtrace", + "futures", + "libc", + "paste", + "pipe-channel", + "strum", + "weechat-macro", + "weechat-sys", +] + +[[package]] +name = "weechat-macro" +version = "0.4.0" +source = "git+https://github.com/poljar/rust-weechat#b4baca13c0808495d1b363266222d571287c4847" +dependencies = [ + "libc", + "proc-macro2", + "quote", + "syn 1.0.99", +] + +[[package]] +name = "weechat-matrix" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "dashmap", + "image", + "matrix-sdk", + "qrcode", + "serde_json", + "strum", + "tokio", + "tracing", + "tracing-subscriber", + "unicode-segmentation", + "url", + "uuid", + "weechat", +] + +[[package]] +name = "weechat-sys" +version = "0.4.0" +source = "git+https://github.com/poljar/rust-weechat#b4baca13c0808495d1b363266222d571287c4847" +dependencies = [ + "bindgen", + "libc", +] + +[[package]] +name = "wildmatch" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0" + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[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.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..983f281 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "weechat-matrix" +version = "0.1.0" +authors = ["Damir Jelić "] +edition = "2018" +license = "ISC" +resolver = "2" + +[lib] +name = "matrix" +crate-type = ["cdylib"] + +[features] +default = [] + +[dependencies] +clap = "2.34.0" +chrono = "0.4.22" +dashmap = "6.1.0" +url = "2.3.1" +serde_json = "1.0.85" +strum = { version = "0.24.0", features = ["derive"] } +tokio = { version = "1.21.1", features = ["rt-multi-thread", "sync"] } +tracing = "0.1.36" +tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } +uuid = { version = "1.1.2", features = ["v4"] } +unicode-segmentation = "1.10.0" + +# Auto-select version from matrix-sdk +qrcode = { version = "*", default-features = false } +image = { version = "*", default-features = false, features = ["jpeg"] } + +[dependencies.weechat] +git = "https://github.com/poljar/rust-weechat" +features = ["async", "config_macro"] + +[dependencies.matrix-sdk] +version = "0.14.0" +# git = "https://github.com/matrix-org/matrix-rust-sdk.git" +# rev = "92192c549b3f53889c23954386743867a73b31b1" +features = ["markdown", "socks", "qrcode"] + +[profile.dev.package] +sha2 = { opt-level = 2 } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c479151 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Weechat Matrix Protocol Plugin +Copyright © 2020 Damir Jelić + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9760f4d --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +# See https://weechat.org/files/doc/weechat/stable/weechat_user.en.html#xdg_directories +XDG_DATA_HOME ?= $(HOME)/.local/share +WEECHAT_DATA_DIR ?= $(XDG_DATA_HOME)/weechat + +SOURCES := $(wildcard src/*.rs src/bar_items/*.rs src/commands/*.rs src/room/*.rs Cargo.lock) + +PROFILE ?= release + +.PHONY: install install-dir lint all help + +all: help + +help: ## Print this help message + @grep -E '^[a-zA-Z._-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +target/debug/libmatrix.so: $(SOURCES) ## Build plugin in dev profile + cargo build + +target/release/libmatrix.so: $(SOURCES) ## Build plugin release profile + cargo build --release + +install: install-dir target/$(PROFILE)/libmatrix.so ## Install plugin to weechat dir + install -m644 target/$(PROFILE)/libmatrix.so $(DESTDIR)$(WEECHAT_DATA_DIR)/plugins/matrix.so + +install-dir: ## Create plugins directory + install -d $(DESTDIR)$(WEECHAT_DATA_DIR)/plugins + +lint: ## Lint issues with clippy + cargo clippy diff --git a/README.md b/README.md new file mode 100644 index 0000000..0be54ee --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +[![build-test-release](https://github.com/poljar/weechat-matrix-rs/actions/workflows/release.yml/badge.svg?event=push)](https://github.com/poljar/weechat-matrix-rs/actions/workflows/release.yml) +[![#weechat-matrix](https://img.shields.io/badge/matrix-%23weechat--matrix:termina.org.uk-blue.svg?style=flat-square)](https://matrix.to/#/!twcBhHVdZlQWuuxBhN:termina.org.uk?via=termina.org.uk&via=matrix.org) +[![license](https://img.shields.io/badge/license-ISC-blue.svg?style=flat-square)](https://github.com/poljar/weechat-matrix-rs/blob/master/LICENSE) + +# What is weechat-matrix? + +[Weechat](https://weechat.org/) is an extensible chat client. + +[Matrix](https://matrix.org/blog/home) is an open network for secure, +decentralized communication. + +weechat-matrix-rs is a Rust plugin for Weechat that lets Weechat communicate +over the Matrix protocol. This is a Rust rewrite of the +[weechat-matrix](https://github.com/poljar/weechat-matrix) Python script. + +# Project status + +This project is a work in progress and doesn't do much yet. It can connect +to a Matrix server and send messages. + +If you are interested in helping out take a look at the issue tracker. + +# Build + +After Rust is installed the plugin can be compiled with: + + cargo build --release + +If you are developing on weechat-matrix-rs, use debug builds which are faster at the expense of plugin performance: + + cargo build + +On Linux this creates a `libmatrix.so` file in the `target/release/` (`target/debug` for dev builds) folder, this +file needs to be renamed to `matrix.so` and copied to your Weechat plugin +directory. A plugin directory can be created in your `$WEECHAT_HOME` folder, by +default `.weechat/plugins/`. + +Alternatively, `make install` (`make install PROFILE=debug` for dev build) will build and install the plugin in your +`$WEECHAT_HOME` as well. + +# Configuration + +Configuration is completed primarily through the Weechat interface. First start Weechat, and then issue the following commands _(replace the placeholders in brackets [] with your own details)_: + +1. Add a server _(make sure the url includes the scheme e.g. 'https://matrix.org')_: + + /matrix server add [server-name] [server-url] + +2. Set your username and password: + + /set matrix-rust.server.[server-name].username [username] + /set matrix-rust.server.[server-name].password [password] + +3. Now try to connect: + + /matrix connect [server-name] + +4. Automatically connect to the server: + + /set matrix-rust.server.[server-name].autoconnect on + +5. If everything works, save the configuration: + + /save + + +# Helpful Commands + +`/help matrix` will print information about the `/matrix` command. + +`/matrix help [command]` will print information for subcommands, such as `/matrix help server` diff --git a/src/bar_items/buffer_name.rs b/src/bar_items/buffer_name.rs new file mode 100644 index 0000000..9e55ad9 --- /dev/null +++ b/src/bar_items/buffer_name.rs @@ -0,0 +1,58 @@ +use weechat::{ + buffer::Buffer, + hooks::{BarItem, BarItemCallback}, + Weechat, +}; + +use crate::{BufferOwner, Servers}; + +pub(super) struct BufferName { + servers: Servers, +} + +impl BufferName { + pub(super) fn create(servers: Servers) -> Result { + let status = BufferName { servers }; + BarItem::new("buffer_name", status) + } +} + +impl BarItemCallback for BufferName { + fn callback(&mut self, _: &Weechat, buffer: &Buffer) -> String { + match self.servers.buffer_owner(buffer) { + BufferOwner::Server(server) => { + let color = if server.is_connection_secure() { + "status_name_ssl" + } else { + "status_name" + }; + + format!( + "{color}server{del_color}[{color}{name}{del_color}]", + color = Weechat::color(color), + del_color = Weechat::color("bar_delim"), + name = server.name() + ) + } + + BufferOwner::Room(server, _) => { + let color = if server.is_connection_secure() { + "status_name_ssl" + } else { + "status_name" + }; + + format!("{}{}", Weechat::color(color), buffer.short_name()) + } + + BufferOwner::Verification(_, _) => { + // TODO special format this + format!("{}{}", Weechat::color("status_name"), buffer.name()) + } + + BufferOwner::None => { + format!("{}{}", Weechat::color("status_name"), buffer.name()) + } + } + } +} diff --git a/src/bar_items/buffer_plugin.rs b/src/bar_items/buffer_plugin.rs new file mode 100644 index 0000000..90e4873 --- /dev/null +++ b/src/bar_items/buffer_plugin.rs @@ -0,0 +1,38 @@ +use weechat::{ + buffer::Buffer, + hooks::{BarItem, BarItemCallback}, + Weechat, +}; + +use crate::{BufferOwner, Servers, PLUGIN_NAME}; + +pub(super) struct BufferPlugin { + servers: Servers, +} + +impl BufferPlugin { + pub(super) fn create(servers: Servers) -> Result { + let status = Self { servers }; + BarItem::new("buffer_plugin", status) + } +} + +impl BarItemCallback for BufferPlugin { + fn callback(&mut self, _: &Weechat, buffer: &Buffer) -> String { + match self.servers.buffer_owner(buffer) { + BufferOwner::Server(s) + | BufferOwner::Room(s, _) + | BufferOwner::Verification(s, _) => { + format!( + "{plugin_name}{del_color}/{color}{name}", + plugin_name = PLUGIN_NAME, + del_color = Weechat::color("bar_delim"), + color = Weechat::color("bar_fg"), + name = s.name() + ) + } + + BufferOwner::None => "".to_owned(), + } + } +} diff --git a/src/bar_items/mod.rs b/src/bar_items/mod.rs new file mode 100644 index 0000000..a4550c5 --- /dev/null +++ b/src/bar_items/mod.rs @@ -0,0 +1,29 @@ +mod buffer_name; +mod buffer_plugin; +mod status; + +use weechat::hooks::BarItem; + +use crate::Servers; +use buffer_name::BufferName; +use buffer_plugin::BufferPlugin; +use status::Status; + +pub struct BarItems { + #[allow(dead_code)] + status: BarItem, + #[allow(dead_code)] + buffer_name: BarItem, + #[allow(dead_code)] + buffer_plugin: BarItem, +} + +impl BarItems { + pub fn hook_all(servers: Servers) -> Result { + Ok(Self { + status: Status::create(servers.clone())?, + buffer_name: BufferName::create(servers.clone())?, + buffer_plugin: BufferPlugin::create(servers)?, + }) + } +} diff --git a/src/bar_items/status.rs b/src/bar_items/status.rs new file mode 100644 index 0000000..48f5f85 --- /dev/null +++ b/src/bar_items/status.rs @@ -0,0 +1,54 @@ +use weechat::{ + buffer::Buffer, + hooks::{BarItem, BarItemCallback}, + Weechat, +}; + +use crate::{BufferOwner, Servers}; + +pub(super) struct Status { + servers: Servers, +} + +impl Status { + pub(super) fn create(servers: Servers) -> Result { + let status = Status { servers }; + BarItem::new("buffer_modes", status) + } +} + +impl BarItemCallback for Status { + fn callback(&mut self, _: &Weechat, buffer: &Buffer) -> String { + let mut signs = Vec::new(); + + if let BufferOwner::Room(server, room) = + self.servers.buffer_owner(buffer) + { + if room.is_encrypted() { + signs.push( + server.config().borrow().look().encrypted_room_sign(), + ); + + if !room.contains_only_verified_devices() { + signs.push( + server + .config() + .borrow() + .look() + .encryption_warning_sign(), + ); + } + } + + if room.is_public() { + signs.push(server.config().borrow().look().public_room_sign()); + } + + if room.is_busy() { + signs.push(server.config().borrow().look().busy_sign()); + } + } + + signs.join("") + } +} diff --git a/src/commands/buffer_clear.rs b/src/commands/buffer_clear.rs new file mode 100644 index 0000000..6c6a859 --- /dev/null +++ b/src/commands/buffer_clear.rs @@ -0,0 +1,39 @@ +use std::borrow::Cow; + +use weechat::{ + buffer::Buffer, + hooks::{CommandRun, CommandRunCallback}, + ReturnCode, Weechat, +}; + +use crate::Servers; + +pub struct BufferClearCommand { + servers: Servers, +} + +impl BufferClearCommand { + pub fn create(servers: &Servers) -> Result { + CommandRun::new( + "/buffer clear", + BufferClearCommand { + servers: servers.clone(), + }, + ) + } +} + +impl CommandRunCallback for BufferClearCommand { + fn callback( + &mut self, + _: &Weechat, + buffer: &Buffer, + _: Cow, + ) -> ReturnCode { + if let Some(room) = self.servers.find_room(buffer) { + room.reset_prev_batch(); + } + + ReturnCode::Ok + } +} diff --git a/src/commands/devices.rs b/src/commands/devices.rs new file mode 100644 index 0000000..7b7e99b --- /dev/null +++ b/src/commands/devices.rs @@ -0,0 +1,151 @@ +use clap::{ + App as Argparse, AppSettings as ArgParseSettings, Arg, ArgMatches, + SubCommand, +}; +use matrix_sdk::ruma::{OwnedDeviceId, OwnedUserId, UserId}; + +use weechat::{ + buffer::Buffer, + hooks::{Command, CommandCallback, CommandSettings}, + Args, Prefix, Weechat, +}; + +use crate::Servers; + +use super::parse_and_run; + +pub struct DevicesCommand { + servers: Servers, +} + +impl DevicesCommand { + pub const DESCRIPTION: &'static str = + "List, delete or rename Matrix devices"; + + pub const SETTINGS: &'static [ArgParseSettings] = &[ + ArgParseSettings::DisableHelpFlags, + ArgParseSettings::DisableVersion, + ArgParseSettings::VersionlessSubcommands, + ArgParseSettings::SubcommandRequiredElseHelp, + ]; + + pub fn create(servers: &Servers) -> Result { + let settings = CommandSettings::new("devices") + .description(Self::DESCRIPTION) + .add_argument("list") + .add_argument("delete ") + .add_argument("set-name ") + .arguments_description( + "device-id: The unique id of the device that should be deleted. + name: The name that the device name should be set to.", + ) + .add_completion("list %(matrix-users)") + .add_completion("delete %(matrix-own-devices)") + .add_completion("set-name %(matrix-own-devices)") + .add_completion("help list|delete|set-name"); + + Command::new( + settings, + DevicesCommand { + servers: servers.clone(), + }, + ) + } + + fn delete(servers: &Servers, buffer: &Buffer, devices: Vec) { + let server = servers.find_server(buffer); + + if let Some(s) = server { + let devices = || async move { + s.delete_devices(devices).await; + }; + Weechat::spawn(devices()).detach(); + } else { + Weechat::print("Must be executed on Matrix buffer") + } + } + + fn list(servers: &Servers, buffer: &Buffer, user_id: Option) { + let server = servers.find_server(buffer); + + if let Some(s) = server { + let devices = || async move { + s.devices(user_id).await; + }; + Weechat::spawn(devices()).detach(); + } else { + Weechat::print("Must be executed on Matrix buffer") + } + } + + pub fn run(buffer: &Buffer, servers: &Servers, args: &ArgMatches) { + match args.subcommand() { + ("list", args) => { + let user_id = args.and_then(|a| { + a.args.get("user-id").and_then(|a| { + a.vals.first().map(|u| { + UserId::parse(u.to_string_lossy().as_ref()) + .expect("Argument wasn't a valid user id") + }) + }) + }); + + Self::list(servers, buffer, user_id); + } + ("delete", args) => { + let devices = args + .and_then(|a| a.args.get("device-id")) + .expect("Args didn't contain any device ids"); + let devices: Vec = devices + .vals + .iter() + .map(|d| d.clone().to_string_lossy().as_ref().into()) + .collect(); + Self::delete(servers, buffer, devices); + } + _ => Weechat::print(&format!( + "{}Subcommand isn't implemented", + Weechat::prefix(Prefix::Error) + )), + } + } + + pub fn subcommands() -> Vec> { + vec![ + SubCommand::with_name("list") + .arg(Arg::with_name("user-id").required(false).validator(|u| { + UserId::parse(u) + .map_err(|_| { + "The given user isn't a valid user ID".to_owned() + }) + .map(|_| ()) + })) + .about("List your own Matrix devices on the server."), + SubCommand::with_name("delete") + .about("Delete the given device") + .arg( + Arg::with_name("device-id") + .require_delimiter(true) + .multiple(true) + .required(true), + ), + SubCommand::with_name("set-name") + .about("Set the human readable name of the given device") + .arg(Arg::with_name("device-id").required(true)) + .arg(Arg::with_name("name").required(true)), + ] + } +} + +impl CommandCallback for DevicesCommand { + fn callback(&mut self, _: &Weechat, buffer: &Buffer, arguments: Args) { + let argparse = Argparse::new("devices") + .about(Self::DESCRIPTION) + .settings(Self::SETTINGS) + .subcommands(Self::subcommands()); + + parse_and_run(argparse, arguments, |matches| { + Self::run(buffer, &self.servers, matches) + }); + } +} diff --git a/src/commands/keys.rs b/src/commands/keys.rs new file mode 100644 index 0000000..baa0c8d --- /dev/null +++ b/src/commands/keys.rs @@ -0,0 +1,130 @@ +use std::path::PathBuf; + +use clap::{ + App as Argparse, AppSettings as ArgParseSettings, Arg, ArgMatches, + SubCommand, +}; + +use weechat::{ + buffer::Buffer, + hooks::{Command, CommandCallback, CommandSettings}, + Args, Weechat, +}; + +use super::parse_and_run; +use crate::{MatrixServer, Servers}; + +pub struct KeysCommand { + servers: Servers, +} + +impl KeysCommand { + pub const DESCRIPTION: &'static str = "Import or export E2EE keys."; + pub const COMPLETION: &'static str = "import|export %(filename)"; + pub const SETTINGS: &'static [ArgParseSettings] = &[ + ArgParseSettings::DisableHelpFlags, + ArgParseSettings::DisableVersion, + ArgParseSettings::VersionlessSubcommands, + ArgParseSettings::SubcommandRequiredElseHelp, + ]; + + pub fn create(servers: &Servers) -> Result { + let settings = CommandSettings::new("keys") + .description(Self::DESCRIPTION) + .add_argument("import ") + .add_argument("export ") + .arguments_description( + "file: Path to a file that is or will contain the E2EE keys export", + ) + .add_completion(Self::COMPLETION) + .add_completion("help import|export"); + + Command::new( + settings, + Self { + servers: servers.clone(), + }, + ) + } + + fn upcast_args(args: &ArgMatches) -> (PathBuf, String) { + let passphrase = args + .args + .get("passphrase") + .and_then(|p| p.vals.first().map(|p| p.clone().into_string().ok())) + .flatten() + .expect("No passphrase found"); + + let file = args + .args + .get("file") + .and_then(|f| f.vals.first()) + .expect("No file found"); + let file = Weechat::expand_home(&file.to_string_lossy()); + let file = PathBuf::from(file); + (file, passphrase) + } + + fn import(server: MatrixServer, file: PathBuf, passphrase: String) { + let import = || async move { + server.import_keys(file, passphrase).await; + }; + Weechat::spawn(import()).detach(); + } + + fn export(server: MatrixServer, file: PathBuf, passphrase: String) { + let export = || async move { + server.export_keys(file, passphrase).await; + }; + Weechat::spawn(export()).detach(); + } + + pub fn run(buffer: &Buffer, servers: &Servers, args: &ArgMatches) { + if let Some(server) = servers.find_server(buffer) { + match args.subcommand() { + ("import", args) => { + let (file, passphrase) = Self::upcast_args( + args.expect("No args were provided to the subcommand"), + ); + Self::import(server, file, passphrase); + } + ("export", args) => { + let (file, passphrase) = Self::upcast_args( + args.expect("No args were provided to the subcommand"), + ); + Self::export(server, file, passphrase); + } + _ => unreachable!(), + } + } else { + Weechat::print("Must be executed on Matrix buffer") + } + } + + pub fn subcommands() -> Vec> { + vec![ + SubCommand::with_name("import") + .about("Import the E2EE keys from the given file.") + .arg(Arg::with_name("file").required(true)) + .arg(Arg::with_name("passphrase").required(true)), + SubCommand::with_name("export") + // TODO add the ability to export keys only for a given room. + .about("Export your E2EE keys to the given file.") + .arg(Arg::with_name("file").required(true)) + .arg(Arg::with_name("passphrase").required(true)), + ] + } +} + +impl CommandCallback for KeysCommand { + fn callback(&mut self, _: &Weechat, buffer: &Buffer, arguments: Args) { + let argparse = Argparse::new("keys") + .about(Self::DESCRIPTION) + .settings(Self::SETTINGS) + .subcommands(Self::subcommands()); + + parse_and_run(argparse, arguments, |matches| { + Self::run(buffer, &self.servers, matches) + }); + } +} diff --git a/src/commands/matrix.rs b/src/commands/matrix.rs new file mode 100644 index 0000000..f470c00 --- /dev/null +++ b/src/commands/matrix.rs @@ -0,0 +1,330 @@ +use clap::{ + App as Argparse, AppSettings as ArgParseSettings, Arg, ArgMatches, + SubCommand, +}; +use url::Url; + +use weechat::{ + buffer::Buffer, + hooks::{Command, CommandCallback, CommandSettings}, + Args, Prefix, Weechat, +}; + +use super::{parse_and_run, verification::VerificationCommand}; +use crate::{ + commands::{DevicesCommand, KeysCommand}, + config::ConfigHandle, + MatrixServer, Servers, PLUGIN_NAME, +}; + +pub struct MatrixCommand { + servers: Servers, + config: ConfigHandle, +} + +impl MatrixCommand { + pub fn create( + servers: &Servers, + config: &ConfigHandle, + ) -> Result { + let matrix_settings = CommandSettings::new("matrix") + .description("Matrix chat protocol command.") + .add_argument("server add [:]") + .add_argument("server delete|list|listfull ") + .add_argument("connect ") + .add_argument("devices delete|list|set-name") + .add_argument("keys import|export ") + .add_argument("disconnect ") + .add_argument("reconnect ") + .add_argument("help []") + .arguments_description(format!( + " server: List, add, or remove Matrix servers. + connect: Connect to Matrix servers. + disconnect: Disconnect from one or all Matrix servers. + reconnect: Reconnect to server(s). + devices: {} + keys: {} +verification: {} + help: Show detailed command help.\n +Use /matrix [command] help to find out more.\n", + DevicesCommand::DESCRIPTION, + KeysCommand::DESCRIPTION, + VerificationCommand::DESCRIPTION, + )) + .add_completion("server add|delete|list|listfull") + .add_completion("devices list|delete|set-name %(matrix-users)") + .add_completion(format!("keys {}", KeysCommand::COMPLETION)) + .add_completion(format!( + "verification {}", + VerificationCommand::COMPLETION + )) + .add_completion("connect %(matrix_servers)") + .add_completion("disconnect %(matrix_servers)") + .add_completion("reconnect %(matrix_servers)") + .add_completion( + "help server|connect|disconnect|reconnect|keys|devices", + ); + + Command::new( + matrix_settings, + MatrixCommand { + servers: servers.clone(), + config: config.clone(), + }, + ) + } + + fn add_server(&self, args: &ArgMatches) { + let server_name = args + .value_of("name") + .expect("Server name not set but was required"); + let homeserver = args + .value_of("homeserver") + .expect("Homeserver not set but was required"); + let homeserver = Url::parse(homeserver) + .expect("Can't parse Homeserver even if validation passed"); + + let mut config_borrow = self.config.borrow_mut(); + let mut section = config_borrow + .search_section_mut("server") + .expect("Can't get server section"); + + let server = MatrixServer::new( + server_name, + &self.config, + &mut section, + self.servers.clone(), + ); + + self.servers.insert(server); + + let homeserver_option = section + .search_option(&format!("{}.homeserver", server_name)) + .expect("Homeserver option wasn't created"); + homeserver_option.set(homeserver.as_str(), true); + + Weechat::print(&format!( + "{}: Server {}{}{} has been added.", + PLUGIN_NAME, + Weechat::color("chat_server"), + server_name, + Weechat::color("reset") + )); + } + + fn delete_server(&self, args: &ArgMatches) { + let server_name = args + .value_of("name") + .expect("Server name not set but was required"); + + let connected = { + if let Some(s) = self.servers.get(server_name) { + s.connected() + } else { + Weechat::print(&format!( + "{}: No such server {}{}{} found.", + PLUGIN_NAME, + Weechat::color("chat_server"), + server_name, + Weechat::color("reset") + )); + return; + } + }; + + if connected { + Weechat::print(&format!( + "{}: Server {}{}{} is still connected.", + PLUGIN_NAME, + Weechat::color("chat_server"), + server_name, + Weechat::color("reset") + )); + return; + } + + let server = self.servers.remove(server_name).unwrap(); + + drop(server); + + Weechat::print(&format!( + "{}: Server {}{}{} has been deleted.", + PLUGIN_NAME, + Weechat::color("chat_server"), + server_name, + Weechat::color("reset") + )); + } + + fn list_servers(&self, details: bool) { + if self.servers.borrow().is_empty() { + return; + } + + Weechat::print("\nAll Matrix servers:"); + + // TODO print out some stats if the server is connected. + for server in self.servers.borrow().values() { + Weechat::print(&format!(" {}", server.get_info_str(details))); + } + } + + fn server_command(&self, args: &ArgMatches) { + match args.subcommand() { + ("add", Some(subargs)) => self.add_server(subargs), + ("delete", Some(subargs)) => self.delete_server(subargs), + ("list", _) => self.list_servers(false), + ("listfull", _) => self.list_servers(true), + _ => self.list_servers(false), + } + } + + fn server_not_found(&self, server_name: &str) { + Weechat::print(&format!( + "{}{}: Server \"{}{}{}\" not found.", + Weechat::prefix(Prefix::Error), + PLUGIN_NAME, + Weechat::color("chat_server"), + server_name, + Weechat::color("reset") + )); + } + + fn connect_command(&self, args: &ArgMatches) { + let server_names = args + .values_of("name") + .expect("Server names not set but were required"); + + for server_name in server_names { + if let Some(s) = self.servers.get(server_name) { + match s.connect() { + Ok(_) => (), + Err(e) => Weechat::print(&format!("{:?}", e)), + } + } else { + self.server_not_found(server_name) + } + } + } + + fn disconnect_command(&self, args: &ArgMatches) { + let server_name = args + .value_of("name") + .expect("Server name not set but was required"); + + if let Some(s) = self.servers.get(server_name) { + s.disconnect(); + } else { + self.server_not_found(server_name) + } + } + + fn run(&self, buffer: &Buffer, args: &ArgMatches) { + match args.subcommand() { + ("connect", Some(subargs)) => self.connect_command(subargs), + ("disconnect", Some(subargs)) => self.disconnect_command(subargs), + ("server", Some(subargs)) => self.server_command(subargs), + ("devices", Some(subargs)) => { + DevicesCommand::run(buffer, &self.servers, subargs) + } + ("keys", Some(subargs)) => { + KeysCommand::run(buffer, &self.servers, subargs) + } + ("verification", Some(subargs)) => { + VerificationCommand::run(buffer, &self.servers, subargs) + } + _ => unreachable!(), + } + } +} + +impl CommandCallback for MatrixCommand { + fn callback( + &mut self, + _weechat: &Weechat, + buffer: &Buffer, + arguments: Args, + ) { + let server_command = SubCommand::with_name("server") + .about("List, add or delete Matrix servers.") + .subcommand( + SubCommand::with_name("add") + .about("Add a new Matrix server.") + .arg( + Arg::with_name("name") + .value_name("server-name") + .required(true), + ) + .arg( + Arg::with_name("homeserver") + .required(true) + .validator(MatrixServer::parse_url), + ), + ) + .subcommand( + SubCommand::with_name("delete") + .about("Delete an existing Matrix server.") + .arg( + Arg::with_name("name") + .value_name("server-name") + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("list") + .about("List the configured Matrix servers."), + ) + .subcommand( + SubCommand::with_name("listfull") + .about("List detailed information about the configured Matrix servers."), + ); + + let argparse = Argparse::new("matrix") + .about("Matrix chat protocol command.") + .global_settings(&[ + ArgParseSettings::DisableHelpFlags, + ArgParseSettings::DisableVersion, + ArgParseSettings::VersionlessSubcommands, + ]) + .setting(ArgParseSettings::SubcommandRequiredElseHelp) + .subcommand(server_command) + .subcommand( + SubCommand::with_name("devices") + .about(DevicesCommand::DESCRIPTION) + .settings(DevicesCommand::SETTINGS) + .subcommands(DevicesCommand::subcommands()), + ) + .subcommand( + SubCommand::with_name("keys") + .about(KeysCommand::DESCRIPTION) + .settings(KeysCommand::SETTINGS) + .subcommands(KeysCommand::subcommands()), + ) + .subcommand( + SubCommand::with_name("verification") + .about(VerificationCommand::DESCRIPTION) + .subcommands(VerificationCommand::subcommands()), + ) + .subcommand( + SubCommand::with_name("connect") + .about("Connect to Matrix servers.") + .arg( + Arg::with_name("name") + .value_name("server-name") + .required(true) + .multiple(true), + ), + ) + .subcommand( + SubCommand::with_name("disconnect") + .about("Disconnect from one or all Matrix servers") + .arg( + Arg::with_name("name") + .value_name("server-name") + .required(true), + ), + ); + + parse_and_run(argparse, arguments, |args| self.run(buffer, args)); + } +} diff --git a/src/commands/me.rs b/src/commands/me.rs new file mode 100644 index 0000000..17c06d2 --- /dev/null +++ b/src/commands/me.rs @@ -0,0 +1,44 @@ +use std::borrow::Cow; + +use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; +use weechat::{ + buffer::Buffer, + hooks::{CommandRun, CommandRunCallback}, + ReturnCode, Weechat, +}; + +use crate::Servers; + +pub struct MeCommand { + servers: Servers, +} + +impl MeCommand { + pub fn create(servers: &Servers) -> Result { + CommandRun::new( + "/me", + MeCommand { + servers: servers.clone(), + }, + ) + } +} + +impl CommandRunCallback for MeCommand { + fn callback( + &mut self, + _: &Weechat, + buffer: &Buffer, + cmd: Cow, + ) -> ReturnCode { + if let Some(room) = self.servers.find_room(buffer) { + self.servers.runtime().block_on(room.send_message( + RoomMessageEventContent::emote_plain( + cmd.strip_prefix("/me ").map(|s| s.to_string()).unwrap(), + ), + )); + } + + ReturnCode::Ok + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..06cd0a5 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,69 @@ +use clap::{App, ArgMatches}; +use verification::VerificationCommand; +use weechat::{ + hooks::{Command, CommandRun}, + Args, Weechat, +}; + +use crate::{config::ConfigHandle, Servers}; + +mod buffer_clear; +mod devices; +mod keys; +mod matrix; +mod me; +mod page_up; +mod verification; + +use buffer_clear::BufferClearCommand; +use devices::DevicesCommand; +use keys::KeysCommand; +use matrix::MatrixCommand; +use me::MeCommand; +use page_up::PageUpCommand; + +pub struct Commands { + _matrix: Command, + _keys: Command, + _devices: Command, + _page_up: CommandRun, + _verification: Command, + _buffer_clear: CommandRun, + _me: CommandRun, +} + +impl Commands { + pub fn hook_all( + servers: &Servers, + config: &ConfigHandle, + ) -> Result { + Ok(Commands { + _matrix: MatrixCommand::create(servers, config)?, + _devices: DevicesCommand::create(servers)?, + _keys: KeysCommand::create(servers)?, + _page_up: PageUpCommand::create(servers)?, + _verification: VerificationCommand::create(servers)?, + _buffer_clear: BufferClearCommand::create(servers)?, + _me: MeCommand::create(servers)?, + }) + } +} + +fn parse_and_run( + parser: App, + arguments: Args, + command: impl FnOnce(&ArgMatches), +) { + match parser.get_matches_from_safe(arguments) { + Ok(m) => command(&m), + Err(e) => { + let error = Weechat::execute_modifier( + "color_decode_ansi", + "1", + &e.to_string(), + ) + .expect("Can't color decode ansi string"); + Weechat::print(&error); + } + } +} diff --git a/src/commands/page_up.rs b/src/commands/page_up.rs new file mode 100644 index 0000000..fc3df2c --- /dev/null +++ b/src/commands/page_up.rs @@ -0,0 +1,44 @@ +use std::borrow::Cow; + +use weechat::{ + buffer::Buffer, + hooks::{CommandRun, CommandRunCallback}, + ReturnCode, Weechat, +}; + +use crate::Servers; + +pub struct PageUpCommand { + servers: Servers, +} + +impl PageUpCommand { + pub fn create(servers: &Servers) -> Result { + CommandRun::new( + "/window page_up", + PageUpCommand { + servers: servers.clone(), + }, + ) + } +} + +impl CommandRunCallback for PageUpCommand { + fn callback( + &mut self, + _: &Weechat, + buffer: &Buffer, + _: Cow, + ) -> ReturnCode { + if let Some(room) = self.servers.find_room(buffer) { + if let Some(window) = buffer.window() { + if window.is_first_line_displayed() || buffer.num_lines() == 0 { + Weechat::spawn(async move { room.get_messages().await }) + .detach(); + } + } + } + + ReturnCode::Ok + } +} diff --git a/src/commands/verification.rs b/src/commands/verification.rs new file mode 100644 index 0000000..40496fd --- /dev/null +++ b/src/commands/verification.rs @@ -0,0 +1,117 @@ +use clap::{ + App as Argparse, AppSettings as ArgParseSettings, ArgMatches, SubCommand, +}; + +use weechat::{ + buffer::Buffer, + hooks::{Command, CommandCallback, CommandSettings}, + Args, Weechat, +}; + +use super::parse_and_run; +use crate::{BufferOwner, Servers}; + +pub struct VerificationCommand { + servers: Servers, +} + +enum CommandType { + Accept, + Confirm, + Cancel, +} + +impl VerificationCommand { + pub const DESCRIPTION: &'static str = + "Control interactive verification flows"; + + pub const COMPLETION: &'static str = "accept|confirm|cancel"; + + pub fn create(servers: &Servers) -> Result { + let settings = CommandSettings::new("verification") + .description(Self::DESCRIPTION) + .add_argument("verification accept|confirm|cancel") + .arguments_description( + "accept: accept the verification request + confirm: confirm that the emojis match on both sides or \ + confirm that the other side has scanned our QR code + cancel: cancel the verification flow or request", + ) + .add_completion(Self::COMPLETION) + .add_completion("help accept|confirm|cancel"); + + Command::new( + settings, + VerificationCommand { + servers: servers.clone(), + }, + ) + } + + fn verification(servers: &Servers, buffer: &Buffer, command: CommandType) { + let buffer_owner = servers.buffer_owner(buffer); + + match buffer_owner { + BufferOwner::Room(_, b) => match command { + CommandType::Accept => b.accept_verification(), + CommandType::Confirm => b.confirm_verification(), + CommandType::Cancel => b.cancel_verification(), + }, + BufferOwner::Verification(_, b) => match command { + CommandType::Accept => b.accept(), + CommandType::Confirm => b.confirm(), + CommandType::Cancel => b.cancel(), + }, + BufferOwner::Server(_) | BufferOwner::None => { + Weechat::print( + "The verification command needs to be executed in a room or \ + verification buffer", + ); + } + } + } + + pub fn run(buffer: &Buffer, servers: &Servers, args: &ArgMatches) { + match args.subcommand() { + ("accept", _) => { + Self::verification(servers, buffer, CommandType::Accept) + } + ("confirm", _) => { + Self::verification(servers, buffer, CommandType::Confirm) + } + ("cancel", _) => { + Self::verification(servers, buffer, CommandType::Cancel) + } + _ => unreachable!(), + } + } + + pub fn subcommands() -> Vec> { + vec![ + SubCommand::with_name("accept") + .about("Accept a verification request"), + SubCommand::with_name("confirm").about( + "Confirm that the emoji matches or that the other side has \ + scanned our QR code", + ), + SubCommand::with_name("cancel") + .about("Cancel the verification flow"), + ] + } +} + +impl CommandCallback for VerificationCommand { + fn callback(&mut self, _: &Weechat, buffer: &Buffer, arguments: Args) { + let argparse = Argparse::new("verification") + .about(Self::DESCRIPTION) + .global_setting(ArgParseSettings::DisableHelpFlags) + .global_setting(ArgParseSettings::DisableVersion) + .global_setting(ArgParseSettings::VersionlessSubcommands) + .setting(ArgParseSettings::SubcommandRequiredElseHelp) + .subcommands(Self::subcommands()); + + parse_and_run(argparse, arguments, |matches| { + Self::run(buffer, &self.servers, &matches) + }); + } +} diff --git a/src/completions.rs b/src/completions.rs new file mode 100644 index 0000000..c2a4114 --- /dev/null +++ b/src/completions.rs @@ -0,0 +1,110 @@ +use std::borrow::Cow; + +use weechat::{ + buffer::Buffer, + hooks::{ + Completion, CompletionCallback, CompletionHook, CompletionPosition, + }, + Weechat, +}; + +use crate::Servers; + +#[allow(dead_code)] +pub struct Completions { + servers: CompletionHook, + users: CompletionHook, +} + +impl Completions { + pub fn hook_all(servers: Servers) -> Result { + Ok(Self { + servers: ServersCompletion::create(servers.clone())?, + users: UsersCompletion::create(servers)?, + }) + } +} + +struct ServersCompletion { + servers: Servers, +} + +impl ServersCompletion { + fn create(servers: Servers) -> Result { + let comp = ServersCompletion { servers }; + + CompletionHook::new( + "matrix_servers", + "Completion for the list of added Matrix servers", + comp, + ) + } +} + +impl CompletionCallback for ServersCompletion { + fn callback( + &mut self, + _weechat: &Weechat, + _buffer: &Buffer, + _completion_name: Cow, + completion: &Completion, + ) -> Result<(), ()> { + for server_name in self.servers.borrow().keys() { + completion.add_with_options( + server_name, + false, + CompletionPosition::Sorted, + ); + } + Ok(()) + } +} + +struct UsersCompletion { + servers: Servers, +} + +impl UsersCompletion { + fn create(servers: Servers) -> Result { + let comp = UsersCompletion { servers }; + + CompletionHook::new( + "matrix-users", + "Completion for the list of Matrix users", + comp, + ) + } +} + +impl CompletionCallback for UsersCompletion { + fn callback( + &mut self, + _: &Weechat, + buffer: &Buffer, + _: Cow, + completion: &Completion, + ) -> Result<(), ()> { + if let Some(server) = self.servers.find_server(buffer) { + if let Some(connection) = server.connection() { + let tracked_users = self + .servers + .runtime() + .block_on(connection.client().encryption().tracked_users()) + .unwrap_or_else(|e| { + tracing::warn!("Error getting tracked users: {e}"); + Default::default() + }); + + for user in tracked_users.into_iter() { + completion.add_with_options( + user.as_str(), + true, + CompletionPosition::Sorted, + ) + } + } + } + + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f37b0df --- /dev/null +++ b/src/config.rs @@ -0,0 +1,274 @@ +//! Configuration module +//! +//! This is the global plugin configuration. +//! +//! Configuration options should be split out into different sections: +//! +//! * network +//! * look +//! * color +//! * server +//! +//! The server config options are added in the server.rs file. +//! +//! The config options created here will be alive as long as the plugin is +//! loaded so they don't need to be freed manually. The drop implementation of +//! the section will do so. + +use std::{ + cell::{Ref, RefCell, RefMut}, + rc::Rc, +}; + +use strum::{EnumVariantNames, VariantNames}; +use weechat::{ + config, + config::{ + Conf, ConfigOption, ConfigSection, ConfigSectionSettings, + EnumOptionSettings, OptionChanged, SectionReadCallback, + }, + Weechat, +}; + +use crate::{MatrixServer, Servers}; + +#[derive(EnumVariantNames)] +#[strum(serialize_all = "kebab_case")] +#[derive(Default)] +pub enum RedactionStyle { + #[default] + StrikeThrough, + Delete, + Notice, +} + +impl From for RedactionStyle { + fn from(value: i32) -> Self { + match value { + 0 => RedactionStyle::StrikeThrough, + 1 => RedactionStyle::Delete, + 2 => RedactionStyle::Notice, + _ => unreachable!(), + } + } +} + +#[derive(EnumVariantNames)] +#[strum(serialize_all = "kebab_case")] +#[derive(Default)] +pub enum ServerBuffer { + #[default] + MergeWithCore, + MergeWithoutCore, + Independent, +} + +impl From for ServerBuffer { + fn from(value: i32) -> Self { + match value { + 0 => ServerBuffer::MergeWithCore, + 1 => ServerBuffer::MergeWithoutCore, + 2 => ServerBuffer::Independent, + _ => unreachable!(), + } + } +} + +config!( + "matrix-rust", + + Section look { + encrypted_room_sign: String { + // Description. + "A sign that is used to show that the current room is encrypted", + // Default value. + "🔒", + }, + + encryption_warning_sign: String { + // Description. + "A sign that is used to show that the current room contains unverified devices", + // Default value. + "❗", + }, + + public_room_sign: String { + // Description. + "A sign indicating that the current room is public", + // Default value. + "🌍", + }, + + busy_sign: String { + // Description. + "A sign that is used to show that the client is busy, \ + e.g. when room history is being fetched", + // Default value. + "⏳", + }, + + local_echo: bool { + // Description + "Should the sending message be printed out before the server \ + confirms the reception of the message", + // Default value + true, + }, + + redaction_style: Enum { + // Description + "The style that should be used when a message needs to be redacted", + RedactionStyle, + }, + }, + + Section network { + debug_buffer: bool { + // Description + "Use a separate buffer for debug logs", + // Default value. + false, + }, + }, + + Section input { + markdown_input: bool { + // Description + "Should the input be parsed as markdown", + // Default value. + true, + }, + } +); + +/// A wrapper for our config struct that can be cloned around. +#[derive(Clone)] +pub struct ConfigHandle { + pub inner: Rc>, + servers: Servers, +} + +impl ConfigHandle { + /// Create a new config and wrap it in our config handle. + pub fn new(servers: &Servers) -> ConfigHandle { + let config = Config::new().expect("Can't create new config"); + + let config = ConfigHandle { + inner: Rc::new(RefCell::new(config)), + servers: servers.clone(), + }; + + // The server section is special since it has a custom section read and + // write implementations to support subsections for every configured + // server. + let server_section_options = ConfigSectionSettings::new("server") + .set_write_callback( + |_weechat: &Weechat, + config: &Conf, + section: &mut ConfigSection| { + config.write_section(section.name()); + for option in section.options() { + config.write_option(option); + } + }, + ) + .set_read_callback(config.clone()); + + { + let mut config_borrow = config.borrow_mut(); + + config_borrow + .new_section(server_section_options) + .expect("Can't create server section"); + + let mut look_section = config_borrow.look_mut(); + + let servers = servers.clone(); + + let settings = EnumOptionSettings::new("server_buffer") + .description("Should the server buffer be merged with other buffers or independent") + .set_change_callback(move |_, _| { + for server in servers.borrow().values() { + server.merge_server_buffers(); + } + }) + .default_value(ServerBuffer::default() as i32) + .string_values( + ServerBuffer::VARIANTS + .iter() + .map(|v| v.to_string()) + .collect::>(), + ); + + look_section + .new_enum_option(settings) + .expect("Can't create server buffers option"); + } + + config + } + + pub fn borrow(&self) -> Ref<'_, Config> { + self.inner.borrow() + } + + pub fn borrow_mut(&self) -> RefMut<'_, Config> { + self.inner.borrow_mut() + } +} + +impl LookSection<'_> { + pub fn server_buffer(&self) -> ServerBuffer { + if let ConfigOption::Enum(o) = + self.search_option("server_buffer").unwrap() + { + ServerBuffer::from(o.value()) + } else { + panic!("Server buffer option has the wrong type"); + } + } +} + +impl SectionReadCallback for ConfigHandle { + fn callback( + &mut self, + _: &Weechat, + _: &Conf, + section: &mut ConfigSection, + option_name: &str, + option_value: &str, + ) -> OptionChanged { + if option_name.is_empty() { + return OptionChanged::Error; + } + + let option_args: Vec<&str> = option_name.splitn(2, '.').collect(); + + if option_args.len() != 2 { + return OptionChanged::Error; + } + + let server_name = option_args[0]; + + // We are reading the config, if the server doesn't yet exists + // we need to create it before setting the option and running + // the option change callback. + if !self.servers.contains(server_name) { + let server = MatrixServer::new( + server_name, + self, + section, + self.servers.clone(), + ); + self.servers.insert(server); + } + + let option = section.search_option(option_name); + + if let Some(o) = option { + o.set(option_value, true) + } else { + OptionChanged::NotFound + } + } +} diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..6d6294e --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,569 @@ +use std::{ + future::Future, + path::PathBuf, + rc::{Rc, Weak}, + time::Duration, +}; + +use tokio::{ + runtime::Runtime, + sync::mpsc::{channel, Receiver, Sender}, +}; + +use tracing::error; + +use matrix_sdk::{ + self, + config::SyncSettings, + deserialized_responses::AmbiguityChange, + room::{Messages, MessagesOptions, Room}, + ruma::{ + api::client::{ + device::{ + delete_devices::v3::Response as DeleteDevicesResponse, + get_devices::v3::Response as DevicesResponse, + }, + filter::{ + FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter, + }, + message::send_message_event::v3::Response as RoomSendResponse, + session::login::v3::Response as LoginResponse, + sync::sync_events::v3::Filter, + uiaa::{AuthData, Password, UserIdentifier}, + }, + events::{ + room::member::RoomMemberEventContent, AnyMessageLikeEventContent, + AnySyncStateEvent, AnySyncTimelineEvent, AnyToDeviceEvent, + SyncStateEvent, + }, + OwnedDeviceId, OwnedRoomId, OwnedTransactionId, + }, + sync::State, + Client, LoopCtrl, Result as MatrixResult, RoomMemberships, +}; + +use weechat::{Task, Weechat}; + +use crate::{ + room::PrevBatch, + server::{InnerServer, MatrixServer}, +}; + +const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30); + +pub struct InteractiveAuthInfo { + pub user: String, + pub password: String, + pub session: Option, +} + +impl InteractiveAuthInfo { + pub fn as_auth_data(&self) -> AuthData { + AuthData::Password(Password::new( + UserIdentifier::UserIdOrLocalpart(self.user.clone()), + self.password.clone(), + )) + } +} + +pub enum ClientMessage { + LoginMessage(LoginResponse), + SyncState(OwnedRoomId, AnySyncStateEvent), + SyncEvent(OwnedRoomId, AnySyncTimelineEvent), + ToDeviceEvent(AnyToDeviceEvent), + MemberEvent( + OwnedRoomId, + SyncStateEvent, + bool, + Option, + ), + RestoredRoom(Room), +} + +/// Struct representing an active connection to the homeserver. +/// +/// Since the rust-sdk `Client` object uses reqwest for the HTTP client making +/// requests requires the request to be made on a tokio runtime. The connection +/// wraps the `Client` object and makes sure that requests are run on the +/// runtime the `Connection` holds. +/// +/// While this struct is alive a sync loop will be going on. To cancel the sync +/// loop drop the object. +#[derive(Debug, Clone)] +pub struct Connection { + #[allow(dead_code)] + receiver_task: Rc>, + client: Client, + pub runtime: Rc, +} + +impl Connection { + pub fn client(&self) -> &Client { + &self.client + } + + pub async fn spawn(&self, future: F) -> F::Output + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.runtime + .spawn(future) + .await + .expect("Tokio error while sending a message") + } + + pub fn new(server: &MatrixServer, client: &Client) -> Self { + let (tx, rx) = channel(10_000); + + let server_name = server.name(); + + let receiver_task = Weechat::spawn(Connection::response_receiver( + rx, + server.clone_weak(), + )); + + let runtime = Runtime::new().unwrap(); + + runtime.spawn(Connection::sync_loop( + client.clone(), + tx, + server.user_name(), + server.password(), + server_name.to_string(), + server.get_server_path(), + )); + + Self { + client: client.clone(), + runtime: runtime.into(), + receiver_task: receiver_task.into(), + } + } + + /// Send a message to the given room. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room which the message should be sent to. + /// + /// * `content` - The content of the message that will be sent to the + /// server. + /// + /// * `transaction_id` - Attach an unique id to this message, later on the + /// event will contain the same id in the unsigned part of the event. + pub async fn send_message( + &self, + room: Room, + content: AnyMessageLikeEventContent, + transaction_id: Option, + ) -> MatrixResult { + self.spawn(async move { + let mut msg = room.send(content); + if let Some(txn_id) = transaction_id.as_deref() { + msg = msg.with_transaction_id(txn_id.to_owned()); + } + + msg.await + }) + .await + } + + pub async fn delete_devices( + &self, + devices: Vec, + auth_info: Option, + ) -> MatrixResult { + let client = self.client.clone(); + Ok(self + .spawn(async move { + if let Some(info) = auth_info { + let auth = Some(info.as_auth_data()); + client.delete_devices(&devices, auth).await + } else { + client.delete_devices(&devices, None).await + } + }) + .await?) + } + + /// Fetch historical messages for the given room. + pub async fn room_messages( + &self, + room: Room, + prev_batch: PrevBatch, + ) -> MatrixResult { + self.spawn(async move { + let request = match &prev_batch { + PrevBatch::Backwards(t) => { + MessagesOptions::backward().from(Some(t.as_ref())) + } + PrevBatch::Forward(t) => { + MessagesOptions::forward().from(Some(t.as_ref())) + } + }; + + room.messages(request).await + }) + .await + } + + /// Get the list of our own devices. + pub async fn devices(&self) -> MatrixResult { + let client = self.client.clone(); + Ok(self.spawn(async move { client.devices().await }).await?) + } + + /// Set or reset a typing notice. + /// + /// # Arguments + /// + /// * `room_id` - The id of the room where the typing notice should be + /// active. + /// + /// * `typing` - Should we set or unset the typing notice. + pub async fn send_typing_notice( + &self, + room: Room, + typing: bool, + ) -> MatrixResult<()> { + self.spawn(async move { room.typing_notice(typing).await }) + .await + } + + fn save_device_id( + user_name: &str, + mut server_path: PathBuf, + response: &LoginResponse, + ) -> std::io::Result<()> { + server_path.push(user_name); + server_path.set_extension("device_id"); + std::fs::write(&server_path, &response.device_id) + } + + fn load_device_id( + user_name: &str, + mut server_path: PathBuf, + ) -> std::io::Result> { + server_path.push(user_name); + server_path.set_extension("device_id"); + + let device_id = std::fs::read_to_string(server_path); + + if let Err(e) = device_id { + // A file not found error is ok, report the rest. + if e.kind() != std::io::ErrorKind::NotFound { + return Err(e); + } + return Ok(None); + }; + + let device_id = device_id.unwrap_or_default(); + + if device_id.is_empty() { + Ok(None) + } else { + Ok(Some(device_id)) + } + } + + /// Response receiver loop. + /// This runs on the main Weechat thread and listens for responses coming + /// from the client running in the tokio executor. + pub async fn response_receiver( + mut receiver: Receiver>, + server: Weak, + ) { + while let Some(message) = receiver.recv().await { + let server = if let Some(s) = server.upgrade() { + s + } else { + return; + }; + + match message { + Ok(message) => match message { + ClientMessage::LoginMessage(r) => server.receive_login(r), + ClientMessage::SyncEvent(r, e) => { + server.receive_joined_timeline_event(&r, e).await + } + ClientMessage::SyncState(r, e) => { + server.receive_joined_state_event(&r, e).await + } + ClientMessage::RestoredRoom(room) => { + server.restore_room(room).await + } + ClientMessage::ToDeviceEvent(e) => { + server.receive_to_device_event(e).await + } + ClientMessage::MemberEvent( + room_id, + e, + is_state, + change, + ) => { + server + .receive_member(room_id, e, is_state, change) + .await + } + }, + Err(e) => server.print_error(&format!("Ruma error {}", e)), + }; + } + } + + #[allow(clippy::field_reassign_with_default)] + fn sync_filter() -> FilterDefinition { + let mut filter = FilterDefinition::default(); + let mut room_filter = RoomFilter::default(); + let mut event_filter = RoomEventFilter::default(); + + event_filter.lazy_load_options = LazyLoadOptions::Enabled { + include_redundant_members: false, + }; + event_filter.limit = Some(10u16.into()); + + room_filter.state = event_filter; + filter.room = room_filter; + + filter + } + + /// Main client sync loop. + /// This runs on the per server tokio executor. + /// It communicates with the main Weechat thread using a async channel. + pub async fn sync_loop( + client: Client, + channel: Sender>, + username: String, + password: String, + server_name: String, + server_path: PathBuf, + ) { + if !client.matrix_auth().logged_in() { + let device_id = + Connection::load_device_id(&username, server_path.clone()); + + let device_id = match device_id { + Err(e) => { + // TODO: do we want to do something with channel.send() + // errors? + let _ = channel + .send(Err(format!( + "Error while reading the device id for server {}: {:?}", + server_name, e + ))) + .await; + return; + } + Ok(d) => d, + }; + + let first_login = device_id.is_none(); + + let mut builder = client + .matrix_auth() + .login_username(&username, &password) + .initial_device_display_name("WeeChat-Matrix-rs"); + + if let Some(device_id) = device_id.as_ref() { + builder = builder.device_id(device_id); + }; + + match builder.send().await { + Ok(response) => { + if let Err(e) = Connection::save_device_id( + &username, + server_path.clone(), + &response, + ) { + let _ = channel + .send(Err(format!( + "Error while writing the device id for server {}: {:?}", + server_name, e + ))).await; + return; + } + + if channel + .send(Ok(ClientMessage::LoginMessage(response))) + .await + .is_err() + { + return; + } + } + Err(e) => { + let _ = channel + .send(Err(format!("Failed to log in: {:?}", e))) + .await; + return; + } + } + + if !first_login { + for room in client.joined_rooms() { + if channel + .send(Ok(ClientMessage::RestoredRoom(room))) + .await + .is_err() + { + return; + } + } + } + } + + let filter = client + .get_or_upload_filter("sync", Connection::sync_filter()) + .await + .unwrap(); + + let sync_token: Option = None; + let sync_settings = SyncSettings::new() + .timeout(DEFAULT_SYNC_TIMEOUT) + .filter(Filter::FilterId(filter)); + + let sync_settings = if let Some(t) = sync_token { + sync_settings.token(t) + } else { + sync_settings + }; + + let sync_channel = &channel; + + let client_ref = &client; + + loop { + let ret = client + .sync_with_callback(sync_settings.clone(), |response| async move { + for event in response.to_device.iter().filter_map(|e| e.to_raw().deserialize().ok()){ + if sync_channel + .send(Ok(ClientMessage::ToDeviceEvent(event))) + .await + .is_err() + { + return LoopCtrl::Break; + } + } + + for (room_id, room) in response.rooms.joined { + if let State::Before(state) = room.state { + for event in + state.iter().filter_map(|e| e.deserialize().ok()) + { + if let AnySyncStateEvent::RoomMember(m) = event { + let change = room + .ambiguity_changes + .get(m.event_id()) + .cloned(); + + if sync_channel + .send(Ok(ClientMessage::MemberEvent( + room_id.clone(), + m, + true, + change, + ))) + .await + .is_err() + { + return LoopCtrl::Break; + } + } else if sync_channel + .send(Ok(ClientMessage::SyncState( + room_id.clone(), + event, + ))) + .await + .is_err() + { + return LoopCtrl::Break; + } + } + } + + for event in room + .timeline + .events + .iter() + .filter_map(|e| e.raw().deserialize().ok()) + { + if let AnySyncTimelineEvent::State( + AnySyncStateEvent::RoomMember(m), + ) = event + { + let change = room + .ambiguity_changes + .get(m.event_id()) + .cloned(); + + if sync_channel + .send(Ok(ClientMessage::MemberEvent( + room_id.clone(), + m, + false, + change, + ))) + .await + .is_err() + { + return LoopCtrl::Break; + } + } else if sync_channel + .send(Ok(ClientMessage::SyncEvent( + room_id.clone(), + event, + ))) + .await + .is_err() + { + return LoopCtrl::Break; + } + } + + if let Some(r) = client_ref.get_room(&room_id) { + if !r.are_members_synced() { + let room_id = room_id.clone(); + let channel = sync_channel.clone(); + + tokio::spawn(async move { + if let Ok(members) = + r.members(RoomMemberships::ACTIVE).await + { + for member in members { + if let Err(e) = channel + .send(Ok( + ClientMessage::MemberEvent( + room_id.clone(), + member.event().as_sync().expect("Member event is not a StateSyncEvent").to_owned(), + true, + None, + ), + )) + .await + { + error!( + "Failed to send room member {}", + e + ); + } + } + } + }); + } + } + } + + LoopCtrl::Continue + }) + .await; + + if let Err(err) = ret { + error!("Matrix sync failed: {err}"); + } else { + break; + } + } + } +} diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..ed94aea --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,60 @@ +use std::{cell::RefMut, io}; + +use weechat::{ + buffer::{BufferBuilder, BufferHandle}, + Weechat, +}; + +use crate::Matrix; + +#[derive(Clone)] +pub struct Debug(); + +impl Debug { + fn create_debug_buffer(debug_buffer: &mut RefMut>) { + let buffer = BufferBuilder::new("Matrix debug") + .build() + .expect("Can't create Matrix debug buffer"); + **debug_buffer = Some(buffer); + } + + async fn write_helper(message: Vec) { + let matrix = Matrix::get(); + + let message = String::from_utf8(message).unwrap(); + let message = + Weechat::execute_modifier("color_decode_ansi", "1", &message) + .unwrap(); + + let mut debug_buffer = matrix.debug_buffer.borrow_mut(); + + if matrix.config.borrow().network().debug_buffer() { + let buffer = if let Some(buffer) = debug_buffer.as_ref() { + if let Ok(buffer) = buffer.upgrade() { + buffer + } else { + Debug::create_debug_buffer(&mut debug_buffer); + debug_buffer.as_ref().unwrap().upgrade().unwrap() + } + } else { + Debug::create_debug_buffer(&mut debug_buffer); + debug_buffer.as_ref().unwrap().upgrade().unwrap() + }; + + buffer.print(&message); + } else { + Weechat::print(&message) + } + } +} + +impl io::Write for Debug { + fn write(&mut self, buf: &[u8]) -> io::Result { + Weechat::spawn_from_thread(Debug::write_helper(buf.to_owned())); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eb26724 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,315 @@ +mod bar_items; +mod commands; +mod completions; +mod config; +mod connection; +mod debug; +mod render; +mod room; +mod server; +mod utils; +mod verification_buffer; + +use std::{ + cell::{Ref, RefCell}, + collections::HashMap, + rc::Rc, +}; + +use tokio::runtime::{Handle, Runtime}; +use tracing_subscriber::layer::SubscriberExt; + +use weechat::{ + buffer::{Buffer, BufferHandle}, + hooks::{SignalCallback, SignalData, SignalHook}, + plugin, Args, Plugin, ReturnCode, Weechat, +}; + +use crate::{ + bar_items::BarItems, commands::Commands, completions::Completions, + config::ConfigHandle, room::RoomHandle, server::MatrixServer, + verification_buffer::VerificationBuffer, +}; + +const PLUGIN_NAME: &str = "matrix"; + +#[derive(Clone, Debug)] +pub struct Servers { + inner: Rc>>, + runtime: Handle, +} + +#[allow(clippy::large_enum_variant)] +pub enum BufferOwner { + Server(MatrixServer), + Room(MatrixServer, RoomHandle), + Verification(MatrixServer, VerificationBuffer), + None, +} + +impl BufferOwner { + fn into_server(self) -> Option { + match self { + BufferOwner::Server(s) => Some(s), + BufferOwner::Room(s, _) => Some(s), + BufferOwner::Verification(s, _) => Some(s), + BufferOwner::None => None, + } + } + + fn into_room(self) -> Option { + if let BufferOwner::Room(_, r) = self { + Some(r) + } else { + None + } + } +} + +impl Servers { + fn new(handle: tokio::runtime::Handle) -> Self { + Servers { + inner: Rc::new(RefCell::new(HashMap::new())), + runtime: handle, + } + } + + fn borrow(&self) -> Ref<'_, HashMap> { + self.inner.borrow() + } + + pub fn runtime(&self) -> &Handle { + &self.runtime + } + + pub fn is_empty(&self) -> bool { + self.inner.borrow().is_empty() + } + + pub fn contains(&self, server_name: &str) -> bool { + self.inner.borrow().contains_key(server_name) + } + + pub fn clear(&self) { + self.inner.borrow_mut().clear(); + } + + pub fn insert(&self, server: MatrixServer) { + self.inner + .borrow_mut() + .insert(server.name().to_string(), server); + } + + pub fn get(&self, server_name: &str) -> Option { + self.inner.borrow().get(server_name).cloned() + } + + pub fn remove(&self, server_name: &str) -> Option { + self.inner.borrow_mut().remove(server_name) + } + + pub fn buffer_owner(&self, buffer: &Buffer) -> BufferOwner { + let servers = self.borrow(); + + for server in servers.values() { + if let Some(b) = &*server.server_buffer() { + if b.upgrade().is_ok_and(|b| &b == buffer) { + return BufferOwner::Server(server.clone()); + } + } + + for room in server.rooms() { + let buffer_handle = room.buffer_handle(); + + if let Ok(b) = buffer_handle.upgrade() { + if buffer == &b { + return BufferOwner::Room(server.clone(), room); + } + } + } + + for verification in server.verifications() { + if let Ok(b) = verification.buffer().upgrade() { + if buffer == &b { + return BufferOwner::Verification( + server.clone(), + verification, + ); + } + } + } + } + + BufferOwner::None + } + + /// Find a `MatrixServer` that the given buffer belongs to. + /// + /// Returns None if the buffer doesn't belong to any of our servers of + /// rooms. + pub fn find_server(&self, buffer: &Buffer) -> Option { + self.buffer_owner(buffer).into_server() + } + + /// Find a `RoomHandle` that the given buffer belongs to. + /// + /// Returns None if the buffer doesn't belong to any of our servers of + /// rooms. + pub fn find_room(&self, buffer: &Buffer) -> Option { + self.buffer_owner(buffer).into_room() + } +} + +impl SignalCallback for Servers { + fn callback( + &mut self, + _: &Weechat, + _signal_name: &str, + data: Option, + ) -> ReturnCode { + if let Some(SignalData::Buffer(buffer)) = data { + if let Some(room) = self.find_room(&buffer) { + room.update_typing_notice(); + } + } + ReturnCode::Ok + } +} + +struct Matrix { + #[allow(dead_code)] + global_runtime: Runtime, + servers: Servers, + #[allow(dead_code)] + commands: Commands, + config: ConfigHandle, + #[allow(dead_code)] + bar_items: BarItems, + #[allow(dead_code)] + typing_notice_signal: SignalHook, + #[allow(dead_code)] + completions: Completions, + debug_buffer: RefCell>, +} + +impl std::fmt::Debug for Matrix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut fmt = f.debug_struct("Matrix"); + fmt.field("servers", &self.servers).finish() + } +} + +impl Matrix { + fn autoconnect(servers: &HashMap) { + for server in servers.values() { + if server.autoconnect() { + match server.connect() { + Ok(_) => (), + Err(e) => Weechat::print(&format!("{:?}", e)), + } + } + } + } + + fn create_default_server(servers: Servers, config: &ConfigHandle) { + // TODO change this to matrix.org. + let server_name = "localhost"; + + let mut config_borrow = config.borrow_mut(); + let mut section = config_borrow + .search_section_mut("server") + .expect("Can't get server section"); + + let server = MatrixServer::new( + server_name, + config, + &mut section, + servers.clone(), + ); + servers.insert(server); + } +} + +impl Plugin for Matrix { + fn init(_: &Weechat, _args: Args) -> Result { + let global_runtime = + Runtime::new().expect("Couldn't create a new global runtime"); + + let servers = Servers::new(global_runtime.handle().to_owned()); + let config = ConfigHandle::new(&servers); + let commands = Commands::hook_all(&servers, &config)?; + + let bar_items = BarItems::hook_all(servers.clone())?; + let completions = Completions::hook_all(servers.clone())?; + + let subscriber = tracing_subscriber::registry() + .with(tracing_subscriber::filter::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_writer(debug::Debug)); + + let _ = tracing::subscriber::set_global_default(subscriber).map_err( + |_err| Weechat::print("Unable to set global default subscriber"), + ); + + { + let config_borrow = config.borrow(); + if config_borrow.read().is_err() { + return Err(()); + } + } + + if servers.is_empty() { + Matrix::create_default_server(servers.clone(), &config) + } + + let typing = SignalHook::new("input_text_changed", servers.clone()) + .expect("Can't create signal hook for the typing notice cb"); + + let plugin = Matrix { + global_runtime, + servers: servers.clone(), + commands, + config, + bar_items, + completions, + debug_buffer: RefCell::new(None), + typing_notice_signal: typing, + }; + + Weechat::spawn(async move { + let servers = servers.borrow(); + Matrix::autoconnect(&servers); + }) + .detach(); + + Ok(plugin) + } +} + +impl Drop for Matrix { + fn drop(&mut self) { + let servers = self.servers.borrow(); + + // Buffer close callbacks get called after this, so disconnect here so + // we don't leave all our rooms. + // + // TODO set a flag on the server as well so we don't even try to leave + // the rooms, once leaving the rooms is implemented when the buffer gets + // closed. + for server in servers.values() { + server.disconnect(); + } + + drop(servers); + + self.servers.clear(); + } +} + +plugin!( + Matrix, + name: "matrix", + author: "Damir Jelić ", + description: "Matrix protocol", + version: "0.1.0", + license: "ISC" +); diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..14301c6 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,1025 @@ +use url::Url; + +use matrix_sdk::{ + encryption::verification::{ + SasVerification, Verification, VerificationRequest, + }, + ruma::{ + events::{ + key::verification::{ + key::{ + KeyVerificationKeyEventContent, + ToDeviceKeyVerificationKeyEventContent, + }, + ready::{ + KeyVerificationReadyEventContent, + ToDeviceKeyVerificationReadyEventContent, + }, + request::ToDeviceKeyVerificationRequestEventContent, + start::{ + KeyVerificationStartEventContent, + ToDeviceKeyVerificationStartEventContent, + }, + }, + room::{ + encrypted::RoomEncryptedEventContent, + member::{MembershipChange, RoomMemberEventContent}, + message::{ + AudioMessageEventContent, EmoteMessageEventContent, + FileMessageEventContent, ImageMessageEventContent, + KeyVerificationRequestEventContent, + LocationMessageEventContent, NoticeMessageEventContent, + RedactedRoomMessageEventContent, + ServerNoticeMessageEventContent, TextMessageEventContent, + VideoMessageEventContent, + }, + EncryptedFile, MediaSource, + }, + OriginalSyncStateEvent, RedactedSyncMessageLikeEvent, + }, + uint, EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedUserId, + TransactionId, UserId, + }, +}; + +use weechat::{Prefix, Weechat}; + +use crate::{room::WeechatRoomMember, utils::ToTag}; + +/// The rendered version of an event. +pub struct RenderedEvent { + /// The UNIX timestamp of the event. + pub message_timestamp: isize, + pub prefix: String, + pub content: RenderedContent, +} + +impl RenderedEvent { + const MSG_TAGS: &'static [&'static str] = &["notify_message"]; + const SELF_TAGS: &'static [&'static str] = + &["notify_none", "no_highlight", "self_msg"]; + + pub fn add_self_tags(self) -> Self { + self.add_tags(Self::SELF_TAGS) + } + + pub fn add_msg_tags(self) -> Self { + self.add_tags(Self::MSG_TAGS) + } + + fn add_tags(mut self, tags: &[&str]) -> Self { + for line in &mut self.content.lines { + line.tags.extend(tags.iter().map(|tag| tag.to_string())) + } + + self + } +} + +#[derive(Debug)] +pub struct RenderedLine { + /// The tags of the line. + pub tags: Vec, + /// The message of the line. + pub message: String, +} + +#[derive(Debug)] +pub struct RenderedContent { + /// The collection of lines that the event has. + pub lines: Vec, +} + +/// Trait allowing events to be rendered for Weechat. +pub trait Render { + /// The event specific tags that should be attached to the rendered event. + const TAGS: &'static [&'static str]; + + /// Some events might need additional context to be rendered. For example, + /// instead of displaying the MXID for the sender, we might want to display + /// the disambiguated display name, which isn't available in the event. + /// + /// This allows the render implementation to be passed some additional data + /// when rendering. + type RenderContext; + + fn tags(&self) -> Vec { + Self::TAGS.iter().map(|t| t.to_string()).collect() + } + + fn event_tags( + &self, + event_id: &EventId, + sender: &UserId, + nick: &str, + color_name: &str, + ) -> Vec { + let mut tags = self.tags(); + let event_tag = event_id.to_tag(); + let sender_tag = sender.to_tag(); + let nick_tag = format!("nick_{}", nick); + let color = format!("prefix_nick_{}", color_name); + tags.push(event_tag); + tags.push(sender_tag); + tags.push(nick_tag); + tags.push(color); + + tags + } + + fn prefix(&self, sender: &WeechatRoomMember) -> String { + format!("{}\t", sender.nick_colored()) + } + + /// Render the event. + fn render_with_prefix( + &self, + timestamp: MilliSecondsSinceUnixEpoch, + event_id: &EventId, + sender: &WeechatRoomMember, + context: &Self::RenderContext, + ) -> RenderedEvent { + let prefix = self.prefix(sender); + let mut content = self.render(context); + let timestamp: i64 = (timestamp.0 / uint!(1000)).into(); + + let tags = self.event_tags( + event_id, + sender.user_id(), + &sender.nick(), + sender.color(), + ); + + for line in &mut content.lines { + line.tags = tags.clone(); + } + + RenderedEvent { + prefix, + message_timestamp: timestamp as isize, + content, + } + } + + fn render_with_prefix_for_echo( + &self, + sender: &WeechatRoomMember, + uuid: &TransactionId, + context: &Self::RenderContext, + ) -> RenderedEvent { + let content = self.render_for_echo(uuid, context); + let prefix = self.prefix(sender); + + RenderedEvent { + prefix, + message_timestamp: 0, + content, + } + } + + fn render_for_echo( + &self, + uuid: &TransactionId, + context: &Self::RenderContext, + ) -> RenderedContent { + let mut content = self.render(context); + let uuid_tag = format!("matrix_echo_{}", uuid); + + for line in &mut content.lines { + let message = Weechat::remove_color(&line.message); + line.message = format!( + "{}{}{}", + Weechat::color_pair("darkgray", "default"), + message, + Weechat::color("reset") + ); + line.tags.push(uuid_tag.clone()) + } + + content + } + + fn render(&self, context: &Self::RenderContext) -> RenderedContent; +} + +impl Render for TextMessageEventContent { + const TAGS: &'static [&'static str] = &["matrix_text"]; + type RenderContext = (); + + fn render(&self, _: &Self::RenderContext) -> RenderedContent { + let lines = self + .body + .lines() + .map(|l| RenderedLine { + message: l.to_owned(), + tags: self.tags(), + }) + .collect(); + // TODO: parse and render using the formatted body. + RenderedContent { lines } + } +} + +impl Render for EmoteMessageEventContent { + const TAGS: &'static [&'static str] = &["matrix_emote"]; + type RenderContext = WeechatRoomMember; + + fn prefix(&self, _: &WeechatRoomMember) -> String { + Weechat::prefix(Prefix::Action) + } + + fn render(&self, sender: &Self::RenderContext) -> RenderedContent { + // TODO: parse and render using the formatted body. + // TODO: handle multiple lines in the body. + let message = format!("{} {}", sender.nick(), self.body); + + let line = RenderedLine { + message, + tags: self.tags(), + }; + + RenderedContent { lines: vec![line] } + } +} + +impl Render for LocationMessageEventContent { + const TAGS: &'static [&'static str] = &["matrix_location"]; + type RenderContext = WeechatRoomMember; + + fn prefix(&self, _: &WeechatRoomMember) -> String { + Weechat::prefix(Prefix::Action) + } + + fn render(&self, sender: &Self::RenderContext) -> RenderedContent { + let message = format!( + "{} has shared a location: {color_delimiter}<{color_reset}{}{color_delimiter}>\ + [{color_reset}{}{color_delimiter}]{color_reset}", + sender.nick(), + self.body, + self.geo_uri, + color_delimiter = Weechat::color("color_delimiter"), + color_reset = Weechat::color("reset") + ); + + let line = RenderedLine { + message, + tags: self.tags(), + }; + + RenderedContent { lines: vec![line] } + } +} + +impl Render for NoticeMessageEventContent { + const TAGS: &'static [&'static str] = &["matrix_notice"]; + type RenderContext = WeechatRoomMember; + + fn prefix(&self, _: &WeechatRoomMember) -> String { + Weechat::prefix(Prefix::Network) + } + + fn render(&self, sender: &Self::RenderContext) -> RenderedContent { + // TODO: parse and render using the formatted body. + let message = format!( + "{color_notice}Notice\ + {color_delim}({color_reset}{}{color_delim}){color_reset}: {}", + sender.nick(), + self.body, + color_notice = Weechat::color("irc.color.notice"), + color_delim = Weechat::color("chat_delimiters"), + color_reset = Weechat::color("reset"), + ); + + let line = RenderedLine { + message, + tags: self.tags(), + }; + + RenderedContent { lines: vec![line] } + } +} + +impl Render for ServerNoticeMessageEventContent { + const TAGS: &'static [&'static str] = &["matrix_server_notice"]; + type RenderContext = WeechatRoomMember; + + fn prefix(&self, _: &WeechatRoomMember) -> String { + Weechat::prefix(Prefix::Network) + } + + fn render(&self, sender: &Self::RenderContext) -> RenderedContent { + let message = format!( + "{color_notice}Server notice\ + {color_delim}({color_reset}{}{color_delim}){color_reset}: {}", + sender.nick(), + self.body, + color_notice = Weechat::color("irc.color.notice"), + color_delim = Weechat::color("chat_delimiters"), + color_reset = Weechat::color("reset"), + ); + + let line = RenderedLine { + message, + tags: self.tags(), + }; + + RenderedContent { lines: vec![line] } + } +} + +/// Create an HTTP download path from a matrix content URI +fn mxc_to_http_download_path( + mxc_url: Url, +) -> Result> { + Ok(format!( + "/_matrix/media/r0/download/{server_name}{media_id}", + server_name = mxc_url.host_str().ok_or("Missing host")?, + media_id = mxc_url.path(), + )) +} + +/// Convert a matrix content URI to HTTP(s), respecting a user's homeserver +fn mxc_to_http( + mxc_url: &MxcUri, + homeserver: &Url, +) -> Result> { + let url = url::Url::parse(mxc_url.as_str())?; + + if url.scheme() != "mxc" { + return Err("URL missing MXC scheme".into()); + } + + if url.path().is_empty() { + return Err("URL missing path".into()); + } + + Ok(homeserver + .join(&mxc_to_http_download_path(url)?)? + .to_string()) +} + +/// Convert a matrix content URI to an encrypted mxc URI, respecting a user's homeserver. +/// +/// The return value of this function will have a URI schema of emxc://. The path of the URI will +/// be converted just like the mxc_to_http() function does, but it will also contain query +/// parameters that are necessary to decrypt the payload the URI is pointing to. +/// +/// This function is useful to present a clickable URI that can be passed to a plumber program that +/// will download and decrypt the content that the matrix content URI is pointing to. +/// +/// The returned URI should never be converted to http and opened directly, as that would expose +/// the decryption parameters to any middleman or ISP. +fn mxc_to_emxc( + mxc_url: &MxcUri, + homeserver: &Url, + encrypted: &EncryptedFile, +) -> Result> { + let url = url::Url::parse(mxc_url.as_str())?; + + if url.scheme() != "mxc" { + return Err("URL missing MXC scheme".into()); + } + + if url.path().is_empty() { + return Err("URL missing path".into()); + } + + let host_str = format!( + "emxc://{}", + homeserver + .host_str() + .ok_or("Missing homeserver host string")? + ); + + let mut emxc_url = url::Url::parse(&host_str)?; + emxc_url + .set_port(homeserver.port_or_known_default()) + .map_err(|_| "Can't set port")?; + + emxc_url = emxc_url.join(&mxc_to_http_download_path(url)?)?; + + // Add query parameters + emxc_url + .query_pairs_mut() + .append_pair("key", &encrypted.key.k.encode()) + .append_pair( + "hash", + &encrypted + .hashes + .get("sha256") + .ok_or("Missing sha256 hash")? + .encode(), + ) + .append_pair("iv", &encrypted.iv.encode()); + + Ok(emxc_url.to_string()) +} + +impl Render for C { + type RenderContext = Url; + const TAGS: &'static [&'static str] = &["matrix_media"]; + + fn render(&self, homeserver: &Self::RenderContext) -> RenderedContent { + // Convert MXC to HTTP(s) or EMXC, but fallback to MXC if unable to. + let mxc_url = match self.encrypted_file() { + Some(encrypted_file) => { + mxc_to_emxc(self.resolve_url(), homeserver, encrypted_file) + } + None => mxc_to_http(self.resolve_url(), homeserver), + } + .unwrap_or_else(|_| self.resolve_url().to_string()); + + let message = format!( + "{color_delimiter}<{color_reset}{}{color_delimiter}>\ + [{color_reset}{}{color_delimiter}]{color_reset}", + self.body(), + mxc_url, + color_delimiter = Weechat::color("color_delimiter"), + color_reset = Weechat::color("reset") + ); + + let line = RenderedLine { + message, + tags: self.tags(), + }; + + RenderedContent { lines: vec![line] } + } +} + +impl Render for RoomEncryptedEventContent { + const TAGS: &'static [&'static str] = &["matrix_encrypted"]; + type RenderContext = (); + + fn render(&self, _: &Self::RenderContext) -> RenderedContent { + let message = format!( + "{}<{}Unable to decrypt message{}>{}", + Weechat::color("chat_delimiters"), + Weechat::color("logger.color.backlog_line"), + Weechat::color("chat_delimiters"), + Weechat::color("reset"), + ); + + let line = RenderedLine { + message, + // TODO: add tags that allow us decrypt the event at a later point in + // time, sender key, algorithm, session id. + tags: self.tags(), + }; + + RenderedContent { lines: vec![line] } + } +} + +impl Render for RedactedSyncMessageLikeEvent { + type RenderContext = WeechatRoomMember; + const TAGS: &'static [&'static str] = &["matrix_redacted"]; + + fn render(&self, redacter: &Self::RenderContext) -> RenderedContent { + // TODO: add the redaction reason. + let message = format!( + "{}<{}Message redacted by: {}{}>{}", + Weechat::color("chat_delimiters"), + Weechat::color("logger.color.backlog_line"), + redacter.nick(), + Weechat::color("chat_delimiters"), + Weechat::color("reset"), + ); + + let line = RenderedLine { + message, + tags: self.tags(), + }; + + RenderedContent { lines: vec![line] } + } +} + +pub enum StartVerificationContext { + Room(OwnedUserId, Verification), + ToDevice(OwnedUserId, Verification), +} + +impl StartVerificationContext { + fn sender(&self) -> &UserId { + match self { + StartVerificationContext::Room(s, _) => &s, + StartVerificationContext::ToDevice(s, _) => &s, + } + } + + fn verification(&self) -> &Verification { + match self { + StartVerificationContext::Room(_, v) => &v, + StartVerificationContext::ToDevice(_, v) => &v, + } + } + + fn is_self_verification(&self) -> bool { + self.verification().is_self_verification() + } +} + +macro_rules! render_start_content { + ($type: ident) => { + impl Render for $type { + const TAGS: &'static [&'static str] = &[]; + + type RenderContext = StartVerificationContext; + + fn prefix(&self, _: &WeechatRoomMember) -> String { + Weechat::prefix(Prefix::Network) + } + + fn render(&self, context: &Self::RenderContext) -> RenderedContent { + let message = match context.verification() { + Verification::SasV1(sas) => { + if context.sender() == sas.own_user_id() { + if context.is_self_verification() { + if sas.started_from_request() { + // We auto accept emoji verifications that start + // from a verification request, so don't print + // anything. + return RenderedContent { + lines: vec![], + } + } else { + format!( + "You have started an interactive emoji \ + verification, accept on your other device.", + ) + } + } else { + format!( + "You have started an interactive emoji \ + verification, waiting for {} to accept", + sas.other_device().user_id() + ) + } + } else { + if sas.started_from_request() { + format!( + "{} has started an interactive emoji verifiaction \ + with you, accept with TODO", + sas.other_device().user_id() + ) + } else { + // We auto accept emoji verifications that start + // from a verification request, so don't print + // anything. + return RenderedContent { + lines: vec![], + } + } + } + } + Verification::QrV1(_) => { + // We don't support QR code scanning, so if there's an QR + // code verification struct it's because someone else + // scanned our QR code. + format!( + "{} has scanned our QR code, confirm that he \ + has done so TODO", + context.sender(), + ) + } + _ => unreachable!(), + }; + + RenderedContent { + lines: vec![RenderedLine { + message, + tags: self.tags(), + }], + } + } + } + }; +} + +render_start_content!(KeyVerificationStartEventContent); +render_start_content!(ToDeviceKeyVerificationStartEventContent); + +pub enum VerificationContext { + Room(WeechatRoomMember, WeechatRoomMember), + ToDevice(VerificationRequest), +} + +macro_rules! render_request_content { + ($type: ident) => { + impl Render for $type { + const TAGS: &'static [&'static str] = &[]; + + type RenderContext = VerificationContext; + + fn prefix(&self, _: &WeechatRoomMember) -> String { + Weechat::prefix(Prefix::Network) + } + + fn render(&self, context: &Self::RenderContext) -> RenderedContent { + let message = match context { + VerificationContext::Room(own_member, sender) => { + if own_member == sender { + "You sent a verification request".to_string() + } else { + format!( + "{} has sent a verification request", + sender.nick_colored() + ) + } + } + VerificationContext::ToDevice(_) => { + format!("You have requested this device to be verified") + } + }; + + RenderedContent { + lines: vec![RenderedLine { + message, + tags: self.tags(), + }], + } + } + } + }; +} + +render_request_content!(KeyVerificationRequestEventContent); +render_request_content!(ToDeviceKeyVerificationRequestEventContent); + +macro_rules! render_ready_content { + ($type: ident) => { + impl Render for $type { + const TAGS: &'static [&'static str] = &[]; + + type RenderContext = (WeechatRoomMember, WeechatRoomMember); + + fn prefix(&self, _: &WeechatRoomMember) -> String { + Weechat::prefix(Prefix::Network) + } + + fn render(&self, context: &Self::RenderContext) -> RenderedContent { + let (own_mebmer, sender) = context; + + let message = if own_mebmer == sender { + "You answered the verification request".to_string() + } else { + format!( + "{} has answered the verification request", + sender.nick_colored() + ) + }; + + RenderedContent { + lines: vec![RenderedLine { + message, + tags: self.tags(), + }], + } + } + } + }; +} + +render_ready_content!(KeyVerificationReadyEventContent); +render_ready_content!(ToDeviceKeyVerificationReadyEventContent); + +macro_rules! render_key_content { + ($type: ident) => { + impl Render for $type { + const TAGS: &'static [&'static str] = &[]; + type RenderContext = SasVerification; + + fn prefix(&self, _: &WeechatRoomMember) -> String { + Weechat::prefix(Prefix::Network) + } + + fn render(&self, sas: &Self::RenderContext) -> RenderedContent { + let (message, short_auth_string) = if sas.supports_emoji() { + ( + "Do the emojis match?".to_string(), + format!("{:?}", sas.emoji()), + ) + } else { + ( + "Do the decimals match".to_string(), + format!("{:?}", sas.decimals()), + ) + }; + + RenderedContent { + lines: vec![ + RenderedLine { + message, + tags: self.tags(), + }, + RenderedLine { + message: short_auth_string, + tags: self.tags(), + }, + ], + } + } + } + }; +} + +render_key_content!(KeyVerificationKeyEventContent); +render_key_content!(ToDeviceKeyVerificationKeyEventContent); + +/// Trait for message event types that contain an optional formatted body. +/// `resolve_body` will return the formatted body if present, else fallback to +/// the regular body. +trait HasFormattedBody { + fn body(&self) -> &str; + fn formatted_body(&self) -> Option<&str>; + #[inline] + fn resolve_body(&self) -> &str { + self.formatted_body().unwrap_or_else(|| self.body()) + } +} + +// Repeating this for each event type would get boring fast so lets use a simple +// macro to implement the trait for a struct that has a `body` and +// `formatted_body` field +macro_rules! has_formatted_body { + ($content: ident) => { + impl HasFormattedBody for $content { + #[inline] + fn body(&self) -> &str { + &self.body + } + + #[inline] + fn formatted_body(&self) -> Option<&str> { + self.formatted.as_ref().map(|f| f.body.as_ref()) + } + } + }; +} + +/// This trait is implemented for message types that can contain either an URL +/// or an encrypted file. One of these _must_ be present. +pub trait HasUrlOrFile { + fn url(&self) -> Option<&MxcUri>; + + fn body(&self) -> &str; + + #[inline] + fn resolve_url(&self) -> &MxcUri { + match self.source() { + MediaSource::Plain(s) => s, + MediaSource::Encrypted(e) => &e.url, + } + } + + fn encrypted_file(&self) -> Option<&EncryptedFile>; + + fn source(&self) -> &MediaSource; +} + +// Same as above: a simple macro to implement the trait for structs with `url` +// and `file` fields. +macro_rules! has_url_or_file { + ($content: ident) => { + impl HasUrlOrFile for $content { + fn body(&self) -> &str { + &self.body + } + + #[inline] + fn url(&self) -> Option<&MxcUri> { + match &self.source { + MediaSource::Plain(url) => Some(url), + _ => None, + } + } + + fn source(&self) -> &MediaSource { + &self.source + } + + fn encrypted_file(&self) -> Option<&EncryptedFile> { + match &self.source { + MediaSource::Encrypted(e) => Some(&e), + _ => None, + } + } + } + }; +} + +// this actually implements the trait for different event types +has_formatted_body!(EmoteMessageEventContent); +has_formatted_body!(NoticeMessageEventContent); +has_formatted_body!(TextMessageEventContent); + +has_url_or_file!(AudioMessageEventContent); +has_url_or_file!(FileMessageEventContent); +has_url_or_file!(ImageMessageEventContent); +has_url_or_file!(VideoMessageEventContent); + +/// Rendering implementation for membership events (joins, leaves, bans, profile +/// changes, etc). +pub fn render_membership( + event: &OriginalSyncStateEvent, + sender: &WeechatRoomMember, + target: &WeechatRoomMember, +) -> String { + const _TAGS: &[&str] = &["matrix_membership"]; + use MembershipChange::*; + let change_op = event.membership_change(); + + let operation = match change_op { + None => "did nothing", + Error => "caused an error", // must never happen + Joined => "has joined the room", + Left => "has left the room", + Banned => "was banned by", + Unbanned => "was unbanned by", + Kicked => "was kicked from the room by", + Invited => "was invited to the room by", + KickedAndBanned => "was kicked and banned by", + InvitationRejected => "rejected the invitation", + InvitationRevoked => "had the invitation revoked by", + ProfileChanged { .. } => "_", + _ => "performed an unimplemented operation", + }; + + fn formatted_name(member: &WeechatRoomMember) -> String { + match member.display_name() { + Some(display_name) => { + format!( + "{name} {color_delim}({color_reset}{user_id}{color_delim}){color_reset}", + name = display_name, + user_id = member.user_id(), + color_delim = Weechat::color("chat_delimiters"), + color_reset = Weechat::color("reset")) + } + + Option::None => member.user_id().to_string(), + } + } + + let (prefix, color_action) = match change_op { + Joined => (Prefix::Join, "green"), + Banned | ProfileChanged { .. } | Invited => { + (Prefix::Network, "magenta") + } + _ => (Prefix::Quit, "red"), + }; + + let color_action = Weechat::color(color_action); + let color_reset = Weechat::color("reset"); + + let operation = format!( + "{color_action}{op}{color_reset}", + color_action = color_action, + op = operation, + color_reset = color_reset + ); + + let target_name = format!( + "{color_user}{target_name}{color_reset}", + target_name = formatted_name(target), + color_user = Weechat::color("reset"), // TODO + color_reset = Weechat::color("reset") + ); + + let sender_name = format!( + "{color_user}{sender_name}{color_reset}", + sender_name = formatted_name(sender), + color_user = Weechat::color("reset"), // TODO + color_reset = Weechat::color("reset") + ); + + // TODO: we should return the tags as well. + match change_op { + ProfileChanged { + displayname_change, + avatar_url_change, + } => { + let new_display_name = &event.content.displayname; + + // TODO: Should we display the new avatar URL? + // let new_avatar = self.content.avatar_url.as_ref(); + + match (displayname_change.is_some(), avatar_url_change.is_some()) { + (false, true) => + format!( + "{prefix}{target} {color_action}changed their avatar{color_reset}", + prefix = Weechat::prefix(prefix), + target = target_name, + color_action = color_action, + color_reset = color_reset + ), + (true, false) => { + match new_display_name { + Some(name) => format!( + "{prefix}{target} {color_action}changed their display name to{color_reset} {new}", + prefix = Weechat::prefix(prefix), + target = event.prev_content().as_ref().and_then(|p| p.displayname.clone()).unwrap_or(target_name), + new = name, + color_action = color_action, + color_reset = color_reset + ), + Option::None => format!( + "{prefix}{target} {color_action}removed their display name{color_reset}", + prefix = Weechat::prefix(prefix), + target = target_name, + color_action = color_action, + color_reset = color_reset + ), + } + } + (true, true) => + match new_display_name { + Some(name) => format!( + "{prefix}{target} {color_action}changed their avatar \ + and changed their display name to{color_reset} {new}", + prefix = Weechat::prefix(prefix), + target = target_name, + new = name, + color_action = color_action, + color_reset = color_reset + ), + Option::None => format!( + "{prefix}{target} {color_action}changed their \ + avatar and removed display name{color_reset}", + prefix = Weechat::prefix(prefix), + target = target_name, + color_action = color_action, + color_reset = color_reset + ), + } + (false, false) => + "Cannot happen: got profile changed but nothing really changed".to_string() + } + } + Banned | Unbanned | Kicked | Invited | InvitationRevoked + | KickedAndBanned => format!( + "{prefix}{target} {op} {sender}", + prefix = Weechat::prefix(prefix), + target = target_name, + op = operation, + sender = sender_name + ), + _ => format!( + "{prefix}{target} {op}", + prefix = Weechat::prefix(prefix), + target = target_name, + op = operation + ), + } +} + +#[cfg(test)] +mod tests { + use matrix_sdk::ruma::{ + events::room::{EncryptedFileInit, JsonWebKeyInit}, + serde::Base64, + OwnedMxcUri, + }; + + use super::*; + + #[test] + fn test_mxc_to_http() { + let homeserver = url::Url::parse("https://matrix.org").unwrap(); + let mxc_url = OwnedMxcUri::from("mxc://matrix.org/some-media-id"); + let expected = + "https://matrix.org/_matrix/media/r0/download/matrix.org/some-media-id"; + assert_eq!(expected, mxc_to_http(&mxc_url, &homeserver).unwrap()); + } + + #[test] + fn test_emxc_to_http() { + use std::collections::BTreeMap; + + let homeserver = url::Url::parse("https://matrix.org").unwrap(); + let mxc_url = OwnedMxcUri::from("mxc://matrix.org/some-media-id"); + let mut hashes: BTreeMap = BTreeMap::new(); + hashes.insert("sha256".to_string(), Base64::parse("aGFzaA").unwrap()); + let encrypt_info = EncryptedFileInit { + key: JsonWebKeyInit { + k: Base64::parse("dGVzdA").unwrap(), + kty: "oct".to_string(), + key_ops: vec![], + ext: true, + alg: "A256CTR".to_string(), + } + .into(), + iv: Base64::parse("aXY").unwrap(), + v: "v2".to_string(), + url: OwnedMxcUri::from("mxc://some-url"), + hashes, + } + .into(); + let expected = + "emxc://matrix.org:443/_matrix/media/r0/download/matrix.org/some-media-id?key=dGVzdA&hash=aGFzaA&iv=aXY"; + assert_eq!( + expected, + mxc_to_emxc(&mxc_url, &homeserver, &encrypt_info).unwrap() + ); + } +} diff --git a/src/room/buffer.rs b/src/room/buffer.rs new file mode 100644 index 0000000..c095374 --- /dev/null +++ b/src/room/buffer.rs @@ -0,0 +1,274 @@ +use std::{borrow::Cow, cell::RefCell, rc::Rc}; + +use matrix_sdk::{ + ruma::{EventId, OwnedRoomAliasId, TransactionId, UserId}, + Room, StoreError, +}; +use tokio::runtime::Handle; +use weechat::{ + buffer::{Buffer, BufferHandle, BufferLine, LineData}, + Prefix, Weechat, +}; + +use crate::{render::RenderedEvent, utils::ToTag}; + +#[derive(Clone)] +pub struct RoomBuffer { + room: Room, + runtime: Handle, + pub(super) inner: Rc>>, +} + +impl RoomBuffer { + pub fn new(room: Room, runtime: Handle) -> Self { + Self { + room, + runtime, + inner: Rc::new(RefCell::new(None)), + } + } + + pub fn buffer_handle(&self) -> BufferHandle { + self.inner + .borrow() + .as_ref() + .expect("Room struct wasn't initialized properly") + .clone() + } + + pub fn short_name(&self) -> String { + self.inner + .borrow() + .as_ref() + .and_then(|b| b.upgrade().ok().map(|b| b.short_name().to_string())) + .unwrap_or_default() + } + + /// Replace the local echo of an event with a fully rendered one. + pub fn replace_local_echo( + &self, + transaction_id: &TransactionId, + rendered: RenderedEvent, + ) { + if let Ok(buffer) = self.buffer_handle().upgrade() { + let uuid_tag = Cow::from(format!("matrix_echo_{}", transaction_id)); + let line_contains_uuid = + |l: &BufferLine| l.tags().contains(&uuid_tag); + + let mut lines = buffer.lines(); + let mut current_line = lines.rfind(line_contains_uuid); + + // We go in reverse order here since we also use rfind(). We got from + // the bottom of the buffer to the top since we're expecting these + // lines to be freshly printed and thus at the bottom. + let mut line_num = rendered.content.lines.len(); + + while let Some(line) = ¤t_line { + line_num -= 1; + let rendered_line = &rendered.content.lines[line_num]; + + line.set_message(&rendered_line.message); + current_line = lines.next_back().filter(line_contains_uuid); + } + } + } + + pub fn replace_edit( + &self, + event_id: &EventId, + sender: &UserId, + event: RenderedEvent, + ) { + if let Ok(buffer) = self.buffer_handle().upgrade() { + let sender_tag = Cow::from(sender.to_tag()); + let event_id_tag = Cow::from(event_id.to_tag()); + + let lines: Vec = buffer + .lines() + .filter(|l| l.tags().contains(&event_id_tag)) + .collect(); + + if lines + .get(0) + .map(|l| l.tags().contains(&sender_tag)) + .unwrap_or(false) + { + self.replace_event_helper(&buffer, lines, event); + } + } + } + + fn replace_event_helper( + &self, + buffer: &Buffer, + lines: Vec>, + event: RenderedEvent, + ) { + use std::cmp::Ordering; + let date = lines.get(0).map(|l| l.date()).unwrap_or_default(); + + for (line, new) in lines.iter().zip(event.content.lines.iter()) { + let data = LineData { + // Our prefixes always come with a \t character, but when we + // replace stuff we're able to replace the prefix and the + // message separately, so trim the whitespace in the prefix. + prefix: Some(event.prefix.trim_end()), + message: Some(&new.message), + ..Default::default() + }; + + line.update(data); + } + + match lines.len().cmp(&event.content.lines.len()) { + Ordering::Greater => { + for line in &lines[event.content.lines.len()..] { + line.set_message(""); + } + } + Ordering::Less => { + for line in &event.content.lines[lines.len()..] { + let message = format!("{}{}", &event.prefix, &line.message); + let tags: Vec<&str> = + line.tags.iter().map(|t| t.as_str()).collect(); + buffer.print_date_tags(date, &tags, &message) + } + + self.sort_messages() + } + Ordering::Equal => (), + } + } + + pub fn sort_messages(&self) { + struct LineCopy { + date: isize, + date_printed: isize, + tags: Vec, + prefix: String, + message: String, + } + + impl<'a> From> for LineCopy { + fn from(line: BufferLine) -> Self { + Self { + date: line.date(), + date_printed: line.date_printed(), + message: line.message().to_string(), + prefix: line.prefix().to_string(), + tags: line.tags().iter().map(|t| t.to_string()).collect(), + } + } + } + + // TODO update the highlight once Weechat starts supporting it. + if let Ok(buffer) = self.buffer_handle().upgrade() { + let mut lines: Vec = + buffer.lines().map(|l| l.into()).collect(); + lines.sort_by_key(|l| l.date); + + for (line, new) in buffer.lines().zip(lines.drain(..)) { + let tags = + new.tags.iter().map(|t| t.as_str()).collect::>(); + let data = LineData { + prefix: Some(&new.prefix), + message: Some(&new.message), + date: Some(new.date), + date_printed: Some(new.date_printed), + tags: Some(&tags), + }; + line.update(data) + } + } + } + + pub fn set_topic(&self) { + if let Ok(buffer) = self.buffer_handle().upgrade() { + buffer.set_title(&self.room.topic().unwrap_or_default()); + } + } + + pub fn set_alias(&self) { + if let Some(alias) = self.alias() { + if let Ok(b) = self.buffer_handle().upgrade() { + b.set_localvar("alias", alias.as_str()); + } + } + } + + fn alias(&self) -> Option { + self.room.canonical_alias() + } + + pub fn calculate_buffer_name(&self) -> Result { + let room = self.room.clone(); + let room_name = self.runtime.block_on(room.display_name())?.to_string(); + + let room_name = if room_name == "#" { + "##".to_owned() + } else if room_name.starts_with('#') + || self.runtime.block_on(room.is_direct()).unwrap_or(false) + { + room_name + } else { + format!("#{}", room_name) + }; + + Ok(room_name.to_string()) + } + + pub fn update_buffer_name(&self) { + let buffer = self.buffer_handle(); + + let buffer = if let Ok(b) = buffer.upgrade() { + b + } else { + return; + }; + + match self.calculate_buffer_name() { + Ok(name) => buffer.set_short_name(&name), + Err(e) => { + Weechat::print(&format!( + "{}: Error fetching the room name from the store: {}", + Weechat::prefix(Prefix::Error), + e.to_string(), + )); + } + } + } + + pub fn replace_verification_event( + &self, + event_id: &EventId, + event: RenderedEvent, + ) { + if let Ok(buffer) = self.buffer_handle().upgrade() { + let event_id_tag = Cow::from(event_id.to_tag()); + + let lines: Vec = buffer + .lines() + .filter(|l| l.tags().contains(&event_id_tag)) + .collect(); + + self.replace_event_helper(&buffer, lines, event); + } + } + + pub fn print_rendered_event(&self, rendered: RenderedEvent) { + let buffer = self.buffer_handle(); + + if let Ok(buffer) = buffer.upgrade() { + for line in rendered.content.lines { + let message = format!("{}{}", &rendered.prefix, &line.message); + let tags: Vec<&str> = + line.tags.iter().map(|t| t.as_str()).collect(); + buffer.print_date_tags( + rendered.message_timestamp, + &tags, + &message, + ) + } + } + } +} diff --git a/src/room/members.rs b/src/room/members.rs new file mode 100644 index 0000000..2661fd5 --- /dev/null +++ b/src/room/members.rs @@ -0,0 +1,422 @@ +use std::rc::Rc; + +use dashmap::DashMap; +use tokio::runtime::Handle; +use tracing::{error, info}; + +use matrix_sdk::{ + deserialized_responses::AmbiguityChange, + room::{Room, RoomMember}, + ruma::{ + events::{ + room::{ + member::{MembershipState, RoomMemberEventContent}, + power_levels::UserPowerLevel, + }, + SyncStateEvent, + }, + uint, Int, OwnedUserId, UserId, + }, +}; + +use weechat::{ + buffer::{Buffer, NickSettings}, + Prefix, Weechat, +}; + +use crate::{render::render_membership, room::buffer::RoomBuffer}; + +#[derive(Clone)] +pub struct Members { + room: Room, + pub(super) runtime: Handle, + ambiguity_map: Rc>, + nicks: Rc>, + buffer: RoomBuffer, +} + +#[derive(Clone, Debug)] +pub struct WeechatRoomMember { + inner: RoomMember, + color: Rc, + ambiguous_nick: Rc, +} + +impl PartialEq for WeechatRoomMember { + fn eq(&self, other: &Self) -> bool { + self.user_id() == other.user_id() + } +} + +impl Members { + pub fn new(room: Room, runtime: Handle, buffer: RoomBuffer) -> Self { + Self { + room, + runtime, + nicks: DashMap::new().into(), + ambiguity_map: DashMap::new().into(), + buffer, + } + } + + fn add_nick(&self, buffer: &Buffer, member: &WeechatRoomMember) { + let nick = member.nick(); + + let group = buffer + .search_nicklist_group(member.nicklist_group_name()) + .expect("No group found when adding member"); + + let nick_settings = NickSettings::new(&nick) + .set_color(member.color()) + .set_prefix(member.nicklist_prefix()) + .set_prefix_color(member.prefix_color()); + + info!("Inserting nick {} for room {}", nick, buffer.short_name()); + + if group.add_nick(nick_settings).is_err() { + error!( + "Error adding nick {} ({}) to room {}, already added.", + nick, + member.user_id(), + buffer.short_name() + ); + }; + + self.nicks.insert(member.user_id().to_owned(), nick); + } + + pub async fn restore_member(&self, user_id: OwnedUserId) { + let room = self.room.clone(); + let user = user_id.to_owned(); + + match self + .runtime + .spawn(async move { room.get_member_no_sync(&user).await }) + .await + .expect("Fetching the room member from the store panicked") + { + Ok(Some(member)) => { + self.ambiguity_map + .insert(user_id.to_owned(), member.name_ambiguous()); + self.update_member(&user_id).await; + } + Ok(None) => { + panic!( + "Couldn't find member {} in {}", + user_id, + self.buffer.short_name() + ) + } + Err(e) => { + Weechat::print(&format!( + "{}: Error fetching a room member from the store: {}", + Weechat::prefix(Prefix::Error), + e, + )); + } + } + } + + pub async fn update_member(&self, user_id: &UserId) { + let buffer = self.buffer.buffer_handle(); + + let buffer = if let Ok(b) = buffer.upgrade() { + b + } else { + return; + }; + + if let Some(nick) = self.nicks.get(user_id) { + buffer.remove_nick(&nick); + } + + let member = self.get(user_id).await.unwrap_or_else(|| { + panic!( + "Couldn't find member {} in {}", + user_id, + buffer.short_name() + ) + }); + + self.add_nick(&buffer, &member); + } + + /// Add a new Weechat room member. + pub async fn add_or_modify( + &self, + user_id: &UserId, + ambiguity_change: Option<&AmbiguityChange>, + ) { + if let Some(change) = ambiguity_change { + self.ambiguity_map + .insert(user_id.to_owned(), change.member_ambiguous); + + if let Some(disambiguated) = &change.disambiguated_member { + self.ambiguity_map.insert(disambiguated.clone(), false); + self.update_member(disambiguated).await; + } + + if let Some(ambiguated) = &change.ambiguated_member { + self.ambiguity_map.insert(ambiguated.clone(), true); + self.update_member(ambiguated).await; + } + } + + self.update_member(user_id).await; + } + + /// Remove a Weechat room member by user ID. + /// + /// Returns either the removed Weechat room member, or an error if the + /// member does not exist. + async fn remove( + &self, + user_id: &UserId, + ambiguity_change: Option<&AmbiguityChange>, + ) { + self.ambiguity_map.remove(user_id); + + if let Some(change) = ambiguity_change { + if let Some(disambiguated) = &change.disambiguated_member { + self.ambiguity_map.insert(disambiguated.clone(), false); + self.update_member(disambiguated).await; + } + + if let Some(ambiguated) = &change.ambiguated_member { + self.ambiguity_map.insert(ambiguated.clone(), true); + self.update_member(ambiguated).await; + } + } + + let buffer = self.buffer.buffer_handle(); + + let buffer = if let Ok(b) = buffer.upgrade() { + b + } else { + return; + }; + + if let Some((_, nick)) = self.nicks.remove(user_id) { + buffer.remove_nick(&nick); + } + } + + /// Retrieve a reference to a Weechat room member by user ID. + pub async fn get(&self, user_id: &UserId) -> Option { + let color = if self.room.own_user_id() == user_id { + "weechat.color.chat_nick_self".into() + } else { + Weechat::info_get("nick_color_name", user_id.as_str()) + .expect("Couldn't get the nick color name") + }; + + let room = self.room.clone(); + let user = user_id.to_owned(); + + match self + .runtime + .spawn(async move { room.get_member_no_sync(&user).await }) + .await + .expect("Fetching the room member from the store panicked") + { + Ok(m) => m.map(|m| WeechatRoomMember { + color: Rc::new(color), + ambiguous_nick: Rc::new( + self.ambiguity_map + .get(m.user_id()) + .map(|a| *a) + .unwrap_or(false), + ), + inner: m, + }), + Err(e) => { + Weechat::print(&format!( + "{}: Error fetching a room member from the store: {}", + Weechat::prefix(Prefix::Error), + e, + )); + None + } + } + } + + fn room(&self) -> &Room { + &self.room + } + + pub async fn handle_membership_event( + &self, + event: &SyncStateEvent, + state_event: bool, + ambiguity_change: Option<&AmbiguityChange>, + ) { + let buffer = self.buffer.buffer_handle(); + let buffer = if let Ok(b) = buffer.upgrade() { + b + } else { + return; + }; + + let event = match event { + SyncStateEvent::Original(e) => e, + SyncStateEvent::Redacted(e) => { + error!("Unhandled redacted event: {e:?}"); + return; + } + }; + + info!( + "Handling membership event for room {} {} {:?}", + buffer.short_name(), + event.state_key, + event.content.membership + ); + + let sender_id = event.sender.clone(); + + let target_id = if let Ok(t) = UserId::parse(event.state_key.clone()) { + t + } else { + error!( + "Invalid state key in room {} from sender {}: {}", + buffer.short_name(), + event.sender, + event.state_key, + ); + return; + }; + + use MembershipState::*; + + // For joins and invites, first we need to check whether a member + // with some MXID exists. If he does, we have to update *that* + // member with the new state. Only if they do not exist yet do we + // create a new one. + // + // For leaves and bans we just need to remove the member. + match event.content.membership { + Invite | Join => { + self.add_or_modify(&target_id, ambiguity_change).await + } + Leave | Ban => self.remove(&target_id, ambiguity_change).await, + _ => (), + }; + + // Names of rooms without display names can get affected by the + // member list so we need to update them. + self.buffer.update_buffer_name(); + + if !state_event { + let sender = self.get(&sender_id).await; + let target = self.get(&target_id).await; + + // Display the event message + let message = match (&sender, &target) { + (Some(sender), Some(target)) => { + render_membership(event, sender, target) + } + + _ => { + if sender.is_none() { + error!( + "Cannot render event since event sender {} is not a room member", + sender_id); + } + + if target.is_none() { + error!( + "Cannot render event since event target {} is not a room member", + target_id); + } + + "ERROR: cannot render event since sender or target are not a room member".into() + } + }; + + let timestamp: i64 = + (event.origin_server_ts.0 / uint!(1000)).into(); + buffer.print_date_tags(timestamp as isize, &[], &message); + } + } +} + +impl WeechatRoomMember { + pub fn user_id(&self) -> &UserId { + self.inner.user_id() + } + + pub fn display_name(&self) -> Option<&str> { + self.inner.display_name() + } + + pub fn color(&self) -> &str { + &self.color + } + + fn nick_raw(&self) -> &str { + self.inner.name() + } + + fn nicklist_group_name(&self) -> &str { + match self.inner.normalized_power_level() { + UserPowerLevel::Infinite => "000|o", + p if p >= Int::from(100) => "000|o", + p if p >= Int::from(50) => "001|h", + p if p > Int::from(0) => "002|v", + _ => "999|...", + } + } + + fn nicklist_prefix(&self) -> &str { + match self.inner.normalized_power_level() { + UserPowerLevel::Infinite => "&", + p if p >= Int::from(100) => "&", + p if p >= Int::from(50) => "@", + p if p > Int::from(0) => "+", + _ => " ", + } + } + + fn prefix(&self) -> &str { + self.nicklist_prefix().trim() + } + + fn prefix_color(&self) -> &str { + match self.prefix() { + "&" => "lightgreen", + "@" => "lightmagenta", + "+" => "yellow", + _ => "default", + } + } + + pub fn nick_colored(&self) -> String { + if *self.ambiguous_nick { + // TODO: this should color the parenthesis differently. + format!( + "{}{}{} ({})", + Weechat::color(self.color()), + self.nick_raw(), + Weechat::color("reset"), + self.user_id(), + ) + } else { + format!( + "{}{}{}{}{}", + Weechat::color(self.prefix_color()), + self.prefix(), + Weechat::color(self.color()), + self.nick_raw(), + Weechat::color("reset") + ) + } + } + + pub fn nick(&self) -> String { + if *self.ambiguous_nick { + format!("{} ({})", self.nick_raw(), self.user_id()) + } else { + self.nick_raw().to_string() + } + } +} diff --git a/src/room/mod.rs b/src/room/mod.rs new file mode 100644 index 0000000..3c2bd47 --- /dev/null +++ b/src/room/mod.rs @@ -0,0 +1,1039 @@ +//! Room buffer module. +//! +//! This module implements creates buffers that processes and prints out all the +//! user visible events +//! +//! Care should be taken when handling events. Events can be state events or +//! timeline events and they can come from a sync response or from a room +//! messages response. +//! +//! Events coming from a sync response and are part of the timeline need to be +//! printed out and they need to change the buffer state (e.g. when someone +//! joins, they need to be added to the nicklist). +//! +//! Events coming from a sync response and are part of the room state only need +//! to change the buffer state. +//! +//! Events coming from a room messages response, meaning they are old events, +//! should never change the room state. They only should be printed out. +//! +//! Care should be taken to model this in a way that event formatting methods +//! are pure functions so they can be reused e.g. if we print messages that +//! we're sending ourselves before we receive them in a sync response, or if we +//! decrypt a previously undecryptable event. + +mod buffer; +mod members; +mod verification; + +use buffer::RoomBuffer; +use members::Members; +pub use members::WeechatRoomMember; +use tokio::runtime::Handle; +use tracing::{debug, trace}; +use verification::Verification; + +use std::{ + borrow::Cow, + cell::RefCell, + collections::HashMap, + ops::Deref, + rc::Rc, + sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, MutexGuard, + }, +}; + +use unicode_segmentation::UnicodeSegmentation; +use url::Url; + +use matrix_sdk::{ + async_trait, + deserialized_responses::AmbiguityChange, + room::Room, + ruma::{ + events::{ + room::{ + member::RoomMemberEventContent, + message::{ + MessageType, RoomMessageEventContent, + TextMessageEventContent, + }, + redaction::SyncRoomRedactionEvent, + }, + AnyMessageLikeEventContent, AnySyncMessageLikeEvent, + AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, + OriginalSyncMessageLikeEvent, SyncMessageLikeEvent, SyncStateEvent, + }, + EventId, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, + OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, + }, + StoreError, +}; + +use weechat::{ + buffer::{ + Buffer, BufferBuilderAsync, BufferHandle, BufferInputCallbackAsync, + BufferLine, + }, + Weechat, +}; + +use crate::{ + config::{Config, RedactionStyle}, + connection::Connection, + render::{Render, RenderedEvent}, + utils::{Edit, VerificationEvent}, + PLUGIN_NAME, +}; + +#[derive(Clone)] +pub struct RoomHandle { + inner: MatrixRoom, +} + +#[derive(Debug, Clone)] +pub enum PrevBatch { + Forward(String), + Backwards(String), +} + +impl Deref for RoomHandle { + type Target = MatrixRoom; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Clone, Debug)] +struct IntMutex { + inner: Rc>>, + locked: Rc, +} + +struct IntMutexGuard<'a> { + inner: MutexGuard<'a, Rc>, +} + +impl Drop for IntMutexGuard<'_> { + fn drop(&mut self) { + self.inner.store(false, Ordering::SeqCst) + } +} + +impl IntMutex { + fn new() -> Self { + let locked = Rc::new(AtomicBool::from(false)); + let inner = Rc::new(Mutex::new(locked.clone())); + + Self { inner, locked } + } + + fn locked(&self) -> bool { + self.locked.load(Ordering::SeqCst) + } + + fn try_lock(&self) -> Result, ()> { + match self.inner.try_lock() { + Ok(guard) => { + guard.store(true, Ordering::SeqCst); + + Ok(IntMutexGuard { inner: guard }) + } + Err(_) => Err(()), + } + } +} + +#[derive(Clone)] +pub struct MatrixRoom { + homeserver: Rc, + room_id: Rc, + own_user_id: Rc, + room: Room, + buffer: RoomBuffer, + + config: Rc>, + connection: Rc>>, + + messages_in_flight: IntMutex, + prev_batch: Rc>>, + + outgoing_messages: MessageQueue, + + members: Members, + verification: Verification, +} + +#[derive(Debug, Clone, Default)] +pub struct MessageQueue { + queue: Rc< + RefCell>, + >, +} + +impl MessageQueue { + fn new() -> Self { + Self { + queue: Rc::new(RefCell::new(HashMap::new())), + } + } + + fn add(&self, uuid: OwnedTransactionId, content: RoomMessageEventContent) { + self.queue.borrow_mut().insert(uuid, (false, content)); + } + + fn add_with_echo( + &self, + uuid: OwnedTransactionId, + content: RoomMessageEventContent, + ) { + self.queue.borrow_mut().insert(uuid, (true, content)); + } + + fn remove( + &self, + uuid: &TransactionId, + ) -> Option<(bool, RoomMessageEventContent)> { + self.queue.borrow_mut().remove(uuid) + } +} + +impl RoomHandle { + pub fn new( + server_name: &str, + runtime: Handle, + connection: &Rc>>, + config: Rc>, + room: Room, + homeserver: Url, + room_id: &RoomId, + own_user_id: &UserId, + ) -> Self { + let buffer = RoomBuffer::new(room.clone(), runtime.clone()); + let members = + Members::new(room.clone(), runtime.clone(), buffer.clone()); + + let own_nick = runtime + .block_on(room.get_member_no_sync(own_user_id)) + .ok() + .flatten() + .map(|m| m.name().to_owned()) + .unwrap_or_else(|| own_user_id.localpart().to_owned()); + + let verification = Verification::new( + own_user_id.into(), + connection.clone(), + members.clone(), + buffer.clone(), + ); + + let room = MatrixRoom { + homeserver: Rc::new(homeserver), + room_id: room_id.into(), + connection: connection.clone(), + config, + prev_batch: Rc::new(RefCell::new( + room.last_prev_batch().map(PrevBatch::Backwards), + )), + own_user_id: own_user_id.into(), + members, + buffer, + verification, + outgoing_messages: MessageQueue::new(), + messages_in_flight: IntMutex::new(), + room, + }; + + let buffer_name = format!("{}.{}", server_name, room_id); + + let buffer_handle = BufferBuilderAsync::new(&buffer_name) + .input_callback(room.clone()) + .close_callback(|_weechat: &Weechat, _buffer: &Buffer| { + // TODO: remove the roombuffer from the server here. + // TODO: leave the room if the plugin isn't unloading. + Ok(()) + }) + .build() + .expect("Can't create new room buffer"); + + let buffer = buffer_handle + .upgrade() + .expect("Can't upgrade newly created buffer"); + + buffer + .add_nicklist_group( + "000|o", + "weechat.color.nicklist_group", + true, + None, + ) + .expect("Can't create nicklist group"); + buffer + .add_nicklist_group( + "001|h", + "weechat.color.nicklist_group", + true, + None, + ) + .expect("Can't create nicklist group"); + buffer + .add_nicklist_group( + "002|v", + "weechat.color.nicklist_group", + true, + None, + ) + .expect("Can't create nicklist group"); + buffer + .add_nicklist_group( + "999|...", + "weechat.color.nicklist_group", + true, + None, + ) + .expect("Can't create nicklist group"); + + buffer.enable_nicklist(); + buffer.disable_nicklist_groups(); + buffer.enable_multiline(); + + buffer.set_localvar("server", server_name); + buffer.set_localvar("nick", &own_nick); + buffer.set_localvar( + "domain", + room.room_id() + .server_name() + .map(|name| name.as_str()) + .unwrap_or_default(), + ); + buffer.set_localvar("room_id", room.room_id().as_str()); + if room.is_direct() { + buffer.set_localvar("type", "private") + } else { + buffer.set_localvar("type", "channel") + } + + if let Some(alias) = room.alias() { + buffer.set_localvar("alias", alias.as_str()); + } + + *room.buffer.inner.borrow_mut() = Some(buffer_handle.clone()); + + Self { inner: room } + } + + pub async fn restore( + server_name: &str, + runtime: Handle, + room: Room, + connection: &Rc>>, + config: Rc>, + homeserver: Url, + ) -> Result { + let room_clone = room.clone(); + let room_id = room.room_id(); + let own_user_id = room.own_user_id(); + let prev_batch = room.last_prev_batch(); + + let room_buffer = Self::new( + server_name, + runtime.clone(), + connection, + config, + room_clone, + homeserver, + room_id, + own_user_id, + ); + + debug!("Restoring room {}", room.room_id()); + + let matrix_members = runtime + .spawn(async move { room.joined_user_ids().await }) + .await + .expect("Couldn't get the joined user ids")?; + + for user_id in matrix_members { + trace!("Restoring member {}", &user_id); + room_buffer.members.restore_member(user_id).await; + } + + *room_buffer.prev_batch.borrow_mut() = + prev_batch.map(PrevBatch::Forward); + + room_buffer.buffer.update_buffer_name(); + room_buffer.buffer.set_topic(); + + Ok(room_buffer) + } +} + +#[async_trait(?Send)] +impl BufferInputCallbackAsync for MatrixRoom { + async fn callback(&mut self, _: BufferHandle, input: String) { + let content = if self.config.borrow().input().markdown_input() { + RoomMessageEventContent::new(MessageType::Text( + TextMessageEventContent::markdown(input), + )) + } else { + RoomMessageEventContent::new(MessageType::Text( + TextMessageEventContent::plain(input), + )) + }; + + self.send_message(content).await; + } +} + +impl MatrixRoom { + pub fn is_encrypted(&self) -> bool { + self.members + .runtime + .block_on(self.room.latest_encryption_state()) + .map(|s| s.is_encrypted()) + .unwrap_or_default() + } + + pub fn contains_only_verified_devices(&self) -> bool { + self.members + .runtime + .block_on(self.room.contains_only_verified_devices()) + .unwrap_or_default() + } + + pub fn is_public(&self) -> bool { + self.room.is_public().unwrap_or_default() + } + + pub fn is_direct(&self) -> bool { + self.members + .runtime + .block_on(self.room.is_direct()) + .unwrap_or_default() + } + + pub fn alias(&self) -> Option { + self.room.canonical_alias() + } + + pub fn room_id(&self) -> &RoomId { + &self.room_id + } + + pub fn buffer_handle(&self) -> BufferHandle { + self.buffer.buffer_handle() + } + + pub fn accept_verification(&self) { + let verification = self.verification.clone(); + Weechat::spawn(async move { verification.accept().await }).detach(); + } + + pub fn confirm_verification(&self) { + let verification = self.verification.clone(); + Weechat::spawn(async move { verification.confirm().await }).detach(); + } + + pub fn cancel_verification(&self) { + todo!() + } + + async fn redact_event(&self, event: &SyncRoomRedactionEvent) { + let event = if let SyncRoomRedactionEvent::Original(e) = event { + e + } else { + // Redacted redaction events don't contain enough data to be applied, so there's + // nothing to do here. + return; + }; + + let buffer_handle = self.buffer_handle(); + + let buffer = if let Ok(b) = buffer_handle.upgrade() { + b + } else { + return; + }; + + // TODO: remove this unwrap. + let redacter = self.members.get(&event.sender).await.unwrap(); + + // TODO: handle unwrapping redacts Option properly for rooms versions 11+ + let event_id_tag = Cow::from(format!( + "{}_id_{}", + PLUGIN_NAME, + event.redacts.clone().unwrap() + )); + let tag = Cow::from("matrix_redacted"); + + let reason = if let Some(r) = &event.content.reason { + format!(", reason: {}", r) + } else { + "".to_owned() + }; + let redaction_message = format!( + "{}<{}Message redacted by: {}{}{}>{}", + Weechat::color("chat_delimiters"), + Weechat::color("logger.color.backlog_line"), + redacter.nick(), + reason, + Weechat::color("chat_delimiters"), + Weechat::color("reset"), + ); + + let redaction_style = self.config.borrow().look().redaction_style(); + + let predicate = |l: &BufferLine| { + let tags = l.tags(); + tags.contains(&event_id_tag) + && !tags.contains(&Cow::from("matrix_redacted")) + }; + + let strike_through = |string: Cow| { + Weechat::remove_color(&string) + .graphemes(true) + .map(|g| format!("{}\u{0336}", g)) + .collect::>() + .join("") + }; + + let redact_first_line = |message: Cow| match redaction_style { + RedactionStyle::Delete => redaction_message.clone(), + RedactionStyle::Notice => { + format!("{} {}", message, redaction_message) + } + RedactionStyle::StrikeThrough => { + format!("{} {}", strike_through(message), redaction_message) + } + }; + + let redact_string = |message: Cow| match redaction_style { + RedactionStyle::Delete => redaction_message.clone(), + RedactionStyle::Notice => { + format!("{} {}", message, redaction_message) + } + RedactionStyle::StrikeThrough => strike_through(message), + }; + + fn modify_line(line: BufferLine, tag: Cow, redaction_func: F) + where + F: Fn(Cow) -> String, + { + let message = line.message(); + let new_message = redaction_func(message); + + let mut tags = line.tags(); + tags.push(tag); + let tags: Vec<&str> = tags.iter().map(|t| t.as_ref()).collect(); + + line.set_message(&new_message); + line.set_tags(&tags); + } + + let mut lines = buffer.lines(); + let first_line = lines.rfind(predicate); + + if let Some(line) = first_line { + modify_line(line, tag.clone(), redact_first_line); + } else { + return; + } + + while let Some(line) = lines.next_back().filter(predicate) { + modify_line(line, tag.clone(), redact_string); + } + } + + async fn render_message_content( + &self, + event_id: &EventId, + send_time: MilliSecondsSinceUnixEpoch, + sender: &WeechatRoomMember, + content: &AnyMessageLikeEventContent, + ) -> Option { + use AnyMessageLikeEventContent::{RoomEncrypted, RoomMessage}; + use MessageType::*; + + let rendered = match content { + RoomEncrypted(c) => { + c.render_with_prefix(send_time, event_id, sender, &()) + } + RoomMessage(c) => match &c.msgtype { + Text(c) => { + c.render_with_prefix(send_time, event_id, sender, &()) + } + Emote(c) => { + c.render_with_prefix(send_time, event_id, sender, sender) + } + Notice(c) => { + c.render_with_prefix(send_time, event_id, sender, sender) + } + ServerNotice(c) => { + c.render_with_prefix(send_time, event_id, sender, sender) + } + Location(c) => { + c.render_with_prefix(send_time, event_id, sender, sender) + } + Audio(c) => c.render_with_prefix( + send_time, + event_id, + sender, + &self.homeserver, + ), + Video(c) => c.render_with_prefix( + send_time, + event_id, + sender, + &self.homeserver, + ), + File(c) => c.render_with_prefix( + send_time, + event_id, + sender, + &self.homeserver, + ), + Image(c) => c.render_with_prefix( + send_time, + event_id, + sender, + &self.homeserver, + ), + _ => return None, + }, + _ => return None, + }; + + Some(rendered) + } + + async fn render_sync_message( + &self, + event: &AnySyncMessageLikeEvent, + ) -> Option { + // TODO: remove this expect. + let sender = + self.members.get(event.sender()).await.expect( + "Rendering a message but the sender isn't in the nicklist", + ); + + if let Some(content) = event.original_content() { + let send_time = event.origin_server_ts(); + self.render_message_content( + event.event_id(), + send_time, + &sender, + &content, + ) + .await + .map(|r| { + // TODO: the tags are different if the room is a DM. + if sender.user_id() == &*self.own_user_id { + r.add_self_tags() + } else { + r.add_msg_tags() + } + }) + } else { + self.render_redacted_event(event).await + } + } + + // Add the content of the message to our outgoing message queue and print out + // a local echo line if local echo is enabled. + async fn queue_outgoing_message( + &self, + transaction_id: &TransactionId, + content: &RoomMessageEventContent, + ) { + if self.config.borrow().look().local_echo() { + if let MessageType::Text(c) = &content.msgtype { + let sender = + self.members.get(&self.own_user_id).await.unwrap_or_else( + || panic!("No own member {}", self.own_user_id), + ); + + let local_echo = c + .render_with_prefix_for_echo(&sender, transaction_id, &()) + .add_self_tags(); + self.buffer.print_rendered_event(local_echo); + + self.outgoing_messages + .add_with_echo(transaction_id.to_owned(), content.clone()); + } else { + self.outgoing_messages + .add(transaction_id.to_owned(), content.clone()); + } + } else { + self.outgoing_messages + .add(transaction_id.to_owned(), content.clone()); + } + } + + /// Send the given content to the server. + /// + /// # Arguments + /// + /// * `content` - The content that should be sent to the server. + /// + /// # Examples + /// + /// ``` + /// let content = MessageEventContent::Text(TextMessageEventContent { + /// body: "Hello world".to_owned(), + /// formatted: None, + /// relates_to: None, + /// }); + /// let content = AnyMessageEventContent::RoomMessage(content); + /// + /// buffer.send_message(content).await + /// ``` + pub async fn send_message(&self, content: RoomMessageEventContent) { + let transaction_id = TransactionId::new(); + + let connection = self.connection.borrow().clone(); + + if let Some(c) = connection { + self.queue_outgoing_message(&transaction_id, &content).await; + match c + .send_message( + self.room().clone(), + AnyMessageLikeEventContent::RoomMessage(content), + Some(transaction_id.clone()), + ) + .await + { + Ok(r) => { + self.handle_outgoing_message(&transaction_id, &r.event_id) + .await; + } + Err(_e) => { + // TODO: print out an error, remember to modify the local + // echo line if there is one. + self.outgoing_messages.remove(&transaction_id); + } + } + } else if let Ok(buffer) = self.buffer_handle().upgrade() { + buffer.print("Error not connected"); + } + } + + /// Send out a typing notice. + /// + /// This will send out a typing notice or reset the one in progress, if + /// needed. It will make sure that only one typing notice request is in + /// flight at a time. + /// + /// Typing notices are sent out only if we have more than 4 letters in the + /// input and the input isn't a command. + /// + /// If the input is empty the typing notice is disabled. + pub fn update_typing_notice(&self) { + let buffer_handle = self.buffer_handle(); + + let buffer = if let Ok(b) = buffer_handle.upgrade() { + b + } else { + return; + }; + + let input = buffer.input(); + + if input.starts_with('/') && !input.starts_with("//") { + // Don't send typing notices for commands. + return; + } + + let connection = self.connection.clone(); + let room = self.room().clone(); + + let send = |typing: bool| async move { + let connection = connection.borrow().clone(); + + if let Some(connection) = connection { + let _ = connection.send_typing_notice(room, typing).await; + }; + }; + + if input.len() < 4 { + // If we have an active typing notice and our input is short, e.g. + // we removed the input set the typing notice to false. + Weechat::spawn(send(false)).detach(); + } else if input.len() >= 4 { + // If we have some valid input and no active typing notice, send + // one out. + Weechat::spawn(send(true)).detach(); + } + } + + pub fn is_busy(&self) -> bool { + self.messages_in_flight.locked() + } + + pub fn reset_prev_batch(&self) { + // TODO: we'll want to be able to scroll up again after we clear the + // buffer. + *self.prev_batch.borrow_mut() = None; + } + + pub async fn get_messages(&self) { + let messages_lock = self.messages_in_flight.clone(); + + let connection = self.connection.borrow().as_ref().cloned(); + + let prev_batch = + if let Some(p) = self.prev_batch.borrow().as_ref().cloned() { + p + } else { + return; + }; + + let guard = if let Ok(l) = messages_lock.try_lock() { + l + } else { + return; + }; + + Weechat::bar_item_update("buffer_modes"); + Weechat::bar_item_update("matrix_modes"); + + if let Some(connection) = connection { + let room = self.room().clone(); + let room_id = room.room_id().to_owned(); + + if let Ok(r) = connection.room_messages(room, prev_batch).await { + for event in + r.chunk.iter().filter_map(|e| e.raw().deserialize().ok()) + { + self.handle_room_event( + &event.into_full_event(room_id.clone()), + ) + .await; + } + + let mut prev_batch = self.prev_batch.borrow_mut(); + + if let Some(PrevBatch::Forward(t)) = prev_batch.as_ref() { + *prev_batch = Some(PrevBatch::Backwards(t.to_owned())); + self.buffer.sort_messages(); + } else if r.chunk.is_empty() { + *prev_batch = None; + } else { + *prev_batch = r.end.map(PrevBatch::Backwards); + self.buffer.sort_messages(); + } + } + } + + drop(guard); + + Weechat::bar_item_update("buffer_modes"); + Weechat::bar_item_update("matrix_modes"); + } + + async fn handle_outgoing_message( + &self, + transaction_id: &TransactionId, + event_id: &EventId, + ) { + if let Some((echo, content)) = + self.outgoing_messages.remove(transaction_id) + { + let event = OriginalSyncMessageLikeEvent { + sender: (*self.own_user_id).to_owned(), + origin_server_ts: MilliSecondsSinceUnixEpoch::now(), + event_id: event_id.to_owned(), + content, + unsigned: Default::default(), + }; + + let event = AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Original(event), + ); + + let rendered = self + .render_sync_message(&event) + .await + .expect("Sent out an event that we don't know how to render"); + + if echo { + self.buffer.replace_local_echo(transaction_id, rendered); + } else { + self.buffer.print_rendered_event(rendered); + } + } + } + + async fn handle_edits(&self, event: &AnySyncMessageLikeEvent) { + // TODO: remove this expect. + let sender = + self.members.get(event.sender()).await.expect( + "Rendering a message but the sender isn't in the nicklist", + ); + + if let Some((event_id, content)) = event.get_edit() { + let send_time = event.origin_server_ts(); + + if let Some(rendered) = self + .render_message_content( + event_id, + send_time, + &sender, + &AnyMessageLikeEventContent::RoomMessage( + content.clone().with_relation(None), + ), + ) + .await + .map(|r| { + // TODO: the tags are different if the room is a DM. + if sender.user_id() == &*self.own_user_id { + r.add_self_tags() + } else { + r.add_msg_tags() + } + }) + { + self.buffer.replace_edit(event_id, event.sender(), rendered); + } + } + } + + async fn handle_room_message(&self, event: &AnySyncMessageLikeEvent) { + // If the event has a transaction id it's an event that we sent out + // ourselves, the content will be in the outgoing message queue and it + // may have been printed out as a local echo. + if let Some(id) = event.transaction_id() { + self.handle_outgoing_message(id, event.event_id()).await; + return; + } + + if let AnySyncMessageLikeEvent::RoomRedaction(r) = event { + self.redact_event(r).await; + } else if event.is_verification() { + self.verification.handle_room_verification(event).await; + } else if event.is_edit() { + self.handle_edits(event).await; + } else if let Some(rendered) = self.render_sync_message(event).await { + self.buffer.print_rendered_event(rendered); + } + } + + async fn render_redacted_event( + &self, + event: &AnySyncMessageLikeEvent, + ) -> Option { + if let AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Redacted(e), + ) = event + { + let redacter = e + .unsigned + .redacted_because + .get_field::("sender") + .ok() + .flatten()?; + let redacter = self.members.get(redacter.as_ref()).await?; + let sender = self.members.get(&e.sender).await?; + + Some(e.render_with_prefix( + e.origin_server_ts, + event.event_id(), + &sender, + &redacter, + )) + } else { + None + } + } + + pub async fn handle_membership_event( + &self, + event: &SyncStateEvent, + state_event: bool, + ambiguity_change: Option<&AmbiguityChange>, + ) { + self.members + .handle_membership_event(event, state_event, ambiguity_change) + .await + } + + fn set_prev_batch(&self) { + if let Ok(buffer) = self.buffer_handle().upgrade() { + if buffer.num_lines() == 0 { + *self.prev_batch.borrow_mut() = + self.room.last_prev_batch().map(PrevBatch::Backwards); + } + } + } + + pub async fn handle_sync_room_event(&self, event: AnySyncTimelineEvent) { + self.set_prev_batch(); + + match &event { + AnySyncTimelineEvent::MessageLike(message) => { + self.handle_room_message(message).await + } + AnySyncTimelineEvent::State(event) => { + self.handle_sync_state_event(event, false).await + } + } + } + + pub async fn handle_room_event(&self, event: &AnyTimelineEvent) { + match &event { + AnyTimelineEvent::MessageLike(event) => { + // TODO: Only print out historical events if they aren't edits of + // other events. + if !event.is_edit() { + let sender = self.members.get(event.sender()).await.expect( + "Rendering a message but the sender isn't in the nicklist", + ); + + let content = if let Some(content) = + event.original_content() + { + content + } else { + tracing::error!("Unhandled redacted event: {event:?}"); + return; + }; + + let send_time = event.origin_server_ts(); + + if let Some(rendered) = self + .render_message_content( + event.event_id(), + send_time, + &sender, + &content, + ) + .await + { + self.buffer.print_rendered_event(rendered); + } + } + } + // TODO: print out state events. + AnyTimelineEvent::State(_) => (), + } + } + + pub fn room(&self) -> &Room { + &self.room + } + + pub async fn handle_sync_state_event( + &self, + event: &AnySyncStateEvent, + _state_event: bool, + ) { + match event { + AnySyncStateEvent::RoomName(_) => self.buffer.update_buffer_name(), + AnySyncStateEvent::RoomTopic(_) => self.buffer.set_topic(), + AnySyncStateEvent::RoomCanonicalAlias(_) => self.buffer.set_alias(), + _ => (), + } + } +} diff --git a/src/room/verification.rs b/src/room/verification.rs new file mode 100644 index 0000000..421017c --- /dev/null +++ b/src/room/verification.rs @@ -0,0 +1,226 @@ +use std::{cell::RefCell, rc::Rc}; + +use matrix_sdk::{ + encryption::verification::{SasVerification, VerificationRequest}, + ruma::{ + events::{ + key::verification::VerificationMethod, room::message::MessageType, + AnySyncMessageLikeEvent, + }, + UserId, + }, +}; + +use crate::{ + connection::Connection, + render::{Render, StartVerificationContext, VerificationContext}, +}; + +use super::{buffer::RoomBuffer, members::Members}; + +#[derive(Clone)] +pub struct Verification { + own_user_id: Rc, + connection: Rc>>, + members: Members, + buffer: RoomBuffer, + inner: Rc>>, +} + +#[derive(Clone, Debug)] +enum ActiveVerification { + Request(VerificationRequest), + Sas(SasVerification), +} + +impl From for ActiveVerification { + fn from(v: VerificationRequest) -> Self { + Self::Request(v) + } +} + +impl From for ActiveVerification { + fn from(v: SasVerification) -> Self { + Self::Sas(v) + } +} + +impl Verification { + pub fn new( + own_user_id: Rc, + connection: Rc>>, + members: Members, + buffer: RoomBuffer, + ) -> Self { + Self { + own_user_id, + connection, + members, + buffer, + inner: Rc::new(RefCell::new(None)), + } + } + + pub async fn confirm(&self) { + let connection = self.connection.borrow().clone(); + + if let Some(c) = connection { + if let Some(ActiveVerification::Sas(verification)) = + self.inner.borrow().clone() + { + let ret = + c.spawn(async move { verification.confirm().await }).await; + } + } + } + + pub async fn accept(&self) { + let connection = self.connection.borrow().clone(); + let verification = self.inner.borrow().clone(); + + if let Some(c) = connection { + if let Some(ActiveVerification::Request(verification)) = + verification + { + let verification_clone = verification.clone(); + + let ret = c + .spawn(async move { + verification + .accept_with_methods(vec![ + VerificationMethod::SasV1, + ]) + .await + }) + .await; + + // We automatically start SAS verification here since it's the + // only method we support. + if let Some(sas) = c + .spawn(async move { verification_clone.start_sas().await }) + .await + .unwrap() + { + *self.inner.borrow_mut() = Some(sas.into()); + } + } + } + } + + pub async fn handle_room_verification( + &self, + event: &AnySyncMessageLikeEvent, + ) { + // TODO remove this expect. + let sender = + self.members.get(event.sender()).await.expect( + "Rendering a message but the sender isn't in the nicklist", + ); + let own_member = self + .members + .get(&self.own_user_id) + .await + .expect("Own member missing from the store"); + let send_time = event.origin_server_ts(); + let connection = self.connection.borrow().clone(); + + match event { + AnySyncMessageLikeEvent::KeyVerificationReady(_) => {} + AnySyncMessageLikeEvent::KeyVerificationStart(e) => { + if let Some(connection) = connection { + let Some(e) = e.as_original() else { + // Unhandled redacted event + return; + }; + let flow_id = &e.content.relates_to.event_id; + + if let Some(sas) = connection + .client() + .encryption() + .get_verification(&e.sender, flow_id.as_str()) + .await + .map(|s| s.sas()) + .flatten() + { + let context = StartVerificationContext::Room( + e.sender.to_owned(), + sas.clone().into(), + ); + let rendered = e.content.render_with_prefix( + send_time, + event.event_id(), + &sender, + &context, + ); + self.buffer + .replace_verification_event(flow_id, rendered); + *self.inner.borrow_mut() = Some(sas.clone().into()); + + // We accept here automatically since the only method + // we're supporting is SAS verification + let ret = connection + .spawn(async move { sas.accept().await }) + .await; + } + } + } + AnySyncMessageLikeEvent::KeyVerificationCancel(_) => { + self.inner.borrow_mut().take(); + } + AnySyncMessageLikeEvent::KeyVerificationAccept(_) => {} + AnySyncMessageLikeEvent::KeyVerificationKey(e) => { + let Some(e) = e.as_original() else { + // Unhandled redacted event + return; + }; + let flow_id = &e.content.relates_to.event_id; + if let Some(ActiveVerification::Sas(sas)) = + self.inner.borrow().clone() + { + if sas.can_be_presented() { + let rendered = e.content.render_with_prefix( + send_time, + event.event_id(), + &sender, + &sas, + ); + self.buffer + .replace_verification_event(flow_id, rendered); + } + } + } + AnySyncMessageLikeEvent::KeyVerificationMac(_) => {} + AnySyncMessageLikeEvent::KeyVerificationDone(_) => {} + AnySyncMessageLikeEvent::RoomMessage(e) => { + let Some(e) = e.as_original() else { + // Unhandled redacted event + return; + }; + if let MessageType::VerificationRequest(content) = + &e.content.msgtype + { + let rendered = content.render_with_prefix( + send_time, + &e.event_id, + &sender.clone(), + &VerificationContext::Room(sender, own_member), + ); + self.buffer.print_rendered_event(rendered); + + if let Some(connection) = connection { + if let Some(verification) = connection + .client() + .encryption() + .get_verification_request(&e.sender, &e.event_id) + .await + { + *self.inner.borrow_mut() = + Some(verification.into()); + } + } + } + } + _ => {} + } + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..bed7d83 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,1368 @@ +//! Matrix server abstraction. +//! +//! A MatrixServer is created for every server the user configures. +//! +//! It will create a per server config subsection. If options are added to the +//! server they need to be removed from the server section when the server is +//! dropped. +//! +//! The server will create a tokio runtime which will spawn a task for the sync +//! loop. +//! +//! It will also spawn a task on the Weechat mainloop, this one waits for +//! responses from the sync loop. +//! +//! A separate task is spawned every time Weechat wants to send a message to the +//! server. +//! +//! +//! Schematically this looks like the following diagram. +//! +//! MatrixServer +//! +--------------------------------------------------------------------+ +//! | | +//! | Weechat mainloop Tokio runtime | +//! | +---------------------------+ +------------------------+ | +//! | | | | | | +//! | | +--------------------+ | | +----------------+ | | +//! | | | | | | | | | | +//! | | | Response receiver +<---------------+ Sync loop | | | +//! | | | | | | | | | | +//! | | | | | | | | | | +//! | | +--------------------+ | | +----------------+ | | +//! | | | | | | +//! | | +--------------------+ | | +----------------+ | | +//! | | | | | Spawn | | | | | +//! | | | Roombuffer input +--------------->+ Send coroutine | | | +//! | | | callback +<---------------+ | | | +//! | | | | | | | | | | +//! | | +--------------------+ | | +----------------+ | | +//! | | | | | | +//! | +---------------------------+ +------------------------+ | +//! | | +//! +--------------------------------------------------------------------+ +//! +//! +//! The tokio runtime and response receiver task will be alive only if the user +//! connects to the server while the room buffer input callback will print an +//! error if the server is disconnected. +//! +//! The server holds all the rooms which in turn hold the buffers, users, and +//! room metadata. +//! +//! The response receiver forwards events to the correct room. The response +//! receiver fetches events individually from a mpsc channel. This makes sure +//! that processing events will not block the Weechat mainloop for too long. + +use chrono::{offset::Utc, DateTime}; +use std::{ + cell::{Ref, RefCell, RefMut}, + cmp::Reverse, + collections::HashMap, + path::PathBuf, + rc::{Rc, Weak}, +}; +use tracing::error; +use url::Url; + +use matrix_sdk::{ + self, + deserialized_responses::AmbiguityChange, + encryption::RoomKeyImportResult, + room::Room, + ruma::{ + api::client::session::login::v3::Response as LoginResponse, + events::{ + room::member::RoomMemberEventContent, AnySyncStateEvent, + AnySyncTimelineEvent, AnyToDeviceEvent, SyncStateEvent, + }, + DeviceId, DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, + OwnedDeviceId, OwnedRoomId, OwnedUserId, RoomId, UserId, + }, + Client, Error, +}; + +use weechat::{ + buffer::{Buffer, BufferBuilder, BufferHandle}, + config::{BooleanOptionSettings, ConfigSection, StringOptionSettings}, + Prefix, Weechat, +}; + +use crate::{ + config::ServerBuffer, + connection::{Connection, InteractiveAuthInfo}, + room::RoomHandle, + verification_buffer::VerificationBuffer, + ConfigHandle, Servers, PLUGIN_NAME, +}; + +#[derive(Debug)] +pub enum ServerError { + StartError(String), + ClientError(matrix_sdk::ClientBuildError), + IoError(String), +} + +#[derive(Debug, Clone, Copy)] +enum DeviceTrust { + Verified, + Unverified, + Unsupported, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ServerSettings { + pub homeserver: Option, + pub proxy: Option, + pub autoconnect: bool, + pub username: String, + pub password: String, + pub ssl_verify: bool, +} + +impl Default for ServerSettings { + fn default() -> Self { + Self { + ssl_verify: true, + proxy: None, + autoconnect: false, + homeserver: None, + username: "".to_owned(), + password: "".to_owned(), + } + } +} + +impl ServerSettings { + pub fn new() -> Self { + Default::default() + } +} + +pub struct LoginInfo { + user_id: OwnedUserId, +} + +#[derive(Clone)] +pub struct MatrixServer { + inner: Rc, +} + +impl std::ops::Deref for MatrixServer { + type Target = InnerServer; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::fmt::Debug for MatrixServer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut fmt = f.debug_struct("MatrixServer"); + fmt.field("name", &self.server_name).finish() + } +} + +pub struct InnerServer { + servers: Servers, + server_name: Rc, + rooms: Rc>>, + settings: Rc>, + current_settings: Rc>, + config: ConfigHandle, + client: Rc>>, + login_state: Rc>>, + connection: Rc>>, + server_buffer: Rc>>, + verification_buffers: Rc>>, +} + +impl MatrixServer { + pub fn new( + name: &str, + config: &ConfigHandle, + server_section: &mut ConfigSection, + servers: Servers, + ) -> Self { + let server_name: Rc = name.to_string().into(); + + let server = InnerServer { + servers, + server_name: server_name.clone(), + rooms: Rc::new(RefCell::new(HashMap::new())), + settings: Rc::new(RefCell::new(ServerSettings::new())), + current_settings: Rc::new(RefCell::new(ServerSettings::new())), + config: config.clone(), + client: Rc::new(RefCell::new(None)), + login_state: Rc::new(RefCell::new(None)), + connection: Rc::new(RefCell::new(None)), + server_buffer: Rc::new(RefCell::new(None)), + verification_buffers: Rc::new(RefCell::new(HashMap::new())), + }; + + let server = server.into(); + + MatrixServer::create_server_conf(&server_name, server_section, &server); + + MatrixServer { inner: server } + } + + pub fn clone_weak(&self) -> Weak { + Rc::downgrade(&self.inner) + } + + pub fn connect(&self) -> Result<(), ServerError> { + if self.connected() { + self.print_error(&format!( + "Already connected to {}{}{}", + Weechat::color("chat_server"), + self.name(), + Weechat::color("reset") + )); + + return Ok(()); + } + + let client = self.get_or_create_client()?; + let connection = Connection::new(self, &client); + self.set_connection(connection); + + self.print_network(&format!( + "Connected to {}{}{}", + Weechat::color("chat_server"), + self.name(), + Weechat::color("reset") + )); + + Ok(()) + } + + fn inner(&self) -> Rc { + self.inner.clone() + } + + pub fn merge_server_buffers(&self) { + let server_buffer = self.inner.server_buffer.borrow_mut(); + + if let Some(buffer) = + server_buffer.as_ref().and_then(|b| b.upgrade().ok()) + { + self.inner.merge_server_buffer(&buffer); + } + } + + /// Parse an URL returning a None if the string is empty. + /// + /// # Panics + /// + /// This panics if the string can't be parsed as an URL. + fn parse_url_unchecked(value: &str) -> Option { + if value.is_empty() { + None + } else { + Some( + Url::parse(value) + .expect("Can't parse URL, did the check callback fail?"), + ) + } + } + + /// Parse an URL returning an error if the parse step fails. + pub fn parse_url(value: String) -> Result<(), String> { + let url = Url::parse(&value); + + match url { + Ok(u) => { + if u.cannot_be_a_base() { + Err(String::from("The Homeserver URL is missing a schema")) + } else { + Ok(()) + } + } + Err(e) => Err(e.to_string()), + } + } + + /// Check if the provided value is a valid URL. + fn is_url_valid(value: &str) -> bool { + if value.is_empty() { + true + } else { + MatrixServer::parse_url(value.to_string()).is_ok() + } + } + + fn create_server_conf( + server_name: &str, + server_section: &mut ConfigSection, + server_ref: &Rc, + ) { + let server = Rc::downgrade(server_ref); + let server_copy = server.clone(); + let autoconnect = + BooleanOptionSettings::new(format!("{}.autoconnect", server_name)) + .set_change_callback(move |_, option| { + let value = option.value(); + + let server_ref = server.upgrade().expect( + "Server got deleted while server config is alive", + ); + + server_ref.settings.borrow_mut().autoconnect = value; + }); + + server_section + .new_boolean_option(autoconnect) + .expect("Can't create autoconnect option"); + + let server = server_copy; + let server_copy = server.clone(); + + let homeserver = + StringOptionSettings::new(format!("{}.homeserver", server_name)) + .set_check_callback(|_, _, value| { + MatrixServer::is_url_valid(&value) + }) + .set_change_callback(move |_, option| { + let server_ref = server.upgrade().expect( + "Server got deleted while server config is alive", + ); + + server_ref.settings.borrow_mut().homeserver = + MatrixServer::parse_url_unchecked(&option.value()); + }); + + server_section + .new_string_option(homeserver) + .expect("Can't create homeserver option"); + + let server = server_copy; + let server_copy = server.clone(); + + let proxy = StringOptionSettings::new(format!("{}.proxy", server_name)) + .set_check_callback(|_, _, value| { + MatrixServer::is_url_valid(&value) + }) + .set_change_callback(move |_, option| { + let server_ref = server + .upgrade() + .expect("Server got deleted while server config is alive"); + + server_ref.settings.borrow_mut().proxy = + MatrixServer::parse_url_unchecked(&option.value()); + }); + + server_section + .new_string_option(proxy) + .expect("Can't create proxy option"); + + let server = server_copy; + let server_copy = server.clone(); + + let username = + StringOptionSettings::new(format!("{}.username", server_name)) + .set_change_callback(move |_, option| { + let server_ref = server.upgrade().expect( + "Server got deleted while server config is alive", + ); + + server_ref.settings.borrow_mut().username = + Weechat::eval_string_expression(&option.value()) + .expect("Can't evaluate username"); + }); + + server_section + .new_string_option(username) + .expect("Can't create username option"); + + let server = server_copy; + let server_copy = server.clone(); + + let password = + StringOptionSettings::new(format!("{}.password", server_name)) + .set_change_callback(move |_, option| { + let server_ref = server.upgrade().expect( + "Server got deleted while server config is alive", + ); + + server_ref.settings.borrow_mut().password = + Weechat::eval_string_expression(&option.value()) + .expect("Can't evaluate password"); + }); + + server_section + .new_string_option(password) + .expect("Can't create password option"); + + let server = server_copy; + + let ssl_verify = + BooleanOptionSettings::new(format!("{}.ssl_verify", server_name)) + .default_value(true) + .set_change_callback(move |_, option| { + let value = option.value(); + + let server_ref = server.upgrade().expect( + "Server got deleted while server config is alive", + ); + + server_ref.settings.borrow_mut().ssl_verify = value; + }); + + server_section + .new_boolean_option(ssl_verify) + .expect("Can't create autoconnect option"); + } +} + +impl Drop for MatrixServer { + fn drop(&mut self) { + // TODO close all the server buffers. + // Only free the server config if it's the only clone of the InnerServer + if Rc::strong_count(&self.inner) == 1 { + let config = &self.config; + let mut config_borrow = config.borrow_mut(); + + let mut section = config_borrow + .search_section_mut("server") + .expect("Can't get server section"); + + for option_name in &[ + "autoconnect", + "homeserver", + "password", + "proxy", + "ssl_verify", + "username", + ] { + let option_name = + &format!("{}.{}", self.server_name, option_name); + section.free_option(option_name).unwrap_or_else(|_| { + panic!("Can't free option {}", option_name) + }); + } + } + } +} + +impl InnerServer { + pub fn name(&self) -> &str { + &self.server_name + } + + pub fn rooms(&self) -> Vec { + self.rooms.borrow().values().cloned().collect() + } + + pub fn verifications(&self) -> Vec { + self.verification_buffers + .borrow() + .values() + .cloned() + .collect() + } + + pub(crate) fn get_or_create_room(&self, room_id: &RoomId) -> RoomHandle { + if !self.rooms.borrow().contains_key(room_id) { + let homeserver = self + .settings + .borrow() + .homeserver + .clone() + .expect("Creating room buffer while no homeserver"); + let login_state = self.login_state.borrow(); + let login_state = login_state + .as_ref() + .expect("Receiving events while not being logged in"); + let client = self.client.borrow(); + let room = client + .as_ref() + .expect("Receiving events without a client") + .get_room(room_id); + + let room = room.unwrap_or_else(|| { + panic!( + "Receiving events for a room while no room found {}", + room_id + ) + }); + let buffer = RoomHandle::new( + &self.server_name, + self.servers.runtime().to_owned(), + &self.connection, + self.config.inner.clone(), + room, + homeserver, + room_id, + &login_state.user_id, + ); + self.rooms.borrow_mut().insert(room_id.to_owned(), buffer); + } + + self.rooms.borrow().get(room_id).cloned().unwrap() + } + + pub fn config(&self) -> ConfigHandle { + self.config.clone() + } + + pub fn user_name(&self) -> String { + self.settings.borrow().username.clone() + } + + pub fn password(&self) -> String { + self.settings.borrow().password.clone() + } + + pub async fn restore_room(&self, room: Room) { + let homeserver = self + .settings + .borrow() + .homeserver + .clone() + .expect("Creating room buffer while no homeserver"); + + match RoomHandle::restore( + &self.server_name, + self.servers.runtime().to_owned(), + room, + &self.connection, + self.config.inner.clone(), + homeserver, + ) + .await + { + Ok(buffer) => { + let room_id = buffer.room_id().to_owned(); + + self.rooms.borrow_mut().insert(room_id, buffer); + } + Err(e) => self.print_error(&format!("Error restoring room: {}", e)), + } + } + + fn create_server_buffer(&self) -> BufferHandle { + let buffer_handle = + BufferBuilder::new(&format!("server.{}", self.server_name)) + .build() + .expect("Can't create Matrix debug buffer"); + + let buffer = buffer_handle + .upgrade() + .expect("Can't upgrade newly created server buffer"); + + let settings = self.settings.borrow(); + + buffer.set_title(&format!( + "Matrix: {}", + settings + .homeserver + .as_ref() + .map(|u| u.to_string()) + .unwrap_or_else(|| self.server_name.to_string()), + )); + buffer.set_short_name(&self.server_name); + buffer.set_localvar("type", "server"); + buffer.set_localvar("nick", &settings.username); + buffer.set_localvar("server", &self.server_name); + + self.merge_server_buffer(&buffer); + + buffer_handle + } + + fn merge_server_buffer(&self, buffer: &Buffer) { + match self.config.borrow().look().server_buffer() { + ServerBuffer::MergeWithCore => { + buffer.unmerge(); + + let core_buffer = buffer.core_buffer(); + buffer.merge(&core_buffer); + } + ServerBuffer::Independent => buffer.unmerge(), + ServerBuffer::MergeWithoutCore => { + let servers = self.servers.borrow(); + + let server = if let Some(server) = servers.values().next() { + server + } else { + return; + }; + + if server.name() == &*self.server_name { + buffer.unmerge(); + } else { + let inner = server.inner(); + + if let Some(Ok(other_buffer)) = + inner.server_buffer().as_ref().map(|b| b.upgrade()) + { + let core_buffer = buffer.core_buffer(); + + buffer.unmerge_to((core_buffer.number() + 1) as u16); + buffer.merge(&other_buffer); + }; + } + } + } + } + + fn get_client(&self) -> Option { + self.client.borrow().clone() + } + + fn get_or_create_client(&self) -> Result { + let client = if let Some(c) = self.get_client() { + c + } else { + self.create_client()? + }; + + // Check if the homeserver setting changed and swap our client if it + // did. + if *self.current_settings.borrow() != *self.settings.borrow() { + // TODO if the homeserver changed close all the room buffers of the + // server here, they don't belong to our client anymore. + self.create_client() + } else { + Ok(client) + } + } + + /// Borrow the server buffer handle. + pub fn server_buffer(&self) -> Ref> { + self.server_buffer.borrow() + } + + fn get_or_create_buffer<'a>( + &self, + server_buffer: &'a mut RefMut>, + ) -> &'a BufferHandle { + if let Some(buffer) = server_buffer.as_ref() { + if buffer.upgrade().is_err() { + let buffer = self.create_server_buffer(); + **server_buffer = Some(buffer); + } + } else { + let buffer = self.create_server_buffer(); + **server_buffer = Some(buffer); + } + + server_buffer.as_ref().unwrap() + } + + /// Print a neutral message to the server buffer. + fn print(&self, message: &str) { + let mut server_buffer = self.server_buffer.borrow_mut(); + let buffer = self + .get_or_create_buffer(&mut server_buffer) + .upgrade() + .unwrap(); + buffer.print(message); + } + + /// Print a message with a given prefix to the server buffer. + pub fn print_with_prefix(&self, prefix: &str, message: &str) { + self.print(&format!("{}{}: {}", prefix, PLUGIN_NAME, message)); + } + + /// Print an network message to the server buffer. + pub fn print_network(&self, message: &str) { + self.print_with_prefix(&Weechat::prefix(Prefix::Network), message); + } + + /// Print an error message to the server buffer. + pub fn print_error(&self, message: &str) { + self.print_with_prefix(&Weechat::prefix(Prefix::Error), message); + } + + /// Is the server connected. + pub fn connected(&self) -> bool { + self.connection.borrow().is_some() + } + + pub async fn receive_to_device_event(&self, event: AnyToDeviceEvent) { + let handle_event = |event, transaction_id: String| async move { + if let Some(b) = + self.verification_buffers.borrow().get(&transaction_id) + { + b.handle_event(event).await; + } + }; + + match &event { + AnyToDeviceEvent::RoomKey(_) => {} + AnyToDeviceEvent::RoomKeyRequest(_) => {} + AnyToDeviceEvent::KeyVerificationRequest(e) => { + if let Some(client) = self.get_client() { + if let Some(request) = client + .encryption() + .get_verification_request( + &e.sender, + &e.content.transaction_id, + ) + .await + { + let buffer = VerificationBuffer::new( + &self.server_name, + &e.sender, + request, + self.connection.clone(), + ); + buffer.handle_event(&event).await; + self.verification_buffers.borrow_mut().insert( + e.content.transaction_id.to_string(), + buffer, + ); + } + } + } + AnyToDeviceEvent::KeyVerificationStart(e) => { + if let Some(client) = self.get_client() { + use matrix_sdk::encryption::verification::Verification; + match client + .encryption() + .get_verification( + &e.sender, + e.content.transaction_id.as_str(), + ) + .await + { + Some(Verification::SasV1(sas)) => { + if !sas.is_cancelled() { + let buffer = self + .verification_buffers + .borrow() + .get(e.content.transaction_id.as_str()) + .cloned(); + + if let Some(mut buffer) = buffer { + buffer.update(sas).await; + buffer.handle_event(&event).await; + } else { + let buffer = VerificationBuffer::new( + &self.server_name, + &e.sender, + sas, + self.connection.clone(), + ); + buffer.handle_event(&event).await; + self.verification_buffers + .borrow_mut() + .insert( + e.content + .transaction_id + .to_string(), + buffer, + ); + } + } + } + Some(Verification::QrV1(qr)) => { + if let Some(buffer) = self + .verification_buffers + .borrow_mut() + .get_mut(e.content.transaction_id.as_str()) + { + buffer.update_qr(qr).await; + buffer.handle_event(&event).await; + } + } + Some(_) => unreachable!(), + None => todo!(), + } + } + } + AnyToDeviceEvent::KeyVerificationCancel(e) => { + handle_event(&event, e.content.transaction_id.to_string()) + .await; + } + AnyToDeviceEvent::KeyVerificationAccept(e) => { + handle_event(&event, e.content.transaction_id.to_string()).await + } + AnyToDeviceEvent::KeyVerificationKey(e) => { + handle_event(&event, e.content.transaction_id.to_string()).await + } + AnyToDeviceEvent::KeyVerificationMac(e) => { + handle_event(&event, e.content.transaction_id.to_string()).await + } + _ => {} + } + } + + pub async fn receive_member( + &self, + room_id: OwnedRoomId, + member: SyncStateEvent, + is_state: bool, + ambiguity_change: Option, + ) { + let room = self.rooms.borrow().get(&room_id).cloned(); + + if let Some(room) = room { + room.handle_membership_event( + &member, + is_state, + ambiguity_change.as_ref(), + ) + .await; + } else { + error!("Room with id {} not found.", room_id); + } + } + + pub async fn receive_joined_state_event( + &self, + room_id: &RoomId, + event: AnySyncStateEvent, + ) { + let room = self.get_or_create_room(room_id); + room.handle_sync_state_event(&event, true).await + } + + pub async fn receive_joined_timeline_event( + &self, + room_id: &RoomId, + event: AnySyncTimelineEvent, + ) { + let room = self.get_or_create_room(room_id); + room.handle_sync_room_event(event).await + } + + pub fn receive_login(&self, response: LoginResponse) { + let login_state = LoginInfo { + user_id: response.user_id, + }; + + *self.login_state.borrow_mut() = Some(login_state); + } + + fn create_server_dir(&self) -> std::io::Result<()> { + let path = self.get_server_path(); + std::fs::create_dir_all(path) + } + + pub fn get_server_path(&self) -> PathBuf { + let mut path = Weechat::home_dir(); + let server_name: &str = &self.server_name; + path.push("matrix-rust"); + path.push(server_name); + + path + } + + pub fn connection(&self) -> Option { + self.connection.borrow().clone() + } + + fn set_connection(&self, connection: Connection) { + *self.connection.borrow_mut() = Some(connection); + } + + pub fn create_client(&self) -> Result { + let settings = self.settings.borrow(); + + let homeserver = settings.homeserver.as_ref().ok_or_else(|| { + ServerError::StartError("Homeserver not configured".to_owned()) + })?; + + self.create_server_dir().map_err(|e| { + ServerError::IoError(format!( + "Error creating the session dir: {}", + e + )) + })?; + + let mut client_builder = Client::builder() + .homeserver_url(homeserver) + .sqlite_store(self.get_server_path(), Some("DEFAULT_PASSPHRASE")); + + if let Some(proxy) = settings.proxy.as_ref() { + client_builder = client_builder.proxy(proxy); + } + + if !settings.ssl_verify { + client_builder = client_builder.disable_ssl_verification(); + } + + let client: Client = self + .servers + .runtime() + .block_on(client_builder.build()) + .map_err(ServerError::ClientError)?; + + *self.current_settings.borrow_mut() = settings.clone(); + *self.client.borrow_mut() = Some(client.clone()); + + Ok(client) + } + + pub async fn delete_devices(&self, devices: Vec) { + let formatted = devices + .iter() + .map(|d| d.to_string()) + .collect::>() + .join(", "); + + let print_success = || { + self.print_network(&format!( + "Successfully deleted device(s) {}", + formatted + )); + }; + + let print_fail = |e| { + self.print_error(&format!( + "Error deleting device(s) {} {:#?}", + formatted, e + )); + }; + + if let Some(c) = self.connection() { + match c.delete_devices(devices.clone(), None).await { + Ok(_) => print_success(), + Err(e) => { + if let Some(info) = e.as_uiaa_response() { + let auth_info = { + let settings = self.settings.borrow(); + InteractiveAuthInfo { + user: settings.username.clone(), + password: settings.password.clone(), + session: info.session.clone(), + } + }; + + if let Err(e) = c + .delete_devices(devices.clone(), Some(auth_info)) + .await + { + print_fail(e); + } else { + print_success(); + } + } else { + print_fail(e) + } + } + } + }; + } + + pub async fn export_keys(&self, file: PathBuf, passphrase: String) { + let client = self.get_client().unwrap(); + + let export = async move { + client + .encryption() + .export_room_keys(file, &passphrase, |_| true) + .await + }; + + if let Some(c) = self.connection() { + if let Err(e) = c.spawn(export).await { + self.print_error(&format!( + "Error exporting E2EE keys {:#?}", + e + )); + } else { + self.print_network("Successfully exported E2EE keys") + } + }; + } + + pub async fn import_keys(&self, file: PathBuf, passphrase: String) { + let client = self.get_client().unwrap(); + + if let Some(c) = self.connection() { + self.print_network(&format!( + "Importing E2EE keys from {}, this may take a while..", + file.display() + )); + let import = async move { + client + .encryption() + .import_room_keys(file, &passphrase) + .await + }; + + match c.spawn(import).await { + Ok(RoomKeyImportResult { + imported_count, + total_count, + .. + }) => { + if imported_count > 0 { + self.print_network(&format!( + "Successfully imported {} E2EE keys", + imported_count + )); + } else if total_count > 0 { + self.print_network( + "No keys were imported, the key export contains only \ + keys that we already have", + ); + } else { + self.print_network( + "No keys were imported, either the key export is empty" + ); + } + } + Err(e) => { + self.print_error(&format!( + "Error importing E2EE keys {:#?}", + e + )); + } + } + }; + } + + async fn list_own_devices( + &self, + connection: Connection, + ) -> Result<(), Error> { + let client = connection.client(); + let mut response = connection.devices().await?; + + if response.devices.is_empty() { + self.print_error("No devices were found for this server"); + return Ok(()); + } + + self.print_network(&format!( + "Devices for server {}{}{}:", + Weechat::color("chat_server"), + self.name(), + Weechat::color("reset") + )); + + response.devices.sort_by_key(|d| Reverse(d.last_seen_ts)); + let own_device_id = client.device_id(); + let own_user_id = client + .session_meta() + .map(|s| s.user_id.to_owned()) + .expect("Getting our own devices while not being logged in"); + + let mut lines: Vec = Vec::new(); + + for device_info in response.devices { + let client = client.clone(); + let own_user_id = own_user_id.clone(); + let device_info_move = device_info.clone(); + let device = match connection + .spawn(async move { + client + .clone() + .encryption() + .get_device(&own_user_id, &device_info_move.device_id) + .await + }) + .await + { + Ok(d) => d, + Err(e) => { + self.print_error(&format!("Failed to obtain device: {e}")); + continue; + } + }; + + let own_device = own_device_id == Some(&device_info.device_id); + + let device_trust = if own_device { + DeviceTrust::Verified + } else { + device + .as_ref() + .map(|d| { + if d.is_verified() { + DeviceTrust::Verified + } else { + DeviceTrust::Unverified + } + }) + .unwrap_or(DeviceTrust::Unsupported) + }; + + let info = Self::format_device( + &device_info.device_id, + device.and_then(|d| { + d.get_key(DeviceKeyAlgorithm::Ed25519) + .map(|f| f.to_base64()) + }), + device_info.display_name.as_deref(), + own_device, + device_trust, + device_info.last_seen_ip, + device_info.last_seen_ts, + ); + + lines.push(info); + } + + let line = lines.join("\n"); + self.print(&line); + + Ok(()) + } + + async fn list_other_devices( + &self, + connection: Connection, + user_id: &UserId, + ) -> Result<(), Error> { + let devices = connection + .client() + .encryption() + .get_user_devices(user_id) + .await?; + + let lines: Vec<_> = devices + .devices() + .map(|device| { + let device_trust = if device.is_verified() { + DeviceTrust::Verified + } else { + DeviceTrust::Unverified + }; + + Self::format_device( + device.device_id(), + device + .get_key(DeviceKeyAlgorithm::Ed25519) + .map(|f| f.to_base64()), + device.display_name(), + false, + device_trust, + None, + None, + ) + }) + .collect(); + + let user_color = Weechat::info_get("nick_color_name", user_id.as_str()) + .expect("Can't get user color"); + + if lines.is_empty() { + self.print_error(&format!( + "No devices were found for user {}{}{} on this server", + Weechat::color(&user_color), + user_id.as_str(), + Weechat::color("reset"), + )); + } else { + self.print_network(&format!( + "Devices for user {}{}{} on server {}{}{}:", + Weechat::color(&user_color), + user_id.as_str(), + Weechat::color("reset"), + Weechat::color("chat_server"), + self.name(), + Weechat::color("reset") + )); + + let line = lines.join("\n"); + self.print(&line); + } + + Ok(()) + } + + fn format_device( + device_id: &DeviceId, + fingerprint: Option, + display_name: Option<&str>, + is_own_device: bool, + device_trust: DeviceTrust, + last_seen_ip: Option, + last_seen_ts: Option, + ) -> String { + let device_color = + Weechat::info_get("nick_color_name", device_id.as_str()) + .expect("Can't get device color"); + + let last_seen_date = last_seen_ts + .and_then(|d| { + d.to_system_time().map(|d| { + let date: DateTime = d.into(); + date.format("%Y/%m/%d %H:%M").to_string() + }) + }) + .unwrap_or_else(|| "?".to_string()); + + let last_seen = format!( + "{} @ {}", + last_seen_ip.as_deref().unwrap_or("-"), + last_seen_date + ); + + let (bold, color) = if is_own_device { + (Weechat::color("bold"), format!("*{}", device_color)) + } else { + ("", device_color) + }; + + let verified = match device_trust { + DeviceTrust::Verified => { + format!( + "{}Trusted{}", + Weechat::color("green"), + Weechat::color("reset") + ) + } + DeviceTrust::Unverified => { + format!( + "{}Not trusted{}", + Weechat::color("red"), + Weechat::color("reset") + ) + } + DeviceTrust::Unsupported => { + format!( + "{}No encryption support{}", + Weechat::color("darkgray"), + Weechat::color("reset") + ) + } + }; + + let fingerprint = if let Some(fingerprint) = fingerprint { + let fingerprint = fingerprint + .chars() + .collect::>() + .chunks(4) + .map(|c| c.iter().collect::()) + .collect::>() + .join(" "); + + format!( + "{}{}{}", + Weechat::color("magenta"), + fingerprint, + Weechat::color("reset") + ) + } else { + format!( + "{}-{}", + Weechat::color("darkgray"), + Weechat::color("reset") + ) + }; + + format!( + " \ + Name: {}{}\n \ + Device ID: {}{}{}\n \ + Security: {}\n\ + Fingerprint: {}\n \ + Last seen: {}\n", + bold, + display_name.unwrap_or(""), + Weechat::color(&color), + device_id.as_str(), + Weechat::color("reset"), + verified, + fingerprint, + last_seen, + ) + } + + pub async fn devices(&self, user_id: Option) { + let connection = if let Some(c) = self.connection() { + c + } else { + self.print_error("You must be connected to execute this command"); + return; + }; + + let ret = if let Some(user_id) = user_id.as_ref() { + if Some(user_id.as_ref()) == connection.client().user_id() { + self.list_own_devices(connection).await + } else { + self.list_other_devices(connection, user_id).await + } + } else { + self.list_own_devices(connection).await + }; + + if let Err(e) = ret { + self.print_error(&format!("Error fetching devices {:?}", e)); + } + } + + pub fn autoconnect(&self) -> bool { + self.settings.borrow().autoconnect + } + + pub fn is_connection_secure(&self) -> bool { + let settings = self.current_settings.borrow(); + + settings.ssl_verify + && settings + .homeserver + .as_ref() + .map(|u| u.scheme() == "https") + .unwrap_or(false) + } + + pub fn disconnect(&self) { + if !self.connected() { + self.print_error(&format!( + "Not connected to {}{}{}", + Weechat::color("chat_server"), + self.name(), + Weechat::color("reset") + )); + + return; + } + + { + let mut connection = self.connection.borrow_mut(); + connection.take(); + } + + self.print_network(&format!( + "Disconnected from {}{}{}", + Weechat::color("chat_server"), + self.name(), + Weechat::color("reset") + )); + } + + pub fn get_info_str(&self, details: bool) -> String { + let mut s = String::from(&format!( + "{}{}{} [{}]", + Weechat::color("chat_server"), + self.server_name.as_ref().to_owned(), + Weechat::color("reset"), + if self.connected() { + "connected" + } else { + "not connected" + } + )); + + if !details { + return s; + } + + let settings = self.settings.borrow(); + s.push_str(&format!( + "\n\ + {:indent$}homeserver: {}\n\ + {:indent$}proxy: {}\n\ + {:indent$}autoconnect: {}\n\ + {:indent$}username: {}\n", + "", + settings.homeserver.as_ref().map_or("", |url| url.as_str()), + "", + settings.proxy.as_ref().map_or("", |url| url.as_str()), + "", + settings.autoconnect, + "", + settings.username, + indent = 8 + )); + s + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..d9d2b91 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,122 @@ +use matrix_sdk::ruma::{ + events::{ + room::message::{ + MessageType, Relation, RoomMessageEventContent, + RoomMessageEventContentWithoutRelation, + }, + AnyMessageLikeEvent, AnySyncMessageLikeEvent, SyncMessageLikeEvent, + }, + EventId, UserId, +}; + +pub trait ToTag { + fn to_tag(&self) -> String; +} + +impl ToTag for EventId { + fn to_tag(&self) -> String { + format!("matrix_id_{}", self.as_str()) + } +} + +impl ToTag for UserId { + fn to_tag(&self) -> String { + format!("matrix_sender_{}", self.as_str()) + } +} + +pub trait Edit { + fn is_edit(&self) -> bool; + fn get_edit( + &self, + ) -> Option<(&EventId, &RoomMessageEventContentWithoutRelation)>; +} + +pub trait VerificationEvent { + fn is_verification(&self) -> bool; +} + +impl VerificationEvent for AnySyncMessageLikeEvent { + fn is_verification(&self) -> bool { + match self { + AnySyncMessageLikeEvent::KeyVerificationReady(_) + | AnySyncMessageLikeEvent::KeyVerificationStart(_) + | AnySyncMessageLikeEvent::KeyVerificationCancel(_) + | AnySyncMessageLikeEvent::KeyVerificationAccept(_) + | AnySyncMessageLikeEvent::KeyVerificationKey(_) + | AnySyncMessageLikeEvent::KeyVerificationMac(_) + | AnySyncMessageLikeEvent::KeyVerificationDone(_) => true, + AnySyncMessageLikeEvent::RoomMessage(m) => { + if let SyncMessageLikeEvent::Original(m) = m { + if let MessageType::VerificationRequest(_) = + m.content.msgtype + { + return true; + } + } + false + } + _ => false, + } + } +} + +impl Edit for RoomMessageEventContent { + fn is_edit(&self) -> bool { + matches!(self.relates_to.as_ref(), Some(Relation::Replacement(_))) + } + + fn get_edit( + &self, + ) -> Option<(&EventId, &RoomMessageEventContentWithoutRelation)> { + if let Some(Relation::Replacement(r)) = self.relates_to.as_ref() { + Some((&r.event_id, &r.new_content)) + } else { + None + } + } +} + +impl Edit for AnySyncMessageLikeEvent { + fn is_edit(&self) -> bool { + if let AnySyncMessageLikeEvent::RoomMessage(e) = self { + e.as_original() + .map(|e| e.content.is_edit()) + .unwrap_or_default() + } else { + false + } + } + + fn get_edit( + &self, + ) -> Option<(&EventId, &RoomMessageEventContentWithoutRelation)> { + if let AnySyncMessageLikeEvent::RoomMessage(e) = self { + e.as_original().and_then(|e| e.content.get_edit()) + } else { + None + } + } +} + +impl Edit for AnyMessageLikeEvent { + fn is_edit(&self) -> bool { + if let AnyMessageLikeEvent::RoomMessage(c) = self { + c.as_original() + .map(|e| e.content.is_edit()) + .unwrap_or_default() + } else { + false + } + } + + fn get_edit( + &self, + ) -> Option<(&EventId, &RoomMessageEventContentWithoutRelation)> { + if let AnyMessageLikeEvent::RoomMessage(e) = self { + e.as_original().and_then(|e| e.content.get_edit()) + } else { + None + } + } +} diff --git a/src/verification_buffer.rs b/src/verification_buffer.rs new file mode 100644 index 0000000..f6d957e --- /dev/null +++ b/src/verification_buffer.rs @@ -0,0 +1,370 @@ +use std::{cell::RefCell, convert::TryInto, rc::Rc}; + +use weechat::{ + buffer::{ + Buffer, BufferBuilderAsync, BufferCloseCallback, BufferHandle, + BufferInputCallbackAsync, + }, + Weechat, +}; + +use qrcode::render::unicode::Dense1x2; + +use matrix_sdk::{ + async_trait, + encryption::verification::{ + QrVerification, SasVerification, Verification as SdkVerification, + VerificationRequest, + }, + ruma::{ + events::{ + key::verification::{ + key::ToDeviceKeyVerificationKeyEventContent, VerificationMethod, + }, + AnyToDeviceEvent, + }, + UserId, + }, + Error, +}; + +use crate::{ + connection::Connection, + render::{ + Render, RenderedContent, StartVerificationContext, VerificationContext, + }, +}; + +#[derive(Clone)] +pub struct VerificationBuffer { + inner: InnerVerificationBuffer, + buffer: BufferHandle, +} + +#[derive(Clone, Debug)] +pub enum Verification { + Request(VerificationRequest), + Sas(SasVerification), + Qr(QrVerification), +} + +impl TryInto for Verification { + type Error = (); + + fn try_into(self) -> Result { + match self { + Verification::Request(_) => Err(()), + Verification::Sas(s) => Ok(s.into()), + Verification::Qr(qr) => Ok(qr.into()), + } + } +} + +impl From for Verification { + fn from(s: SasVerification) -> Self { + Self::Sas(s) + } +} + +impl From for Verification { + fn from(v: VerificationRequest) -> Self { + Self::Request(v) + } +} + +impl From for Verification { + fn from(v: QrVerification) -> Self { + Self::Qr(v) + } +} + +impl Verification { + async fn accept(&self) -> Result<(), Error> { + match self { + Verification::Request(r) => r.accept().await, + Verification::Sas(s) => s.accept().await, + Verification::Qr(qr) => qr.confirm().await, + } + } + + async fn generate_qr_code(&self) -> Option { + match self { + Verification::Request(r) => r.generate_qr_code().await.unwrap(), + Verification::Sas(_) => None, + Verification::Qr(_) => None, + } + } + + async fn cancel(&self) -> Result<(), Error> { + match self { + Verification::Request(r) => r.cancel().await, + Verification::Sas(s) => s.cancel().await, + Verification::Qr(qr) => qr.cancel().await, + } + } +} + +#[derive(Clone)] +struct InnerVerificationBuffer { + verification: Rc>, + connection: Rc>>, +} + +impl InnerVerificationBuffer { + fn print_done(&self, buffer: BufferHandle) {} + + pub async fn accept(&self, buffer: BufferHandle) -> Result<(), Error> { + if let Some(c) = self.connection.borrow().clone() { + let verification = self.verification.borrow().clone(); + let verification_clone = verification.clone(); + + c.spawn(async move { verification_clone.accept().await }) + .await?; + + if let Verification::Request(request) = verification { + if request + .their_supported_methods() + .unwrap_or_default() + .contains(&VerificationMethod::QrCodeShowV1) + { + if let Some(qr_code) = request.generate_qr_code().await? { + if let Ok(code) = qr_code.to_qr_code() { + if let Ok(b) = buffer.upgrade() { + let string = code + .render::() + .light_color(Dense1x2::Dark) + .dark_color(Dense1x2::Light) + .build(); + b.print(&string); + } + } + } + } else if let Some(sas) = + c.spawn(async move { request.start_sas().await }).await? + { + *self.verification.borrow_mut() = sas.into(); + } + } + } + + Ok(()) + } + + pub async fn confirm(&self, buffer: BufferHandle) -> Result<(), Error> { + if let Some(c) = self.connection.borrow().clone() { + if let Verification::Sas(s) = self.verification.borrow().clone() { + let sas = s.clone(); + c.spawn(async move { s.confirm().await }).await?; + + if sas.is_done() { + self.print_done(buffer); + } + } else if let Verification::Qr(qr) = + self.verification.borrow().clone() + { + c.spawn(async move { qr.confirm().await }).await?; + + // if qr.is_done() { + // self.print_done(buffer); + // } + } else if let Ok(b) = buffer.upgrade() { + b.print("Error, can't confirm the verification yet"); + } + } + + Ok(()) + } + + pub async fn cancel(&self) -> Result<(), Error> { + if let Some(c) = self.connection.borrow().clone() { + let verification = self.verification.borrow().clone(); + c.spawn(async move { verification.cancel().await }).await?; + } + + Ok(()) + } + + async fn handle_input( + &mut self, + buffer: BufferHandle, + input: &str, + ) -> Result<(), Error> { + if input == "accept" { + self.accept(buffer).await?; + } else if input == "confirm" { + self.confirm(buffer).await?; + } else if input == "cancel" { + self.cancel().await?; + } + + Ok(()) + } +} + +#[async_trait(?Send)] +impl BufferInputCallbackAsync for InnerVerificationBuffer { + async fn callback(&mut self, buffer: BufferHandle, input: String) { + if let Err(e) = self.handle_input(buffer.clone(), &input).await { + if let Ok(buffer) = buffer.upgrade() { + buffer.print(&format!( + "Error with the verification flow {:?}", + e + )); + } + } + } +} + +impl BufferCloseCallback for InnerVerificationBuffer { + fn callback(&mut self, _: &Weechat, _: &Buffer) -> Result<(), ()> { + let inner = self.clone(); + Weechat::spawn(async move { inner.cancel().await }).detach(); + + Ok(()) + } +} + +impl VerificationBuffer { + pub fn new( + server_name: &str, + sender: &UserId, + verification: impl Into, + connection: Rc>>, + ) -> Self { + let inner = InnerVerificationBuffer { + verification: Rc::new(RefCell::new(verification.into())), + connection, + }; + + let buffer_name = format!("{}.verification", server_name); + + let buffer_handle = BufferBuilderAsync::new(&buffer_name) + .input_callback(inner.clone()) + .close_callback(|_weechat: &Weechat, _buffer: &Buffer| { + // TODO remove the roombuffer from the server here. + // TODO leave the room if the plugin isn't unloading. + Ok(()) + }) + .build() + .expect("Can't create new room buffer"); + + let buffer = buffer_handle + .upgrade() + .expect("Can't upgrade newly created buffer"); + + buffer.disable_nicklist(); + buffer.disable_nicklist_groups(); + buffer.enable_multiline(); + + buffer.set_localvar("server", server_name); + + Self { + inner, + buffer: buffer_handle, + } + } + + pub fn buffer(&self) -> BufferHandle { + self.buffer.clone() + } + + pub fn accept(&self) { + let buffer = self.buffer(); + let inner = self.inner.clone(); + Weechat::spawn(async move { inner.accept(buffer).await }).detach(); + } + + pub fn cancel(&self) { + let inner = self.inner.clone(); + Weechat::spawn(async move { inner.cancel().await }).detach(); + } + + pub fn confirm(&self) { + let buffer = self.buffer(); + let inner = self.inner.clone(); + Weechat::spawn(async move { inner.confirm(buffer).await }).detach(); + } + + pub async fn update_qr(&mut self, qr: QrVerification) { + *self.inner.verification.borrow_mut() = qr.into(); + } + + pub async fn update(&mut self, sas: SasVerification) -> Result<(), Error> { + *self.inner.verification.borrow_mut() = sas.into(); + let verification = self.inner.verification.borrow().clone(); + + if let Some(c) = self.inner.connection.borrow().clone() { + c.spawn(async move { verification.accept().await }).await?; + } else { + // TODO print an error + } + + Ok(()) + } + + pub async fn handle_event(&self, event: &AnyToDeviceEvent) { + match event { + AnyToDeviceEvent::KeyVerificationRequest(e) => { + if let Verification::Request(request) = + self.inner.verification.borrow().clone() + { + let content = e + .content + .render(&VerificationContext::ToDevice(request)); + + self.print(&content); + } + } + AnyToDeviceEvent::KeyVerificationStart(e) => { + let verification = self.inner.verification.borrow().clone(); + + if let Ok(verification) = verification.try_into() { + let content = + e.content.render(&StartVerificationContext::ToDevice( + e.sender.clone(), + verification, + )); + + self.print(&content); + } + } + AnyToDeviceEvent::KeyVerificationCancel(_) => { + // let message = + // format!("The verification flow has been canceled"); + // self.print(&message); + } + AnyToDeviceEvent::KeyVerificationKey(e) => { + self.print_sas(&e.content); + } + AnyToDeviceEvent::KeyVerificationMac(_) => { + if let Verification::Sas(sas) = + self.inner.verification.borrow().clone() + { + if sas.is_done() { + self.inner.print_done(self.buffer.clone()); + } + } + } + AnyToDeviceEvent::KeyVerificationDone(_) => {} + _ => {} + } + } + + fn print(&self, message: &RenderedContent) { + if let Ok(buffer) = self.buffer.upgrade() { + for line in &message.lines { + buffer.print_date_tags(0, &[], &line.message); + } + } else { + Weechat::print("BUFFER CLOSED"); + } + } + + fn print_sas(&self, content: &ToDeviceKeyVerificationKeyEventContent) { + if let Verification::Sas(sas) = self.inner.verification.borrow().clone() + { + let message = content.render(&sas); + self.print(&message); + } + } +}