From 2e04fd25424748eb711d4288a706a1cc2c93f63c Mon Sep 17 00:00:00 2001 From: osmaczko <33099791+osmaczko@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:15:06 +0200 Subject: [PATCH] feat(chat-cli): wire up logos-delivery transport and switch to client API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the direct use of `conversations::Context` with `client::ChatClient`, which is the intended public API for library consumers. Remove `MessageEnvelope` and the username-keyed session model. The envelope was never part of the wire protocol — sender identity was only tracked in the CLI's local state. Chats are now keyed by conversation ID; add `/nickname` as the user-facing replacement for named sessions. Add a logos-delivery (Waku) transport alongside the existing file transport. The active transport is selected at compile time: set `LOGOS_DELIVERY_LIB_DIR` to link liblogosdelivery, otherwise the file transport is used. Add logos-delivery as a Nix flake input and expose `.#logos-delivery` so the library can be built with `nix build` and referenced by `LOGOS_DELIVERY_LIB_DIR`. CI: rename `c-ffi-smoketest` to `smoketest`; add logos-delivery build step and a `--smoketest` invocation of chat-cli to verify startup. --- .github/workflows/ci.yml | 31 +- Cargo.lock | 725 +++++++++++++++--- README.md | 19 + bin/chat-cli/Cargo.toml | 12 +- bin/chat-cli/README.md | 168 ++-- bin/chat-cli/build.rs | 20 + bin/chat-cli/src/app.rs | 497 +++++------- bin/chat-cli/src/main.rs | 228 ++++-- bin/chat-cli/src/transport.rs | 142 +--- bin/chat-cli/src/transport/file.rs | 136 ++++ bin/chat-cli/src/transport/logos_delivery.rs | 303 ++++++++ .../src/transport/logos_delivery/sys.rs | 102 +++ .../src/transport/logos_delivery/wrapper.rs | 227 ++++++ bin/chat-cli/src/ui.rs | 59 +- flake.lock | 107 ++- flake.nix | 12 +- 16 files changed, 2073 insertions(+), 715 deletions(-) create mode 100644 bin/chat-cli/build.rs create mode 100644 bin/chat-cli/src/transport/file.rs create mode 100644 bin/chat-cli/src/transport/logos_delivery.rs create mode 100644 bin/chat-cli/src/transport/logos_delivery/sys.rs create mode 100644 bin/chat-cli/src/transport/logos_delivery/wrapper.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1f19d5..b817f1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,13 +37,26 @@ jobs: - run: rustup component add rustfmt - run: cargo fmt --all -- --check - c-ffi-smoketest: - name: C FFI Smoketest - runs-on: ubuntu-latest + smoketest: + name: Smoketest + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - run: rustup update stable && rustup default stable + - uses: cachix/install-nix-action@v31 + with: + nix_version: 2.34.6 + extra_nix_config: | + experimental-features = nix-command flakes + - uses: nix-community/cache-nix-action@v6 + with: + primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.nix', 'flake.lock') }} + restore-prefixes-first-match: nix-${{ runner.os }}- - name: Install valgrind + if: runner.os == 'Linux' run: sudo apt-get install -y valgrind - name: Build C FFI example run: make @@ -52,8 +65,15 @@ jobs: run: ./c-client working-directory: crates/client-ffi/examples/message-exchange - name: Run C FFI smoketest under valgrind + if: runner.os == 'Linux' run: make valgrind working-directory: crates/client-ffi/examples/message-exchange + - name: Build logos-delivery + run: nix build .#logos-delivery + - name: Build chat-cli (logos-delivery) + run: LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli + - name: Run chat-cli smoketest + run: ./target/release/chat-cli --name ci-test --smoketest nix-build: name: Nix Build @@ -65,6 +85,11 @@ jobs: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v31 with: + nix_version: 2.34.6 extra_nix_config: | experimental-features = nix-command flakes + - uses: nix-community/cache-nix-action@v6 + with: + primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.nix', 'flake.lock') }} + restore-prefixes-first-match: nix-${{ runner.os }}- - run: nix build --print-build-logs diff --git a/Cargo.lock b/Cargo.lock index c1a3e4e..e763180 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,15 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -25,10 +34,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "anyhow" -version = "1.0.100" +name = "anstream" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arboard" @@ -46,7 +105,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "x11rb", ] @@ -70,9 +129,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake2" @@ -127,9 +186,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.58" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -171,12 +230,16 @@ version = "0.1.0" dependencies = [ "anyhow", "arboard", + "base64", + "clap", + "client", "crossterm 0.29.0", - "hex", - "libchat", "ratatui", "serde", "serde_json", + "thiserror", + "tracing", + "tracing-subscriber", ] [[package]] @@ -210,6 +273,46 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "client" version = "0.1.0" @@ -238,6 +341,12 @@ dependencies = [ "error-code", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "compact_str" version = "0.8.1" @@ -313,7 +422,7 @@ dependencies = [ "document-features", "mio", "parking_lot", - "rustix 1.1.3", + "rustix 1.1.4", "signal-hook", "signal-hook-mio", "winapi", @@ -384,7 +493,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -407,7 +516,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -418,7 +527,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -439,7 +548,7 @@ checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -461,7 +570,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -502,7 +611,7 @@ dependencies = [ "chacha20poly1305", "chat-sqlite", "hkdf", - "rand 0.9.3", + "rand 0.9.4", "rand_core 0.6.4", "serde", "storage", @@ -613,9 +722,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" @@ -634,7 +743,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -700,7 +809,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "windows-link", ] @@ -723,10 +832,23 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "half" version = "2.7.1" @@ -751,9 +873,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "hashlink" @@ -794,6 +916,12 @@ dependencies = [ "digest", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -816,12 +944,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -852,18 +982,24 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "inventory" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -888,6 +1024,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.185" @@ -934,9 +1082,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" @@ -985,10 +1133,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d" [[package]] -name = "memchr" -version = "2.7.6" +name = "matchers" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" @@ -1022,6 +1179,15 @@ dependencies = [ "pxfm", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1106,9 +1272,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -1118,18 +1290,18 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl-src" -version = "300.5.5+3.5.5" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -1173,6 +1345,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkcs8" version = "0.10.2" @@ -1185,9 +1363,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "png" @@ -1233,10 +1411,20 @@ dependencies = [ ] [[package]] -name = "proc-macro-crate" -version = "3.4.0" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -1270,14 +1458,14 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "quick-error" @@ -1287,9 +1475,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1301,10 +1489,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -1313,9 +1507,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -1389,6 +1583,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rusqlite" version = "0.35.0" @@ -1427,14 +1638,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -1476,7 +1687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f25be5ba5f319542edb31925517e0380245ae37df50a9752cdbc05ef948156" dependencies = [ "macro_rules_attribute", - "prettyplease", + "prettyplease 0.1.25", "proc-macro2", "quote", "syn 1.0.109", @@ -1490,9 +1701,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1521,7 +1732,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1554,6 +1765,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" +[[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" @@ -1653,7 +1873,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "rand 0.8.5", + "rand 0.8.6", "syn 1.0.109", ] @@ -1696,7 +1916,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1718,9 +1938,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1729,14 +1949,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -1757,7 +1977,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", ] [[package]] @@ -1776,18 +2005,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", "toml_datetime", @@ -1797,24 +2026,85 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] -name = "typenum" -version = "1.19.0" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -1845,6 +2135,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uninit" version = "0.5.1" @@ -1870,6 +2166,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1890,11 +2198,54 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] @@ -1937,7 +2288,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -1955,14 +2315,31 @@ 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", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1971,42 +2348,84 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2014,10 +2433,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.7.14" +name = "windows_x86_64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] @@ -2027,6 +2452,94 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease 0.2.37", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease 0.2.37", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "with_builtin_macros" @@ -2055,7 +2568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "gethostname", - "rustix 1.1.3", + "rustix 1.1.4", "x11rb-protocol", ] @@ -2087,7 +2600,7 @@ dependencies = [ "derive_more 0.99.20", "ed25519", "ed25519-dalek", - "rand 0.8.5", + "rand 0.8.6", "sha2", "x25519-dalek", "zeroize", @@ -2095,22 +2608,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2130,7 +2643,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] diff --git a/README.md b/README.md index 62066bc..186e1ed 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # libchat Supporting library for Logos-chat + +## Example app + +[`bin/chat-cli`](bin/chat-cli/) is an end-to-end encrypted CLI chat app +built on this library. It uses [logos-delivery](https://github.com/logos-messaging/logos-delivery) +(Waku-based) as the transport so two users anywhere in the world can chat by +sharing an intro bundle. + +```sh +# Build logos-delivery with Nix +nix build .#logos-delivery +# Build chat-cli with Cargo +LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli +# Run binary +./target/release/chat-cli --name alice +``` + +See [`bin/chat-cli/README.md`](bin/chat-cli/README.md) for full build, +run, and test instructions. diff --git a/bin/chat-cli/Cargo.toml b/bin/chat-cli/Cargo.toml index 6b8ca6e..8a7b784 100644 --- a/bin/chat-cli/Cargo.toml +++ b/bin/chat-cli/Cargo.toml @@ -8,11 +8,19 @@ name = "chat-cli" path = "src/main.rs" [dependencies] -libchat = { path = "../../core/conversations" } +client = { path = "../../crates/client" } + ratatui = "0.29" crossterm = "0.29" +clap = { version = "4", features = ["derive"] } anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -hex = "0.4" arboard = "3" +base64 = "0.22" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(logos_delivery)'] } diff --git a/bin/chat-cli/README.md b/bin/chat-cli/README.md index 5ae5367..470b16b 100644 --- a/bin/chat-cli/README.md +++ b/bin/chat-cli/README.md @@ -1,117 +1,109 @@ -# Chat CLI +# chat-cli -A terminal chat application based on libchat library. +A terminal chat application built on top of libchat. End-to-end encrypted messaging in your terminal. -## Features +## Building -- End-to-end encrypted messaging using libchat -- File-based transport for local simulation (no network required) -- Persistent storage (SQLite + JSON state) -- Multiple chat support with chat switching +### With logos-delivery transport (recommended) -## Usage - -Run two instances with different usernames in separate terminals: - -### Terminal 1 (Alice) +[logos-delivery](https://github.com/logos-messaging/logos-delivery) is exposed as a Nix package. +Build it once, then point `LOGOS_DELIVERY_LIB_DIR` at the result: ```bash -cargo run -p chat-cli -- alice +nix build .#logos-delivery +LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli ``` -### Terminal 2 (Bob) +The binary lands at `target/release/chat-cli`. + +### File transport only (no Nix required) ```bash -cargo run -p chat-cli -- bob +cargo build --release -p chat-cli ``` -### Establishing a Connection +## Transports -1. In Alice's terminal, type `/intro` to generate an introduction bundle -2. Copy the intro string -3. In Bob's terminal, type `/connect alice ` (paste Alice's intro bundle) -4. Bob can now send messages to Alice -5. Alice will see Bob's initial "Hello!" message and can reply +| Transport | Description | +|-----------|-------------| +| File (default) | Shared directory; no network needed — great for local testing | +| logos-delivery | Embedded Waku node on the logos.dev network | -### Commands +The transport is selected automatically at compile time: if `LOGOS_DELIVERY_LIB_DIR` is set when building, logos-delivery is used; otherwise the file transport is used. + +## Quick start (file transport) + +Run two instances in separate terminals, pointing at the same data directory: + +```bash +# Terminal 1 +cargo run -p chat-cli -- --name alice + +# Terminal 2 +cargo run -p chat-cli -- --name bob +``` + +### Establishing a connection + +1. In Alice's terminal, type `/intro` — the bundle is copied to your clipboard automatically. +2. In Bob's terminal, type `/connect `. +3. Bob's "Hello!" message appears in Alice's terminal. Both can now chat. + +## logos-delivery transport + +After building with `LOGOS_DELIVERY_LIB_DIR` set, run: + +```bash +./target/release/chat-cli --name alice +``` + +Optional flags: + +| Flag | Default | Description | +|------|---------|-------------| +| `--db ` | *(ephemeral)* | SQLite file for persistent identity across restarts | +| `--preset ` | `logos.dev` | Network preset (`logos.dev` or `twn`) | +| `--port ` | `60000` | TCP port for the embedded logos-delivery node | +| `--log-file ` | *(stderr, off)* | Write logs to a file instead of stderr | + +## Commands | Command | Description | |---------|-------------| | `/help` | Show available commands | -| `/intro` | Generate and display your introduction bundle | -| `/connect ` | Connect to a user using their introduction bundle | -| `/chats` | List all your established chats | -| `/switch ` | Switch to a different chat | -| `/delete ` | Delete a chat (removes session and crypto state) | -| `/peers` | List transport-level peers (users with inbox directories) | -| `/status` | Show connection status and your address | +| `/intro` | Generate your introduction bundle (copies to clipboard) | +| `/connect ` | Connect to a user using their introduction bundle | +| `/chats` | List all established chats | +| `/switch ` | Switch active chat | +| `/delete ` | Delete a chat session | +| `/status` | Show identity and connection info | | `/clear` | Clear current chat's message history | -| `/quit` or `Esc` or `Ctrl+C` | Exit the application | +| `/quit` · `Esc` · `Ctrl+C` | Exit | -#### `/peers` vs `/chats` +## Storage (file transport) -- **`/peers`**: Shows users whose CLI has been started (have inbox directories). These are potential contacts you *could* message. -- **`/chats`**: Shows users you have an **encrypted session** with (via `/connect`). These are active conversations. +All data lives under `tmp/chat-cli-data/` by default (override with `--data`): -### Sending Messages +| Path | Contents | +|------|----------| +| `.db` | SQLite — identity keys, ratchet state, chat metadata (encrypted) | +| `_state.json` | UI state — message history, active chat | +| `transport//` | Inbox directory watched for incoming messages | -Simply type your message and press Enter. Messages are automatically encrypted and delivered via file-based transport. - -## How It Works - -### File-Based Transport - -Messages are passed between users via files in a shared directory: - -1. Each user has an "inbox" directory at `tmp/chat-cli-data/transport//` -2. When Alice sends a message to Bob, it's written as a JSON file in Bob's inbox -3. Bob's client watches for new files and processes incoming messages -4. Files are deleted after processing - -### Storage - -Data is stored in the `tmp/chat-cli-data/` directory: - -| File | Purpose | -|------|---------| -| `.db` | SQLite database for identity keys, inbox keys, chat metadata, and Double Ratchet state | -| `_state.json` | CLI state: username↔chat mappings, message history, active chat | -| `transport//` | Inbox directory for receiving messages | - -The sqlite tables can be viewed with app `DB Browser for SQLite`, password is `123456`, config use `SQLCipher 4 defaults`. - -## Example Session - -``` -# Terminal 1 (Alice) -$ cargo run -p chat-cli -- alice - -/intro -# Output: logos_chatintro_abc123 - -# Terminal 2 (Bob) -$ cargo run -p chat-cli -- bob - -/connect alice logos_chatintro_abc123 -# Connected! Bob sends "Hello!" automatically - -# Now type messages in either terminal to chat! - -# To see your chats: -/chats -# Output: alice (active) - -# To switch between chats (if you have multiple): -/switch alice -``` +The SQLite database can be inspected with *DB Browser for SQLite*: password `chat-cli`, cipher `SQLCipher 4 defaults`. ## Architecture ``` -chat-cli/ +bin/chat-cli/ ├── src/ -│ ├── main.rs # Entry point -│ ├── app.rs # Application state and logic -│ ├── transport.rs # File-based message transport -│ └── ui.rs # Ratatui terminal UI +│ ├── main.rs entry point, CLI arg parsing, transport selection +│ ├── app.rs application state and command handling +│ ├── ui.rs ratatui terminal UI +│ ├── utils.rs shared helpers +│ ├── transport.rs module declarations +│ └── transport/ +│ ├── file.rs file-based transport +│ └── logos_delivery.rs logos-delivery (Waku) transport + FFI +└── build.rs links liblogosdelivery when LOGOS_DELIVERY_LIB_DIR is set ``` diff --git a/bin/chat-cli/build.rs b/bin/chat-cli/build.rs new file mode 100644 index 0000000..a7fa686 --- /dev/null +++ b/bin/chat-cli/build.rs @@ -0,0 +1,20 @@ +fn main() { + println!("cargo::rustc-check-cfg=cfg(logos_delivery)"); + println!("cargo:rerun-if-env-changed=LOGOS_DELIVERY_LIB_DIR"); + + let Ok(lib_dir) = std::env::var("LOGOS_DELIVERY_LIB_DIR") else { + return; + }; + + println!("cargo:rustc-cfg=logos_delivery"); + println!("cargo:rustc-link-search=native={lib_dir}"); + println!("cargo:rustc-link-lib=dylib=logosdelivery"); + + // Set rpath so the binary finds the shared library at runtime. + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + match target_os.as_str() { + "macos" => println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}"), + "linux" => println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}"), + other => panic!("unsupported OS for logos-delivery transport: {other}"), + } +} diff --git a/bin/chat-cli/src/app.rs b/bin/chat-cli/src/app.rs index 43a54c2..77bf2bd 100644 --- a/bin/chat-cli/src/app.rs +++ b/bin/chat-cli/src/app.rs @@ -1,17 +1,15 @@ -//! Chat application logic. - use std::collections::HashMap; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::sync::mpsc; -use anyhow::{Context, Result}; +use anyhow::Result; use arboard::Clipboard; -use libchat::{ChatStorage, Context as ChatManager, Introduction, StorageConfig}; +use client::{ConversationIdOwned, DeliveryService}; use serde::{Deserialize, Serialize}; -use crate::{transport::FileTransport, utils::now}; +use crate::utils::now; -/// A chat message for display. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DisplayMessage { pub from_self: bool, @@ -19,87 +17,68 @@ pub struct DisplayMessage { pub timestamp: u64, } -/// Metadata for a chat session (persisted). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatSession { pub chat_id: String, - pub remote_user: String, + pub nickname: Option, pub messages: Vec, } -/// App state that gets persisted. +impl ChatSession { + /// Human-readable label: nickname if set, otherwise the first 8 chars of the chat ID. + pub fn display_name(&self) -> &str { + self.nickname + .as_deref() + .unwrap_or_else(|| &self.chat_id[..8.min(self.chat_id.len())]) + } +} + #[derive(Debug, Default, Serialize, Deserialize)] pub struct AppState { - /// Map from remote username to chat session. + /// Keyed by chat_id (conversation ID). pub chats: HashMap, - /// Currently active chat (remote username). + /// Holds the active chat_id. pub active_chat: Option, } -/// The chat application state. -pub struct ChatApp { - /// The logos-chat manager. - pub manager: ChatManager, - /// File-based transport for message passing. - pub transport: FileTransport, - /// Our introduction bundle (to share with others). - pub intro_bundle: Option>, - /// Persisted app state. +pub struct ChatApp { + pub client: client::ChatClient, + inbound: mpsc::Receiver>, pub state: AppState, - /// Global messages (shown when no active chat). - pub global_messages: Vec, - /// Input buffer. + /// Ephemeral command output — not persisted, cleared on chat switch. + command_output: Vec, pub input: String, - /// Status message. pub status: String, - /// Our user name. pub user_name: String, - /// Path to state file. state_path: PathBuf, } -impl ChatApp { - /// Create a new chat application. - pub fn new(user_name: &str, data_dir: &PathBuf) -> Result { - // Create database path - let db_path = data_dir.join(format!("{}.db", user_name)); - std::fs::create_dir_all(data_dir)?; +impl ChatApp { + pub fn new( + client: client::ChatClient, + inbound: mpsc::Receiver>, + user_name: &str, + data_dir: &Path, + ) -> Result { + fs::create_dir_all(data_dir)?; - // Open or create the chat manager with file-based storage - let manager = ChatManager::new_from_store( - user_name, - ChatStorage::new(StorageConfig::Encrypted { - path: db_path.to_string_lossy().to_string(), - key: "123456".to_string(), - })?, - ) - .context("Failed to open ChatManager")?; - - // Create file-based transport - let transport = - FileTransport::new(user_name, data_dir).context("Failed to create file transport")?; - - // Load persisted state - let state_path = data_dir.join(format!("{}_state.json", user_name)); + let state_path = data_dir.join(format!("{user_name}_state.json")); let state = Self::load_state(&state_path); - // Count existing chats let chat_count = state.chats.len(); let status = if chat_count > 0 { format!( - "Welcome back, {}! {} chat(s) loaded. Type /help for commands.", - user_name, chat_count + "Welcome back, {user_name}! {chat_count} chat(s) loaded. Type /help for commands." ) } else { - format!("Welcome, {}! Type /help for commands.", user_name) + format!("Welcome, {user_name}! Type /help for commands.") }; Ok(Self { - manager, - transport, - intro_bundle: None, + client, + inbound, state, - global_messages: Vec::new(), + command_output: Vec::new(), input: String::new(), status, user_name: user_name.to_string(), @@ -107,8 +86,7 @@ impl ChatApp { }) } - /// Load state from file. - fn load_state(path: &PathBuf) -> AppState { + fn load_state(path: &Path) -> AppState { if path.exists() && let Ok(contents) = fs::read_to_string(path) && let Ok(state) = serde_json::from_str(&contents) @@ -118,14 +96,12 @@ impl ChatApp { AppState::default() } - /// Save state to file. fn save_state(&self) -> Result<()> { let json = serde_json::to_string_pretty(&self.state)?; fs::write(&self.state_path, json)?; Ok(()) } - /// Get the current chat session (if any). pub fn current_session(&self) -> Option<&ChatSession> { self.state .active_chat @@ -133,135 +109,90 @@ impl ChatApp { .and_then(|name| self.state.chats.get(name)) } - /// Get the current messages to display. pub fn messages(&self) -> Vec<&DisplayMessage> { - if let Some(session) = self.current_session() { - session.messages.iter().collect() - } else { - // Show global messages when no active chat - self.global_messages.iter().collect() - } + let chat = self + .current_session() + .map(|s| s.messages.as_slice()) + .unwrap_or(&[]); + chat.iter().chain(self.command_output.iter()).collect() } - /// Create and display our introduction bundle. - pub fn create_intro(&mut self) -> Result { - let intro = self.manager.create_intro_bundle()?; - let bundle_string = String::from_utf8_lossy(&intro).to_string(); - self.intro_bundle = Some(intro); - self.status = "Introduction bundle created. Share it with others!".to_string(); - Ok(bundle_string) + fn set_active_chat(&mut self, chat_id: Option) { + self.state.active_chat = chat_id; + self.command_output.clear(); } - /// Connect to another user using their introduction bundle. - pub fn connect(&mut self, remote_user: &str, bundle_str: &str) -> Result<()> { - // Check if we already have a chat with this user - if self.state.chats.contains_key(remote_user) { - return Err(anyhow::anyhow!( - "Already have a chat with {}. Use /switch {} to switch to it.", - remote_user, - remote_user - )); + /// Find a chat_id by nickname (exact) or chat_id prefix. + fn resolve_chat_id(&self, query: &str) -> Option<&str> { + // Exact nickname match first. + if let Some((id, _)) = self + .state + .chats + .iter() + .find(|(_, s)| s.nickname.as_deref() == Some(query)) + { + return Some(id.as_str()); } + // Fall back to chat_id prefix. + self.state + .chats + .keys() + .find(|id| id.starts_with(query)) + .map(String::as_str) + } - let intro = Introduction::try_from(bundle_str.as_bytes()) - .map_err(|e| anyhow::anyhow!("Invalid bundle: {:?}", e))?; + pub fn process_incoming(&mut self) -> Result<()> { + while let Ok(payload) = self.inbound.try_recv() { + match self.client.receive(&payload) { + Ok(Some(content)) => { + let chat_id = &content.conversation_id; - let (chat_id, envelopes) = self - .manager - .create_private_convo(&intro, "👋 Hello!".as_bytes())?; + if !self.state.chats.contains_key(chat_id) && content.is_new_convo { + let session = ChatSession { + chat_id: chat_id.clone(), + nickname: None, + messages: Vec::new(), + }; + self.state.chats.insert(chat_id.clone(), session); + let label = chat_id[..8.min(chat_id.len())].to_string(); + self.set_active_chat(Some(chat_id.clone())); + self.status = format!("New chat ({label})! Use /nickname to name it."); + } - // Send the envelopes via file transport - for envelope in envelopes { - self.transport.send(remote_user, envelope.data)?; + if !content.data.is_empty() { + let text = String::from_utf8_lossy(&content.data).to_string(); + if let Some(session) = self.state.chats.get_mut(chat_id) { + session.messages.push(DisplayMessage { + from_self: false, + content: text, + timestamp: now(), + }); + } + } + + self.save_state()?; + } + Ok(None) => {} + Err(e) => tracing::warn!("receive error: {e:?}"), + } } - - // Create new session - let mut session = ChatSession { - chat_id: chat_id.clone().to_string(), - remote_user: remote_user.to_string(), - messages: Vec::new(), - }; - session.messages.push(DisplayMessage { - from_self: true, - content: "👋 Hello!".to_string(), - timestamp: now(), - }); - - self.state.chats.insert(remote_user.to_string(), session); - self.state.active_chat = Some(remote_user.to_string()); - self.save_state()?; - - self.status = format!("Connected to {}!", remote_user); Ok(()) } - /// Switch to a different chat. - pub fn switch_chat(&mut self, remote_user: &str) -> Result<()> { - if self.state.chats.contains_key(remote_user) { - self.state.active_chat = Some(remote_user.to_string()); - self.save_state()?; - self.status = format!("Switched to chat with {}", remote_user); - Ok(()) - } else { - Err(anyhow::anyhow!( - "No chat with {}. Use /chats to list available chats.", - remote_user - )) - } - } - - /// Delete a chat session. - pub fn delete_chat(&mut self, remote_user: &str) -> Result<()> { - if let Some(_session) = self.state.chats.remove(remote_user) { - // TODO delete not implemented in libchat - // Also delete from the library's storage - // if let Err(e) = self.manager.delete_chat(&session.chat_id) { - // // Log but don't fail - the CLI state is already updated - // self.status = format!("Warning: failed to delete crypto state: {}", e); - // } - - // If we deleted the active chat, clear it - if self.state.active_chat.as_deref() == Some(remote_user) { - // Switch to another chat if available, otherwise clear - self.state.active_chat = self.state.chats.keys().next().cloned(); - } - - self.save_state()?; - self.status = format!("Deleted chat with {}", remote_user); - Ok(()) - } else { - Err(anyhow::anyhow!( - "No chat with {}. Use /chats to list available chats.", - remote_user - )) - } - } - - /// Send a message in the current chat. pub fn send_message(&mut self, content: &str) -> Result<()> { - let active = self + let chat_id = self .state .active_chat .clone() .ok_or_else(|| anyhow::anyhow!("No active chat. Use /connect or /switch first."))?; - let session = self - .state - .chats - .get(&active) - .ok_or_else(|| anyhow::anyhow!("Chat session not found"))?; + let convo_id: ConversationIdOwned = chat_id.as_str().into(); - let chat_id = session.chat_id.clone(); - let remote_user = session.remote_user.clone(); + self.client + .send_message(&convo_id, content.as_bytes()) + .map_err(|e| anyhow::anyhow!("{e:?}"))?; - let envelopes = self.manager.send_content(&chat_id, content.as_bytes())?; - - for envelope in envelopes { - self.transport.send(&remote_user, envelope.data)?; - } - - // Update messages - if let Some(session) = self.state.chats.get_mut(&active) { + if let Some(session) = self.state.chats.get_mut(&chat_id) { session.messages.push(DisplayMessage { from_self: true, content: content.to_string(), @@ -273,77 +204,14 @@ impl ChatApp { Ok(()) } - /// Process incoming messages from transport. - pub fn process_incoming(&mut self) -> Result<()> { - while let Some(envelope) = self.transport.try_recv() { - self.handle_incoming_envelope(&envelope)?; - } - Ok(()) - } - - /// Handle an incoming envelope. - fn handle_incoming_envelope( - &mut self, - envelope: &crate::transport::MessageEnvelope, - ) -> Result<()> { - match self.manager.handle_payload(&envelope.data) { - Ok(content) => { - let from_user = &envelope.from; - let content = content.ok_or(anyhow::anyhow!("Convo not exist"))?; - let chat_id = content.conversation_id.clone(); - - // Find or create session for this user - if !self.state.chats.contains_key(from_user) { - // New chat from someone - let session = ChatSession { - chat_id: chat_id.clone(), - remote_user: from_user.clone(), - messages: Vec::new(), - }; - self.state.chats.insert(from_user.clone(), session); - self.state.active_chat = Some(from_user.clone()); - self.status = format!("New chat from {}!", from_user); - } - - let message = String::from_utf8_lossy(&content.data).to_string(); - if !message.is_empty() - && let Some(session) = self.state.chats.get_mut(from_user) - { - session.messages.push(DisplayMessage { - from_self: false, - content: message, - timestamp: envelope.timestamp, - }); - } - - self.save_state()?; - } - Err(e) => { - self.status = format!("Error: {}", e); - } - } - Ok(()) - } - - /// Add a system message to the current chat (for display only). fn add_system_message(&mut self, content: &str) { - let msg = DisplayMessage { + self.command_output.push(DisplayMessage { from_self: true, content: content.to_string(), timestamp: now(), - }; - - if let Some(active) = &self.state.active_chat.clone() - && let Some(session) = self.state.chats.get_mut(active) - { - session.messages.push(msg); - return; - } - // No active chat - add to global messages - self.global_messages.push(msg); + }); } - /// Handle a command (starts with /). pub fn handle_command(&mut self, cmd: &str) -> Result> { let parts: Vec<&str> = cmd.splitn(2, ' ').collect(); let command = parts[0]; @@ -353,91 +221,153 @@ impl ChatApp { "/help" => { self.add_system_message("── Commands ──"); self.add_system_message("/intro - Show your introduction bundle"); - self.add_system_message("/connect - Connect to a user"); + self.add_system_message("/connect - Connect using a bundle"); + self.add_system_message("/nickname - Name the active chat"); self.add_system_message("/chats - List all chats"); - self.add_system_message("/switch - Switch to chat with user"); - self.add_system_message("/delete - Delete chat with user"); - self.add_system_message("/peers - List transport peers"); + self.add_system_message("/switch - Switch active chat"); + self.add_system_message("/delete - Delete a chat"); self.add_system_message("/status - Show connection status"); self.add_system_message("/clear - Clear current chat messages"); self.add_system_message("/quit or Esc or Ctrl+C - Exit"); Ok(Some("Help displayed".to_string())) } "/intro" => { - let bundle = self.create_intro()?; + let bundle_bytes = self + .client + .create_intro_bundle() + .map_err(|e| anyhow::anyhow!("{e:?}"))?; + let bundle_str = String::from_utf8_lossy(&bundle_bytes).to_string(); self.add_system_message("── Your Introduction Bundle ──"); - self.add_system_message(&bundle); - let clipboard_msg = match Clipboard::new().and_then(|mut cb| cb.set_text(&bundle)) { - Ok(()) => "Bundle copied to clipboard! Paste with Cmd+V in /connect.", + self.add_system_message(&bundle_str); + let clipboard_msg = match Clipboard::new() + .and_then(|mut cb| cb.set_text(&bundle_str)) + { + Ok(()) => "Bundle copied to clipboard! Share it, then /connect their bundle.", Err(_) => "Share this bundle with others to connect!", }; self.add_system_message(clipboard_msg); - Ok(Some("Bundle created and copied to clipboard".to_string())) + Ok(Some("Bundle created".to_string())) } "/connect" => { - let connect_parts: Vec<&str> = args.splitn(2, ' ').collect(); - if connect_parts.len() < 2 { - return Ok(Some("Usage: /connect ".to_string())); + if args.is_empty() { + return Ok(Some("Usage: /connect ".to_string())); } - let remote_user = connect_parts[0]; - let bundle = connect_parts[1]; - self.connect(remote_user, bundle)?; - Ok(Some(format!("Connected to {}", remote_user))) + let initial = format!("Hello from {}!", self.user_name); + let convo_id = self + .client + .create_conversation(args.as_bytes(), initial.as_bytes()) + .map_err(|e| anyhow::anyhow!("{e:?}"))?; + + let chat_id = convo_id.to_string(); + let label = chat_id[..8.min(chat_id.len())].to_string(); + let mut session = ChatSession { + chat_id: chat_id.clone(), + nickname: None, + messages: Vec::new(), + }; + session.messages.push(DisplayMessage { + from_self: true, + content: initial, + timestamp: now(), + }); + self.state.chats.insert(chat_id.clone(), session); + self.set_active_chat(Some(chat_id)); + self.save_state()?; + self.status = format!("Connected ({label})! Use /nickname to name this chat."); + Ok(Some(format!("Connected ({label})"))) + } + "/nickname" => { + if args.is_empty() { + return Ok(Some("Usage: /nickname ".to_string())); + } + let chat_id = self + .state + .active_chat + .clone() + .ok_or_else(|| anyhow::anyhow!("No active chat."))?; + let session = self + .state + .chats + .get_mut(&chat_id) + .ok_or_else(|| anyhow::anyhow!("Chat session not found."))?; + session.nickname = Some(args.to_string()); + self.save_state()?; + self.status = format!("Chat named '{args}'."); + Ok(Some(format!("Nickname set to '{args}'"))) } "/chats" => { - let chat_names: Vec<_> = self.state.chats.keys().cloned().collect(); - if chat_names.is_empty() { + let sessions: Vec<_> = self.state.chats.values().cloned().collect(); + if sessions.is_empty() { Ok(Some("No chats yet. Use /connect to start one.".to_string())) } else { - self.add_system_message(&format!("── Your Chats ({}) ──", chat_names.len())); - for name in &chat_names { - let marker = if Some(name) == self.state.active_chat.as_ref() { + self.add_system_message(&format!("── Your Chats ({}) ──", sessions.len())); + for s in &sessions { + let marker = if self.state.active_chat.as_deref() == Some(&s.chat_id) { " (active)" } else { "" }; - self.add_system_message(&format!(" • {}{}", name, marker)); + let label = format!( + " • {} ({}){marker}", + s.display_name(), + &s.chat_id[..8.min(s.chat_id.len())] + ); + self.add_system_message(&label); } - Ok(Some(format!("{} chat(s)", chat_names.len()))) + Ok(Some(format!("{} chat(s)", sessions.len()))) } } "/switch" => { if args.is_empty() { - return Ok(Some("Usage: /switch ".to_string())); + return Ok(Some("Usage: /switch ".to_string())); } - self.switch_chat(args)?; - Ok(Some(format!("Switched to {}", args))) + let chat_id = self + .resolve_chat_id(args) + .map(str::to_string) + .ok_or_else(|| anyhow::anyhow!("No chat matching '{args}'."))?; + let label = self.state.chats[&chat_id].display_name().to_string(); + self.set_active_chat(Some(chat_id)); + self.save_state()?; + self.status = format!("Switched to '{label}'."); + Ok(Some(format!("Switched to '{label}'"))) } "/delete" => { if args.is_empty() { - return Ok(Some("Usage: /delete ".to_string())); + return Ok(Some("Usage: /delete ".to_string())); } - self.delete_chat(args)?; - Ok(Some(format!("Deleted chat with {}", args))) - } - "/peers" => { - let peers = self.transport.list_peers(); - if peers.is_empty() { - Ok(Some( - "No peers found. Start another chat-cli instance.".to_string(), - )) - } else { - self.add_system_message(&format!("── Peers ({}) ──", peers.len())); - for peer in &peers { - self.add_system_message(&format!(" • {}", peer)); - } - Ok(Some(format!("{} peer(s)", peers.len()))) + let chat_id = self + .resolve_chat_id(args) + .map(str::to_string) + .ok_or_else(|| anyhow::anyhow!("No chat matching '{args}'."))?; + let label = self.state.chats[&chat_id].display_name().to_string(); + self.state.chats.remove(&chat_id); + if self.state.active_chat.as_deref() == Some(&chat_id) { + self.state.active_chat = self.state.chats.keys().next().cloned(); } + self.save_state()?; + self.status = format!("Deleted '{label}'."); + Ok(Some(format!("Deleted '{label}'"))) } "/status" => { - let chats = self.state.chats.len(); - let active = self.state.active_chat.as_deref().unwrap_or("none"); + let active_label = self + .state + .active_chat + .as_ref() + .and_then(|id| self.state.chats.get(id)) + .map(|s| { + format!( + "{} ({})", + s.display_name(), + &s.chat_id[..8.min(s.chat_id.len())] + ) + }) + .unwrap_or_else(|| "none".to_string()); let status = format!( - "User: {}\nAddress: {}\nChats: {}\nActive: {}", + "User: {}\nIdentity: {}\nChats: {}\nActive: {}", self.user_name, - hex::encode(self.manager.installation_key().as_bytes()), - chats, - active + self.client.installation_name(), + self.state.chats.len(), + active_label, ); Ok(Some(status)) } @@ -452,8 +382,7 @@ impl ChatApp { } "/quit" => Ok(None), _ => Ok(Some(format!( - "Unknown command: {}. Type /help for commands.", - command + "Unknown command: {command}. Type /help for commands." ))), } } diff --git a/bin/chat-cli/src/main.rs b/bin/chat-cli/src/main.rs index ebbefcf..0326330 100644 --- a/bin/chat-cli/src/main.rs +++ b/bin/chat-cli/src/main.rs @@ -1,93 +1,189 @@ -//! Chat CLI - A terminal chat application using logos-chat. -//! -//! This application demonstrates how to use the logos-chat library -//! with file-based transport for local communication. -//! -//! # Usage -//! -//! Run two instances with different usernames: -//! -//! ```bash -//! # Terminal 1 -//! cargo run -p chat-cli -- alice -//! -//! # Terminal 2 -//! cargo run -p chat-cli -- bob -//! ``` -//! -//! Then in alice's terminal: -//! 1. Type `/intro` to get your introduction bundle -//! 2. Copy the bundle string -//! -//! In bob's terminal: -//! 1. Type `/connect alice ` (paste alice's bundle) -//! -//! Now bob can send messages to alice, and alice can reply. - mod app; mod transport; mod ui; mod utils; -use std::{env, path::PathBuf}; +use std::path::PathBuf; use anyhow::{Context, Result}; +use clap::Parser; +use client::DeliveryService; -/// Get the data directory (in project folder). -fn get_data_dir() -> PathBuf { - env::current_dir() - .unwrap_or_else(|_| PathBuf::from(".")) - .join("tmp/chat-cli-data") +use app::ChatApp; + +#[derive(Parser, Debug)] +#[command(name = "chat-cli", about = "End-to-end encrypted terminal chat")] +struct Cli { + /// Your identity name. + #[arg(long, short)] + name: String, + + // ── File-transport options ──────────────────────────────────────────────── + /// Shared data directory for file transport (both peers must use the same path). + #[arg(long, default_value = "tmp/chat-cli-data")] + data: PathBuf, + + // ── logos-delivery transport options ────────────────────────────────────── + /// Persistent SQLite database for logos-delivery transport (omit for ephemeral identity). + #[arg(long)] + db: Option, + + /// logos-delivery network preset (`logos.dev` or `twn`). + #[arg(long, default_value = "logos.dev")] + preset: String, + + /// TCP port for the embedded logos-delivery node. + #[arg(long, default_value_t = 60000)] + port: u16, + + /// Write logs to a file instead of stderr (keeps TUI output clean). + #[arg(long)] + log_file: Option, + + /// Initialize and immediately exit without launching the TUI (for CI). + #[arg(long)] + smoketest: bool, } fn main() -> Result<()> { - // Parse arguments - let args: Vec = std::env::args().collect(); - if args.len() < 2 { - eprintln!("Usage: {} ", args[0]); - eprintln!("\nExample:"); - eprintln!(" Terminal 1: {} alice", args[0]); - eprintln!(" Terminal 2: {} bob", args[0]); - std::process::exit(1); + let cli = Cli::parse(); + setup_logging(cli.log_file.as_deref())?; + #[cfg(logos_delivery)] + return run_logos_delivery(cli); + #[cfg(not(logos_delivery))] + run_file(cli) +} + +#[cfg(not(logos_delivery))] +fn run_file(cli: Cli) -> Result<()> { + use transport::file::FileTransport; + + std::fs::create_dir_all(&cli.data).context("failed to create data directory")?; + + println!("Starting chat as '{}'...", cli.name); + println!("Data dir: {}", cli.data.display()); + + let transport_dir = cli.data.join("transport"); + let (transport, inbound) = + FileTransport::new(&transport_dir).context("failed to create file transport")?; + + let db_path = cli.data.join(format!("{}.db", cli.name)); + let client = client::ChatClient::open( + cli.name.clone(), + client::StorageConfig::Encrypted { + path: db_path.to_string_lossy().to_string(), + key: "chat-cli".to_string(), + }, + transport, + ) + .map_err(|e| anyhow::anyhow!("{e:?}")) + .context("failed to open chat client")?; + + let mut app = ChatApp::new(client, inbound, &cli.name, &cli.data)?; + + if cli.smoketest { + return Ok(()); } - let user_name = &args[1]; - - // Setup data directory in project folder - let data_dir = get_data_dir(); - std::fs::create_dir_all(&data_dir).context("Failed to create data directory")?; - - println!("Starting chat as '{}'...", user_name); - println!("Data dir: {:?}", data_dir); - - // Create app - let mut app = app::ChatApp::new(user_name, &data_dir).context("Failed to create chat app")?; - - // Initialize terminal UI - let mut terminal = ui::init().context("Failed to initialize terminal")?; - - // Main loop + let mut terminal = ui::init().context("failed to initialize terminal")?; let result = run_app(&mut terminal, &mut app); - - // Restore terminal - ui::restore().context("Failed to restore terminal")?; - + ui::restore().context("failed to restore terminal")?; result } -fn run_app(terminal: &mut ui::Tui, app: &mut app::ChatApp) -> Result<()> { +#[cfg_attr(not(logos_delivery), allow(dead_code, unused_variables))] +fn run_logos_delivery(cli: Cli) -> Result<()> { + #[cfg(logos_delivery)] + { + use transport::logos_delivery::{Config, Service}; + + eprintln!("Starting logos-delivery node (preset={})...", cli.preset); + eprintln!("This may take a few seconds while connecting to the network."); + + let logos_cfg = Config { + preset: cli.preset.clone(), + tcp_port: cli.port, + ..Default::default() + }; + let (delivery, inbound) = + Service::start(logos_cfg).context("failed to start logos-delivery")?; + + eprintln!("Node connected. Initializing chat client..."); + + let data_dir = cli + .db + .as_ref() + .and_then(|p| p.parent()) + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| cli.data.clone()); + + let client = match cli.db { + Some(ref path) => { + let db_str = path + .to_str() + .context("db path contains non-UTF-8 characters")? + .to_string(); + client::ChatClient::open( + cli.name.clone(), + client::StorageConfig::Encrypted { + path: db_str, + key: "chat-cli".to_string(), + }, + delivery, + ) + .map_err(|e| anyhow::anyhow!("{e:?}")) + .context("failed to open persistent client")? + } + None => client::ChatClient::new(cli.name.clone(), delivery), + }; + + let mut app = ChatApp::new(client, inbound, &cli.name, &data_dir)?; + + if cli.smoketest { + return Ok(()); + } + + let mut terminal = ui::init().context("failed to initialize terminal")?; + let result = run_app(&mut terminal, &mut app); + ui::restore().context("failed to restore terminal")?; + return result; + } + + #[cfg(not(logos_delivery))] + anyhow::bail!( + "logos-delivery transport is not available in this build.\n\ + Build with LOGOS_DELIVERY_LIB_DIR set to enable it." + ) +} + +fn run_app(terminal: &mut ui::Tui, app: &mut ChatApp) -> Result<()> { loop { - // Process incoming messages app.process_incoming()?; - - // Draw UI terminal.draw(|frame| ui::draw(frame, app))?; - - // Handle input if !ui::handle_events(app)? { break; } } + Ok(()) +} + +fn setup_logging(log_file: Option<&std::path::Path>) -> Result<()> { + use tracing_subscriber::EnvFilter; + + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")); + + if let Some(path) = log_file { + let file = std::fs::File::create(path) + .with_context(|| format!("failed to create log file: {}", path.display()))?; + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_writer(file) + .init(); + } else { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::new("off")) + .init(); + } Ok(()) } diff --git a/bin/chat-cli/src/transport.rs b/bin/chat-cli/src/transport.rs index 22b4701..625f9e2 100644 --- a/bin/chat-cli/src/transport.rs +++ b/bin/chat-cli/src/transport.rs @@ -1,138 +1,4 @@ -//! File-based transport for local chat communication. -//! -//! Messages are passed between users via files in a shared directory. - -use std::collections::HashSet; -use std::fs; -use std::path::{Path, PathBuf}; - -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; - -use crate::utils::now; - -/// A message envelope for transport. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MessageEnvelope { - pub from: String, - pub data: Vec, - pub timestamp: u64, -} - -/// File-based transport for local communication. -pub struct FileTransport { - /// Our user name. - user_name: String, - /// Base directory for transport files. - base_dir: PathBuf, - /// Our inbox directory. - inbox_dir: PathBuf, - /// Set of processed message files (to avoid reprocessing). - processed: HashSet, -} - -impl FileTransport { - /// Create a new file transport. - pub fn new(user_name: &str, data_dir: &Path) -> Result { - let base_dir = data_dir.join("transport"); - let inbox_dir = base_dir.join(user_name); - - // Create our inbox directory - fs::create_dir_all(&inbox_dir).context("Failed to create inbox directory")?; - - Ok(Self { - user_name: user_name.to_string(), - base_dir, - inbox_dir, - processed: HashSet::new(), - }) - } - - /// Send a message to a specific user. - pub fn send(&self, to_user: &str, data: Vec) -> Result<()> { - let target_dir = self.base_dir.join(to_user); - - // Create target inbox if it doesn't exist - fs::create_dir_all(&target_dir).context("Failed to create target inbox")?; - - let envelope = MessageEnvelope { - from: self.user_name.clone(), - data, - timestamp: now(), - }; - - // Write message to a unique file - let filename = format!("{}_{}.json", self.user_name, now()); - let filepath = target_dir.join(&filename); - - let json = serde_json::to_string_pretty(&envelope)?; - fs::write(&filepath, json).context("Failed to write message file")?; - - Ok(()) - } - - /// Try to receive an incoming message (non-blocking). - pub fn try_recv(&mut self) -> Option { - // List files in our inbox - let entries = match fs::read_dir(&self.inbox_dir) { - Ok(e) => e, - Err(_) => return None, - }; - - for entry in entries.flatten() { - let path = entry.path(); - - // Skip non-json files - if path.extension().map(|e| e != "json").unwrap_or(true) { - continue; - } - - let filename = path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("") - .to_string(); - - // Skip already processed files - if self.processed.contains(&filename) { - continue; - } - - // Try to read and parse the message - if let Ok(contents) = fs::read_to_string(&path) - && let Ok(envelope) = serde_json::from_str::(&contents) - { - // Mark as processed and delete - self.processed.insert(filename); - let _ = fs::remove_file(&path); - return Some(envelope); - } - } - - None - } - - /// List available peers (users with inbox directories). - pub fn list_peers(&self) -> Vec { - let mut peers = Vec::new(); - - if let Ok(entries) = fs::read_dir(&self.base_dir) { - for entry in entries.flatten() { - if entry.path().is_dir() - && let Some(name) = entry.file_name().to_str() - && name != self.user_name - { - peers.push(name.to_string()); - } - } - } - - peers - } - - /// Get our user name. - #[allow(dead_code)] - pub fn user_name(&self) -> &str { - &self.user_name - } -} +#[cfg(not(logos_delivery))] +pub mod file; +#[cfg(logos_delivery)] +pub mod logos_delivery; diff --git a/bin/chat-cli/src/transport/file.rs b/bin/chat-cli/src/transport/file.rs new file mode 100644 index 0000000..c1d76d0 --- /dev/null +++ b/bin/chat-cli/src/transport/file.rs @@ -0,0 +1,136 @@ +use std::collections::BTreeMap; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, BufReader, Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use client::{AddressedEnvelope, DeliveryService}; + +#[derive(Debug, thiserror::Error)] +pub enum FileTransportError { + #[error(transparent)] + Io(#[from] io::Error), +} + +pub struct FileTransport { + transport_dir: PathBuf, +} + +impl FileTransport { + /// All instances pointing at the same `transport_dir` share one broadcast bus. + /// + /// Messages are written to `{transport_dir}/{delivery_address}/{hours_since_epoch}.bin` + /// as length-prefixed frames (`[u32 BE length][payload bytes]`). The background + /// thread reads all files under `transport_dir` and forwards every frame to + /// the returned channel; `client.receive()` discards frames it cannot decrypt. + pub fn new(transport_dir: &Path) -> io::Result<(Self, mpsc::Receiver>)> { + fs::create_dir_all(transport_dir)?; + + let (tx, rx) = mpsc::sync_channel(1024); + let dir = transport_dir.to_path_buf(); + + thread::Builder::new() + .name("file-transport".into()) + .spawn(move || poll_reader(dir, tx))?; + + Ok(( + Self { + transport_dir: transport_dir.to_path_buf(), + }, + rx, + )) + } +} + +impl DeliveryService for FileTransport { + type Error = FileTransportError; + + fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), FileTransportError> { + let addr_dir = self.transport_dir.join(&envelope.delivery_address); + fs::create_dir_all(&addr_dir)?; + + let filename = format!("{}.bin", current_hour()); + let path = addr_dir.join(filename); + + let mut file = OpenOptions::new().create(true).append(true).open(&path)?; + let len = envelope.data.len() as u32; + file.write_all(&len.to_be_bytes())?; + file.write_all(&envelope.data)?; + Ok(()) + } +} + +/// Hours since Unix epoch — used as the rolling filename. +fn current_hour() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + / 3600 +} + +fn poll_reader(transport_dir: PathBuf, tx: mpsc::SyncSender>) { + // Maps absolute file path → number of bytes already consumed. + let mut offsets: BTreeMap = BTreeMap::new(); + + loop { + let bin_files = collect_bin_files(&transport_dir); + + for path in bin_files { + let offset = offsets.entry(path.clone()).or_insert(0); + + let file = match File::open(&path) { + Ok(f) => f, + Err(_) => continue, + }; + let mut reader = BufReader::new(file); + if reader.seek(SeekFrom::Start(*offset)).is_err() { + continue; + } + + loop { + let mut len_buf = [0u8; 4]; + if reader.read_exact(&mut len_buf).is_err() { + break; // no complete header yet + } + let len = u32::from_be_bytes(len_buf) as usize; + let mut payload = vec![0u8; len]; + if reader.read_exact(&mut payload).is_err() { + break; // partial frame — wait for writer to finish + } + let _ = tx.try_send(payload); + *offset += (4 + len) as u64; + } + } + + thread::sleep(Duration::from_millis(100)); + } +} + +/// Walk `transport_dir/*/` and collect all `*.bin` files, sorted by path +/// (address subdir first, then filename = hour order). +fn collect_bin_files(transport_dir: &Path) -> Vec { + let mut files = Vec::new(); + let Ok(addr_entries) = fs::read_dir(transport_dir) else { + return files; + }; + for addr_entry in addr_entries.flatten() { + let addr_path = addr_entry.path(); + if !addr_path.is_dir() { + continue; + } + let Ok(file_entries) = fs::read_dir(&addr_path) else { + continue; + }; + for file_entry in file_entries.flatten() { + let p = file_entry.path(); + if p.extension().is_some_and(|e| e == "bin") { + files.push(p); + } + } + } + files.sort(); + files +} diff --git a/bin/chat-cli/src/transport/logos_delivery.rs b/bin/chat-cli/src/transport/logos_delivery.rs new file mode 100644 index 0000000..ada57bd --- /dev/null +++ b/bin/chat-cli/src/transport/logos_delivery.rs @@ -0,0 +1,303 @@ +//! logos-delivery backed [`client::DeliveryService`] implementation. +//! +//! `LogosDeliveryService` wraps an embedded logos-delivery node running on a +//! dedicated `std::thread`. All interaction is via synchronous `std::sync::mpsc` +//! channels. +//! +//! ## Content topic mapping +//! +//! `AddressedEnvelope::delivery_address` maps to logos-delivery content topic +//! `/logos-chat/1/{delivery_address}/proto`. + +pub(crate) mod sys; +pub(crate) mod wrapper; + +use std::sync::{Arc, Mutex, mpsc}; +use std::thread; +use std::time::Duration; + +use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64; +use client::{AddressedEnvelope, DeliveryService}; +use tracing::{error, info, warn}; + +use wrapper::LogosNodeCtx; + +pub fn content_topic_for(delivery_address: &str) -> String { + format!("/logos-chat/1/{delivery_address}/proto") +} + +// ── Error ──────────────────────────────────────────────────────────────────── + +#[derive(Debug, thiserror::Error)] +pub enum DeliveryError { + #[error("node startup failed: {0}")] + StartupFailed(String), + #[error("publish failed: {0}")] + PublishFailed(String), + #[error("send channel closed")] + ChannelClosed, +} + +// ── Internals ──────────────────────────────────────────────────────────────── + +struct OutboundCmd { + message_json: String, + reply: mpsc::SyncSender>, +} + +type SubscriberList = Arc>>>>; + +// ── Config ─────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone)] +pub struct Config { + pub preset: String, + pub tcp_port: u16, + pub log_level: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + preset: "logos.dev".into(), + tcp_port: 60000, + log_level: "ERROR".into(), + } + } +} + +// ── Wire types ────────────────────────────────────────────────────────────── + +/// Outbound message sent to the logos-delivery node. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct WakuMessage { + #[serde(rename = "contentTopic")] + content_topic: String, + /// Base64-encoded payload. + payload: String, + ephemeral: bool, +} + +/// Top-level event envelope received from the logos-delivery node callback. +#[derive(Debug, serde::Deserialize)] +struct WakuEvent { + #[serde(rename = "eventType")] + event_type: String, + message: Option, +} + +/// Message payload from a `message_received` event. +#[derive(Debug, serde::Deserialize)] +struct ReceivedMessage { + #[serde(rename = "contentTopic")] + content_topic: String, + /// The node may deliver the payload as either a base64 string or a JSON + /// array of byte values. + payload: WakuPayload, +} + +/// Untagged union that handles both payload representations. +#[derive(Debug, serde::Deserialize)] +#[serde(untagged)] +enum WakuPayload { + Base64(String), + Bytes(Vec), +} + +impl WakuPayload { + fn decode(self) -> Option> { + match self { + WakuPayload::Base64(s) => BASE64.decode(s).ok(), + WakuPayload::Bytes(b) => Some(b), + } + } +} + +// ── Service ────────────────────────────────────────────────────────────────── + +/// logos-delivery backed delivery service. Cheap to clone — all clones share +/// the same background node. +#[derive(Clone)] +pub struct Service { + outbound: mpsc::SyncSender, + #[allow(dead_code)] + subscribers: SubscriberList, +} + +impl Service { + /// Start the embedded logos-delivery node. Returns the service and a + /// receiver for inbound raw payloads. + pub fn start(cfg: Config) -> Result<(Self, mpsc::Receiver>), DeliveryError> { + let (out_tx, out_rx) = mpsc::sync_channel::(256); + let subscribers: SubscriberList = Arc::new(Mutex::new(Vec::new())); + let (ready_tx, ready_rx) = mpsc::channel::>(); + // Create the inbound channel before spawning so the receiver is + // registered inside the thread, before any event callback fires. + let (inbound_tx, inbound_rx) = mpsc::sync_channel::>(1024); + + let subs_for_thread = subscribers.clone(); + + let handle = thread::Builder::new() + .name("logos-node".into()) + .spawn(move || { + if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + Self::node_thread(cfg, out_rx, subs_for_thread, inbound_tx, ready_tx); + })) { + let msg = panic + .downcast_ref::<&str>() + .map(|s| s.to_string()) + .or_else(|| panic.downcast_ref::().cloned()) + .unwrap_or_else(|| "unknown panic".into()); + error!("logos-node thread panicked: {msg}"); + } + }) + .map_err(|e| DeliveryError::StartupFailed(e.to_string()))?; + + // On failure, the node thread drops LogosNodeCtx (stop+destroy against + // a half-initialized Nim node). Join it so the process doesn't begin + // teardown mid-destroy — that race SIGSEGVs inside the Nim async loop. + let ready = ready_rx.recv().unwrap_or_else(|_| { + Err(DeliveryError::StartupFailed( + "node thread exited before ready".into(), + )) + }); + if let Err(e) = ready { + let _ = handle.join(); + return Err(e); + } + + Ok(( + Self { + outbound: out_tx, + subscribers, + }, + inbound_rx, + )) + } + + fn node_thread( + cfg: Config, + out_rx: mpsc::Receiver, + subscribers: SubscriberList, + inbound_tx: mpsc::SyncSender>, + ready_tx: mpsc::Sender>, + ) { + // discv5UdpPort defaults to 9000 in libwaku, so a second instance with + // a distinct --port still collides on UDP. Bind it to tcp_port so a + // single --port knob keeps both ports distinct across instances. + let config_json = serde_json::json!({ + "logLevel": cfg.log_level, + "mode": "Core", + "preset": cfg.preset, + "tcpPort": cfg.tcp_port, + "discv5UdpPort": cfg.tcp_port, + }) + .to_string(); + + let mut node = match LogosNodeCtx::new(&config_json) { + Ok(n) => n, + Err(e) => { + let _ = ready_tx.send(Err(DeliveryError::StartupFailed(e))); + return; + } + }; + + // Register the inbound sender before installing the event callback so + // there is no window where the callback is live but the channel is not + // yet in the subscriber list. + subscribers.lock().unwrap().push(inbound_tx); + + let subs_for_cb = subscribers.clone(); + let event_closure = move |_ret: i32, data: &str| { + if let Some(payload) = Self::parse_message_received(data) { + let mut guard = match subs_for_cb.lock() { + Ok(g) => g, + Err(e) => { + error!("subscriber mutex poisoned: {e}"); + return; + } + }; + guard.retain(|tx| match tx.try_send(payload.clone()) { + Ok(()) => true, + Err(mpsc::TrySendError::Full(_)) => true, + Err(mpsc::TrySendError::Disconnected(_)) => false, + }); + } + }; + node.set_event_callback(event_closure); + + if let Err(e) = node.start() { + let _ = ready_tx.send(Err(DeliveryError::StartupFailed(e))); + return; + } + info!("logos-delivery node started (preset={})", cfg.preset); + + // FIXME: This unconditional sleep is a stand-in for proper + // peer-connectivity detection. The right approach is to listen for a + // `peer_connected` (or equivalent status-change) event from the node + // callback and only proceed once at least one peer is reachable, + // falling back to a configurable timeout. logos-delivery would need to + // surface such an event via its callback mechanism for this to work. + thread::sleep(Duration::from_secs(3)); + + let default_topic = content_topic_for("delivery_address"); + if let Err(e) = node.subscribe(&default_topic) { + warn!("subscribe to {default_topic}: {e}"); + } else { + info!("subscribed to {default_topic}"); + } + + let _ = ready_tx.send(Ok(())); + + while let Ok(cmd) = out_rx.recv() { + let result = node + .send(&cmd.message_json) + .map(|_| ()) + .map_err(DeliveryError::PublishFailed); + let _ = cmd.reply.try_send(result); + } + + info!("logos-node outbound loop finished"); + } + + fn parse_message_received(data: &str) -> Option> { + let event: WakuEvent = serde_json::from_str(data).ok()?; + + if event.event_type != "message_received" { + return None; + } + + let msg = event.message?; + + if !msg.content_topic.starts_with("/logos-chat/1/") { + return None; + } + + msg.payload.decode() + } +} + +impl DeliveryService for Service { + type Error = DeliveryError; + + fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), DeliveryError> { + let msg = WakuMessage { + content_topic: content_topic_for(&envelope.delivery_address), + payload: BASE64.encode(&envelope.data), + ephemeral: false, + }; + let message_json = + serde_json::to_string(&msg).map_err(|e| DeliveryError::PublishFailed(e.to_string()))?; + + let (reply_tx, reply_rx) = mpsc::sync_channel(1); + self.outbound + .send(OutboundCmd { + message_json, + reply: reply_tx, + }) + .map_err(|_| DeliveryError::ChannelClosed)?; + + reply_rx.recv().map_err(|_| DeliveryError::ChannelClosed)? + } +} diff --git a/bin/chat-cli/src/transport/logos_delivery/sys.rs b/bin/chat-cli/src/transport/logos_delivery/sys.rs new file mode 100644 index 0000000..2036116 --- /dev/null +++ b/bin/chat-cli/src/transport/logos_delivery/sys.rs @@ -0,0 +1,102 @@ +//! Raw FFI declarations matching liblogosdelivery.h (trampoline pattern). +//! +//! No `#[link]` attribute — build.rs handles linking to liblogosdelivery. +#![allow(unused)] + +use std::os::raw::{c_char, c_int, c_void}; +use std::slice; + +pub const RET_OK: i32 = 0; + +pub type FFICallBack = unsafe extern "C" fn(c_int, *const c_char, usize, *const c_void); + +unsafe extern "C" { + pub fn logosdelivery_create_node( + config_json: *const c_char, + cb: FFICallBack, + user_data: *const c_void, + ) -> *mut c_void; + + pub fn logosdelivery_start_node( + ctx: *mut c_void, + cb: FFICallBack, + user_data: *const c_void, + ) -> c_int; + + pub fn logosdelivery_stop_node( + ctx: *mut c_void, + cb: FFICallBack, + user_data: *const c_void, + ) -> c_int; + + pub fn logosdelivery_destroy( + ctx: *mut c_void, + cb: FFICallBack, + user_data: *const c_void, + ) -> c_int; + + pub fn logosdelivery_subscribe( + ctx: *mut c_void, + cb: FFICallBack, + user_data: *const c_void, + content_topic: *const c_char, + ) -> c_int; + + pub fn logosdelivery_unsubscribe( + ctx: *mut c_void, + cb: FFICallBack, + user_data: *const c_void, + content_topic: *const c_char, + ) -> c_int; + + /// `message_json`: `{"contentTopic": "...", "payload": "", "ephemeral": false}` + pub fn logosdelivery_send( + ctx: *mut c_void, + cb: FFICallBack, + user_data: *const c_void, + message_json: *const c_char, + ) -> c_int; + + pub fn logosdelivery_set_event_callback( + ctx: *mut c_void, + cb: FFICallBack, + user_data: *const c_void, + ); + + pub fn logosdelivery_get_node_info( + ctx: *mut c_void, + cb: FFICallBack, + user_data: *const c_void, + node_info_id: *const c_char, + ) -> c_int; +} + +// ── Trampoline ─────────────────────────────────────────────────────────────── + +pub unsafe extern "C" fn trampoline( + return_val: c_int, + buffer: *const c_char, + buffer_len: usize, + data: *const c_void, +) where + C: FnMut(i32, &str), +{ + if data.is_null() { + return; + } + let closure = unsafe { &mut *(data as *mut C) }; + if buffer.is_null() || buffer_len == 0 { + closure(return_val, ""); + return; + } + let bytes = unsafe { slice::from_raw_parts(buffer as *const u8, buffer_len) }; + let s = String::from_utf8_lossy(bytes); + closure(return_val, &s); +} + +pub fn get_trampoline(_: &C) -> FFICallBack +where + C: FnMut(i32, &str), +{ + trampoline:: +} diff --git a/bin/chat-cli/src/transport/logos_delivery/wrapper.rs b/bin/chat-cli/src/transport/logos_delivery/wrapper.rs new file mode 100644 index 0000000..04296a7 --- /dev/null +++ b/bin/chat-cli/src/transport/logos_delivery/wrapper.rs @@ -0,0 +1,227 @@ +//! Safe synchronous wrapper around the raw liblogosdelivery FFI. +//! +//! # Why Box::into_raw for one-shot callbacks? +//! +//! `sendRequestToFFIThread` (nim-ffi) signals the caller as soon as the FFI +//! thread *receives* the request, before it processes it. The actual result +//! callback fires later, from the Nim async event loop, after the Rust call +//! frame has returned and its stack variables are gone. Passing `&mut closure` +//! as `user_data` therefore produces a dangling pointer by the time the +//! callback fires — a use-after-free that manifests as a SIGSEGV when the +//! operation fails and the callback tries to write an error into captured +//! stack memory. +//! +//! Fix: heap-allocate each one-shot closure with `Box::into_raw`, synchronise +//! via an `mpsc` channel (blocking until the callback fires), then drop the +//! box. The pointer is valid for the entire async lifetime of the request. +//! +//! # Why store the event callback inside LogosNodeCtx? +//! +//! Rust drops locals in reverse declaration order. If the event-callback box +//! were held by the caller (outside the node), it would be freed before the +//! node's Drop runs stop+destroy. During stop/destroy the Nim async event +//! loop can still fire the event callback, which would access freed memory. +//! +//! By storing the box as `_event_cb` inside `LogosNodeCtx`, Rust's field-drop +//! order guarantees it is freed *after* Drop::drop returns (i.e. after +//! stop+destroy complete), so the pointer is always valid when Nim calls it. + +use std::ffi::CString; +use std::os::raw::c_void; +use std::sync::mpsc; + +use super::sys::{self as ffi, RET_OK, get_trampoline}; + +/// Opaque handle to a logos-delivery node context. +pub struct LogosNodeCtx { + ctx: *mut c_void, + /// Keeps the event-callback closure alive for the lifetime of the node. + _event_cb: Option>, +} + +// The logos-delivery ctx pointer is thread-safe (serialized calls inside C/Nim). +unsafe impl Send for LogosNodeCtx {} +unsafe impl Sync for LogosNodeCtx {} + +impl LogosNodeCtx { + pub fn new(config_json: &str) -> Result { + let config_cstr = CString::new(config_json).map_err(|e| e.to_string())?; + + let (tx, rx) = mpsc::sync_channel::>(1); + let closure = move |ret: i32, data: &str| { + let _ = tx.send(if ret == RET_OK { + Ok(()) + } else { + Err(data.to_string()) + }); + }; + let raw = Box::into_raw(Box::new(closure)); + let cb = get_trampoline(unsafe { &*raw }); + + let ctx = unsafe { + ffi::logosdelivery_create_node(config_cstr.as_ptr(), cb, raw as *const c_void) + }; + + // create_node may call the callback synchronously (try_recv) or + // asynchronously (recv). Handle both. + let callback_result: Result<(), String> = if ctx.is_null() { + rx.try_recv() + .unwrap_or(Err("logosdelivery_create_node returned null".into())) + } else { + rx.recv() + .unwrap_or(Err("callback channel disconnected".into())) + }; + drop(unsafe { Box::from_raw(raw) }); + + callback_result.map(|_| Self { + ctx, + _event_cb: None, + }) + } + + pub fn start(&self) -> Result<(), String> { + let (tx, rx) = mpsc::sync_channel::>(1); + let closure = move |ret: i32, data: &str| { + let _ = tx.send(if ret == RET_OK { + Ok(()) + } else { + Err(data.to_string()) + }); + }; + let raw = Box::into_raw(Box::new(closure)); + let cb = get_trampoline(unsafe { &*raw }); + + let ret = unsafe { ffi::logosdelivery_start_node(self.ctx, cb, raw as *const c_void) }; + + if ret != RET_OK { + drop(unsafe { Box::from_raw(raw) }); + return Err(format!("logosdelivery_start_node returned {ret}")); + } + let result = rx + .recv() + .unwrap_or(Err("callback channel disconnected".into())); + drop(unsafe { Box::from_raw(raw) }); + result + } + + pub fn subscribe(&self, content_topic: &str) -> Result<(), String> { + let topic_cstr = CString::new(content_topic).map_err(|e| e.to_string())?; + + let (tx, rx) = mpsc::sync_channel::>(1); + let closure = move |ret: i32, data: &str| { + let _ = tx.send(if ret == RET_OK { + Ok(()) + } else { + Err(data.to_string()) + }); + }; + let raw = Box::into_raw(Box::new(closure)); + let cb = get_trampoline(unsafe { &*raw }); + + let ret = unsafe { + ffi::logosdelivery_subscribe(self.ctx, cb, raw as *const c_void, topic_cstr.as_ptr()) + }; + + if ret != RET_OK { + drop(unsafe { Box::from_raw(raw) }); + return Err(format!("logosdelivery_subscribe returned {ret}")); + } + let result = rx + .recv() + .unwrap_or(Err("callback channel disconnected".into())); + drop(unsafe { Box::from_raw(raw) }); + result + } + + /// Returns the request ID on success. + pub fn send(&self, message_json: &str) -> Result { + let msg_cstr = CString::new(message_json).map_err(|e| e.to_string())?; + + let (tx, rx) = mpsc::sync_channel::>(1); + let closure = move |ret: i32, data: &str| { + let _ = tx.send(if ret == RET_OK { + Ok(data.to_string()) + } else { + Err(data.to_string()) + }); + }; + let raw = Box::into_raw(Box::new(closure)); + let cb = get_trampoline(unsafe { &*raw }); + + let ret = unsafe { + ffi::logosdelivery_send(self.ctx, cb, raw as *const c_void, msg_cstr.as_ptr()) + }; + + if ret != RET_OK { + drop(unsafe { Box::from_raw(raw) }); + return Err(format!("logosdelivery_send returned {ret}")); + } + let result = rx + .recv() + .unwrap_or(Err("callback channel disconnected".into())); + drop(unsafe { Box::from_raw(raw) }); + result + } + + /// Stores the event callback inside the node so it is dropped *after* + /// stop+destroy in Drop, keeping the pointer valid for the node's lifetime. + pub fn set_event_callback(&mut self, closure: C) + where + C: FnMut(i32, &str) + Send + 'static, + { + let mut boxed = Box::new(closure); + let cb = get_trampoline(&*boxed); + let user_data = &mut *boxed as *mut C as *const c_void; + unsafe { + ffi::logosdelivery_set_event_callback(self.ctx, cb, user_data); + } + // Move the box into self; the heap address (user_data) is unaffected. + self._event_cb = Some(boxed); + } + + pub fn stop(&self) -> Result<(), String> { + let (tx, rx) = mpsc::sync_channel::>(1); + let closure = move |ret: i32, data: &str| { + let _ = tx.send(if ret == RET_OK { + Ok(()) + } else { + Err(data.to_string()) + }); + }; + let raw = Box::into_raw(Box::new(closure)); + let cb = get_trampoline(unsafe { &*raw }); + + let ret = unsafe { ffi::logosdelivery_stop_node(self.ctx, cb, raw as *const c_void) }; + + if ret != RET_OK { + drop(unsafe { Box::from_raw(raw) }); + return Err(format!("logosdelivery_stop_node returned {ret}")); + } + let result = rx + .recv() + .unwrap_or(Err("callback channel disconnected".into())); + drop(unsafe { Box::from_raw(raw) }); + result + } +} + +impl Drop for LogosNodeCtx { + fn drop(&mut self) { + // stop+destroy must complete before _event_cb is freed. + // Rust drops fields after Drop::drop returns, so _event_cb outlives + // everything below — the event callback pointer stays valid throughout. + if let Err(e) = self.stop() { + tracing::warn!("logosdelivery_stop_node failed during drop: {e}"); + } + + let (tx, rx) = mpsc::sync_channel::<()>(1); + let closure = move |_: i32, _: &str| { + let _ = tx.send(()); + }; + let raw = Box::into_raw(Box::new(closure)); + let cb = get_trampoline(unsafe { &*raw }); + unsafe { ffi::logosdelivery_destroy(self.ctx, cb, raw as *const c_void) }; + let _ = rx.recv(); + drop(unsafe { Box::from_raw(raw) }); + } +} diff --git a/bin/chat-cli/src/ui.rs b/bin/chat-cli/src/ui.rs index ce280d1..4de7067 100644 --- a/bin/chat-cli/src/ui.rs +++ b/bin/chat-cli/src/ui.rs @@ -16,6 +16,8 @@ use ratatui::{ widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}, }; +use client::DeliveryService; + use crate::app::ChatApp; pub type Tui = Terminal>; @@ -36,7 +38,7 @@ pub fn restore() -> io::Result<()> { } /// Draw the UI. -pub fn draw(frame: &mut Frame, app: &ChatApp) { +pub fn draw(frame: &mut Frame, app: &ChatApp) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -53,13 +55,16 @@ pub fn draw(frame: &mut Frame, app: &ChatApp) { draw_status(frame, app, chunks[3]); } -fn draw_header(frame: &mut Frame, app: &ChatApp, area: Rect) { +fn draw_header(frame: &mut Frame, app: &ChatApp, area: Rect) { let title = match app.current_session() { - Some(session) => format!(" 💬 Chat: {} ↔ {} ", app.user_name, session.remote_user), - None => format!( - " 💬 {} (no active chat - use /connect or /chats) ", - app.user_name - ), + Some(session) => { + let id = &session.chat_id[..8.min(session.chat_id.len())]; + match &session.nickname { + Some(name) => format!(" 💬 Chat: {} ↔ {name} ({id}) ", app.user_name), + None => format!(" 💬 Chat: {} ↔ ({id}) ", app.user_name), + } + } + None => format!(" 💬 {} — no active chat ", app.user_name), }; let header = Paragraph::new(title) @@ -73,10 +78,10 @@ fn draw_header(frame: &mut Frame, app: &ChatApp, area: Rect) { frame.render_widget(header, area); } -fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) { +fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) { let remote_name = app .current_session() - .map(|s| s.remote_user.as_str()) + .map(|s| s.display_name()) .unwrap_or("Them"); // Inner width: area minus borders (2) for wrapping long content. @@ -108,7 +113,7 @@ fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) { let first_line_width = inner_width.saturating_sub(prefix_len).max(1); // First line includes the prefix. - let (first_chunk, rest) = if content.len() <= first_line_width { + let (first_chunk, rest): (&str, &str) = if content.len() <= first_line_width { (content.as_str(), "") } else { content.split_at(first_line_width) @@ -121,7 +126,7 @@ fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) { // Continuation lines are indented to align with content. let indent = " ".repeat(prefix_len); - let mut remaining = rest; + let mut remaining: &str = rest; while !remaining.is_empty() { let chunk_width = inner_width.saturating_sub(prefix_len).max(1); let (chunk, tail) = if remaining.len() <= chunk_width { @@ -141,17 +146,25 @@ fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) { .collect(); let title = match app.current_session() { - Some(session) => format!(" Messages with {} ", session.remote_user), - None => " Messages ".to_string(), + Some(s) => match &s.nickname { + Some(name) => format!(" Messages with {name} "), + None => format!(" Messages ({}) ", &s.chat_id[..8.min(s.chat_id.len())]), + }, + None => " Command output ".to_string(), }; + let item_count = messages.len(); let messages_widget = List::new(messages).block(Block::default().title(title).borders(Borders::ALL)); - frame.render_widget(messages_widget, area); + // Scroll so the last line is always visible (area height minus two borders). + let visible = area.height.saturating_sub(2) as usize; + let offset = item_count.saturating_sub(visible); + let mut list_state = ratatui::widgets::ListState::default().with_offset(offset); + frame.render_stateful_widget(messages_widget, area, &mut list_state); } -fn draw_input(frame: &mut Frame, app: &ChatApp, area: Rect) { +fn draw_input(frame: &mut Frame, app: &ChatApp, area: Rect) { // Inner width: area minus borders (2). let inner_width = area.width.saturating_sub(2) as usize; let input_len = app.input.len(); @@ -165,13 +178,11 @@ fn draw_input(frame: &mut Frame, app: &ChatApp, area: Rect) { let visible_input = &app.input[scroll_offset..]; - let input = Paragraph::new(visible_input) - .style(Style::default().fg(Color::White)) - .block( - Block::default() - .title(" Input (Enter to send) ") - .borders(Borders::ALL), - ); + let input = Paragraph::new(visible_input).style(Style::default()).block( + Block::default() + .title(" Input (Enter to send) ") + .borders(Borders::ALL), + ); frame.render_widget(input, area); @@ -180,7 +191,7 @@ fn draw_input(frame: &mut Frame, app: &ChatApp, area: Rect) { frame.set_cursor_position((cursor_x, area.y + 1)); } -fn draw_status(frame: &mut Frame, app: &ChatApp, area: Rect) { +fn draw_status(frame: &mut Frame, app: &ChatApp, area: Rect) { let status = Paragraph::new(app.status.as_str()) .style(Style::default().fg(Color::Gray)) .block(Block::default().title(" Status ").borders(Borders::ALL)) @@ -190,7 +201,7 @@ fn draw_status(frame: &mut Frame, app: &ChatApp, area: Rect) { } /// Handle keyboard events. -pub fn handle_events(app: &mut ChatApp) -> io::Result { +pub fn handle_events(app: &mut ChatApp) -> io::Result { // Poll for events with a short timeout to allow checking incoming messages if event::poll(std::time::Duration::from_millis(100))? && let Event::Key(key) = event::read()? diff --git a/flake.lock b/flake.lock index 1d78474..3f54c9e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,6 +1,42 @@ { "nodes": { + "logos-delivery": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "zerokit": "zerokit" + }, + "locked": { + "lastModified": 1777287099, + "narHash": "sha256-H2gpbDUg6Wy+uIY9wL0t9ICUPN82B/vCnXZ2mo3Wa/E=", + "owner": "logos-messaging", + "repo": "logos-delivery", + "rev": "5034086fefe2f32bf95319cdd39aa62fc622e4bc", + "type": "github" + }, + "original": { + "owner": "logos-messaging", + "repo": "logos-delivery", + "type": "github" + } + }, "nixpkgs": { + "locked": { + "lastModified": 1770464364, + "narHash": "sha256-z5NJPSBwsLf/OfD8WTmh79tlSU8XgIbwmk6qB1/TFzY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "23d72dabcb3b12469f57b37170fcbc1789bd7457", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "23d72dabcb3b12469f57b37170fcbc1789bd7457", + "type": "github" + } + }, + "nixpkgs_2": { "locked": { "lastModified": 1775710090, "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", @@ -18,11 +54,55 @@ }, "root": { "inputs": { - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" + "logos-delivery": "logos-delivery", + "nixpkgs": "nixpkgs_2", + "rust-overlay": "rust-overlay_3" } }, "rust-overlay": { + "inputs": { + "nixpkgs": [ + "logos-delivery", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775099554, + "narHash": "sha256-3xBsGnGDLOFtnPZ1D3j2LU19wpAlYefRKTlkv648rU0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8d6387ed6d8e6e6672fd3ed4b61b59d44b124d99", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "nixpkgs": [ + "logos-delivery", + "zerokit", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1771211437, + "narHash": "sha256-lcNK438i4DGtyA+bPXXyVLHVmJjYpVKmpux9WASa3ro=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "c62195b3d6e1bb11e0c2fb2a494117d3b55d410f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_3": { "inputs": { "nixpkgs": [ "nixpkgs" @@ -41,6 +121,29 @@ "repo": "rust-overlay", "type": "github" } + }, + "zerokit": { + "inputs": { + "nixpkgs": [ + "logos-delivery", + "nixpkgs" + ], + "rust-overlay": "rust-overlay_2" + }, + "locked": { + "lastModified": 1771279884, + "narHash": "sha256-tzkQPwSl4vPTUo1ixHh6NCENjsBDroMKTjifg2q8QX8=", + "owner": "vacp2p", + "repo": "zerokit", + "rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477", + "type": "github" + }, + "original": { + "owner": "vacp2p", + "repo": "zerokit", + "rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 9f5de6c..ca1b52e 100644 --- a/flake.nix +++ b/flake.nix @@ -7,12 +7,14 @@ url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; + logos-delivery.url = "github:logos-messaging/logos-delivery"; }; - outputs = { self, nixpkgs, rust-overlay }: + outputs = { self, nixpkgs, rust-overlay, logos-delivery }: let systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ]; forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f { + inherit system; pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; @@ -20,15 +22,21 @@ }); in { - packages = forAllSystems ({ pkgs }: + packages = forAllSystems ({ pkgs, system }: let rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust_toolchain.toml; rustPlatform = pkgs.makeRustPlatform { cargo = rustToolchain; rustc = rustToolchain; }; + logos-delivery-lib = logos-delivery.packages.${system}.liblogosdelivery.override { + enablePostgres = false; + enableNimDebugDlOpen = false; + chroniclesLogLevel = "FATAL"; + }; in { + logos-delivery = logos-delivery-lib; default = rustPlatform.buildRustPackage { pname = "libchat"; version = (builtins.fromTOML (builtins.readFile ./crates/client-ffi/Cargo.toml)).package.version;