From 05e2355615950efe32afccf6ac7e4b49f860171a Mon Sep 17 00:00:00 2001 From: y1lm0z Date: Sat, 4 Apr 2026 01:33:50 +0300 Subject: [PATCH] first commit --- .gitignore | 2 + .rustfmt.toml | 1 + .travis.yml | 29 + Cargo.lock | 4916 ++++++++++++++++++++++++++++++++ Cargo.toml | 44 + LICENSE | 14 + Makefile | 29 + README.md | 71 + src/bar_items/buffer_name.rs | 58 + src/bar_items/buffer_plugin.rs | 38 + src/bar_items/mod.rs | 29 + src/bar_items/status.rs | 54 + src/commands/buffer_clear.rs | 39 + src/commands/devices.rs | 151 + src/commands/keys.rs | 130 + src/commands/matrix.rs | 330 +++ src/commands/me.rs | 44 + src/commands/mod.rs | 69 + src/commands/page_up.rs | 44 + src/commands/verification.rs | 117 + src/completions.rs | 110 + src/config.rs | 274 ++ src/connection.rs | 569 ++++ src/debug.rs | 60 + src/lib.rs | 315 ++ src/render.rs | 1025 +++++++ src/room/buffer.rs | 274 ++ src/room/members.rs | 422 +++ src/room/mod.rs | 1039 +++++++ src/room/verification.rs | 226 ++ src/server.rs | 1368 +++++++++ src/utils.rs | 122 + src/verification_buffer.rs | 370 +++ 33 files changed, 12383 insertions(+) create mode 100644 .gitignore create mode 100644 .rustfmt.toml create mode 100644 .travis.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 src/bar_items/buffer_name.rs create mode 100644 src/bar_items/buffer_plugin.rs create mode 100644 src/bar_items/mod.rs create mode 100644 src/bar_items/status.rs create mode 100644 src/commands/buffer_clear.rs create mode 100644 src/commands/devices.rs create mode 100644 src/commands/keys.rs create mode 100644 src/commands/matrix.rs create mode 100644 src/commands/me.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/page_up.rs create mode 100644 src/commands/verification.rs create mode 100644 src/completions.rs create mode 100644 src/config.rs create mode 100644 src/connection.rs create mode 100644 src/debug.rs create mode 100644 src/lib.rs create mode 100644 src/render.rs create mode 100644 src/room/buffer.rs create mode 100644 src/room/members.rs create mode 100644 src/room/mod.rs create mode 100644 src/room/verification.rs create mode 100644 src/server.rs create mode 100644 src/utils.rs create mode 100644 src/verification_buffer.rs 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); + } + } +}