From 1c1228e1b7133bef0e3a482b50183b582e66bea6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:03:55 +0000 Subject: [PATCH 1/3] build(deps): bump nostr-sdk from 0.44.1 to 0.45.1 Bumps [nostr-sdk](https://github.com/nostrdevkit/nostr) from 0.44.1 to 0.45.1. - [Commits](https://github.com/nostrdevkit/nostr/commits) --- updated-dependencies: - dependency-name: nostr-sdk dependency-version: 0.45.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 273 ++++++++++++++++++++++++++++------------------------- Cargo.toml | 2 +- 2 files changed, 145 insertions(+), 130 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29a88522..d77f04e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,20 +191,21 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.13.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7d8c7d34a225ba919dd9ba44d4b9106d20142da545e086be8ae21d1897e043" +checksum = "2c713e1f14c7b82e32ea159af1c6e2f070cfadbdf23fb2512acce9af0a26f1a2" dependencies = [ - "async-utility", "futures", "futures-util", "js-sys", "tokio", + "tokio-happy-eyeballs", "tokio-rustls", "tokio-socks", "tokio-tungstenite", "url", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", ] @@ -217,12 +218,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "atomic-destructor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" - [[package]] name = "autocfg" version = "1.5.0" @@ -250,17 +245,11 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - [[package]] name = "bech32" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" +checksum = "efbd3e1070bbdf4cd88a75264e18e8a26f7cb5c6949eadf0ceb85fb159cf08f8" [[package]] name = "better-panic" @@ -298,12 +287,27 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" +dependencies = [ + "bitcoin-internals 0.6.0", +] + [[package]] name = "bitcoin-internals" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + [[package]] name = "bitcoin-io" version = "0.1.4" @@ -316,7 +320,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ - "bitcoin-internals", + "bitcoin-internals 0.2.0", "hex-conservative 0.1.2", ] @@ -328,6 +332,17 @@ checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" dependencies = [ "bitcoin-io", "hex-conservative 0.2.2", +] + +[[package]] +name = "bitcoin_hashes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e" +dependencies = [ + "bitcoin-consensus-encoding", + "bitcoin-internals 0.6.0", + "hex-conservative 1.2.0", "serde", ] @@ -391,6 +406,12 @@ version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -828,7 +849,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -942,7 +962,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -1136,6 +1155,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1316,10 +1345,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -1343,6 +1370,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] @@ -1380,6 +1408,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1417,6 +1454,16 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -1445,12 +1492,12 @@ dependencies = [ ] [[package]] -name = "hmac" -version = "0.12.1" +name = "hex-conservative" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" dependencies = [ - "digest", + "arrayvec", ] [[package]] @@ -1665,18 +1712,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1831,12 +1866,6 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" -[[package]] -name = "lru" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" - [[package]] name = "lru" version = "0.18.0" @@ -1944,79 +1973,70 @@ dependencies = [ [[package]] name = "nostr" -version = "0.44.1" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3595fecf0e0aaacb69a0dc0101a4453f3c76eda333d6bbc49f68f64390b3d85" +checksum = "5dde8c76076d334409d86c2e1db3e97abe5deb8cb92744f939cbc1fa45bd69e7" dependencies = [ "base64", "bech32", "bip39", - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 1.2.0", "cbc", "chacha20", "chacha20poly1305", - "getrandom 0.2.16", - "hex", - "instant", - "scrypt", + "faster-hex", + "opaquerr", + "rand 0.10.2", "secp256k1", "serde", "serde_json", "unicode-normalization", + "universal-time", "url", + "zeroize", ] [[package]] name = "nostr-database" -version = "0.44.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" +checksum = "4b1fdb9fcba732e32719662afad1b267e50322dbe89e506017ec13f24361bddf" dependencies = [ - "lru 0.16.2", "nostr", - "tokio", + "opaquerr", ] [[package]] name = "nostr-gossip" -version = "0.44.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" +checksum = "fa07539e52a71cb91fe0d693facaa298f03fcf9edcd66a521094e18e286e2336" dependencies = [ "nostr", + "opaquerr", ] [[package]] -name = "nostr-relay-pool" -version = "0.44.0" +name = "nostr-sdk" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" +checksum = "26c86342f367bd9b173ec4a697e936e3a82d6dad5b4aa06c0d35d9b4f88a8e72" dependencies = [ "async-utility", "async-wsocket", - "atomic-destructor", - "hex", - "lru 0.16.2", + "faster-hex", + "futures", + "lru", "negentropy", "nostr", "nostr-database", - "tokio", - "tracing", -] - -[[package]] -name = "nostr-sdk" -version = "0.44.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" -dependencies = [ - "async-utility", - "nostr", - "nostr-database", "nostr-gossip", - "nostr-relay-pool", + "opaquerr", + "rand 0.10.2", "tokio", + "tokio-stream", "tracing", + "universal-time", ] [[package]] @@ -2236,6 +2256,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "opaquerr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9" + [[package]] name = "option-ext" version = "0.2.0" @@ -2340,33 +2366,12 @@ dependencies = [ "windows-link", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -2613,6 +2618,16 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2651,6 +2666,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "ratatui" version = "0.30.2" @@ -2680,7 +2701,7 @@ dependencies = [ "hashbrown 0.17.1", "itertools 0.14.0", "kasuari", - "lru 0.18.0", + "lru", "palette", "serde", "strum", @@ -2994,15 +3015,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -3018,27 +3030,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "password-hash", - "pbkdf2", - "salsa20", - "sha2", -] - [[package]] name = "secp256k1" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ + "bitcoin_hashes 0.14.0", "rand 0.8.6", "secp256k1-sys", - "serde", ] [[package]] @@ -3629,6 +3629,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-happy-eyeballs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8564c32dfb6f4257f8bc6edfc178a34af97520e0b7b9815500c55eb3d092f29f" +dependencies = [ + "tokio", +] + [[package]] name = "tokio-macros" version = "2.7.0" @@ -3676,9 +3685,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", @@ -3860,9 +3869,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", @@ -3960,6 +3969,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-time" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a939edecc3c5a7b83c02e5f6b3c31d2bc69eabcc9a87ab12c6d37ee6dbc856" + [[package]] name = "untrusted" version = "0.9.0" @@ -4699,9 +4714,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 2ed7ee30..61f3f948 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ json5 = "1" lazy_static = "1" libc = "0.2" log = "0.4" -nostr-sdk = "0.44" +nostr-sdk = "0.45" nowhear = "0.5" percent-encoding = "2" ratatui = { version = "0.30", features = ["serde", "macros"] } From 83d6db9737242738152eab867c6ace1cc7512182 Mon Sep 17 00:00:00 2001 From: Akiomi Kamakura Date: Tue, 11 Aug 2026 22:09:24 +0900 Subject: [PATCH 2/3] fix: migrate to nostr-sdk 0.45 --- Cargo.lock | 1 - Cargo.toml | 1 - src/application/state.rs | 14 +- src/domain/collections.rs | 3 +- src/domain/nostr/event.rs | 37 ++-- src/domain/nostr/nip10.rs | 143 +++++++++++---- src/domain/nostr/nip38.rs | 4 +- src/domain/nostr/profile.rs | 5 +- src/infrastructure.rs | 1 - src/infrastructure/nostr.rs | 178 ------------------- src/infrastructure/subscription/nostr.rs | 163 +++++++++-------- src/main.rs | 10 +- src/model/editor.rs | 4 +- src/model/nostr.rs | 6 +- src/model/nostr_gateway.rs | 6 +- src/model/timeline.rs | 22 ++- src/model/timeline/text_note.rs | 84 +++++---- src/presentation/components/home/list.rs | 3 +- src/presentation/widgets/name_with_handle.rs | 5 +- src/presentation/widgets/public_key.rs | 16 +- src/presentation/widgets/tab_bar.rs | 2 +- src/presentation/widgets/text_note.rs | 23 +-- src/runtime.rs | 60 ++++--- 23 files changed, 364 insertions(+), 427 deletions(-) delete mode 100644 src/infrastructure/nostr.rs diff --git a/Cargo.lock b/Cargo.lock index d77f04e5..e198f0a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2071,7 +2071,6 @@ dependencies = [ "signal-hook 0.4.4", "sorted-vec", "strip-ansi-escapes", - "strum", "tears", "thousands", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 61f3f948..fde23da6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,6 @@ serde_json = "1" signal-hook = "0.4" sorted-vec = "0.8" strip-ansi-escapes = "0.2" -strum = { version = "0.28", features = ["derive"] } tears = "0.10.2" thousands = "0.2" tokio = { version = "1", features = ["full"] } diff --git a/src/application/state.rs b/src/application/state.rs index de9edc96..18bd74c9 100644 --- a/src/application/state.rs +++ b/src/application/state.rs @@ -306,10 +306,11 @@ impl<'a> AppState<'a> { let event_builder = if let Some(reply_to_event) = self.editor.reply_target() { log::info!("Publishing reply: {content}"); // Build NIP-10 reply tags (root/reply markers, deduped p-tag). - EventBuilder::text_note(&content).tags(ReplyTagsBuilder::build(reply_to_event.clone())) + EventBuilder::new(Kind::TextNote, &content) + .tags(ReplyTagsBuilder::build(reply_to_event.clone())) } else { log::info!("Publishing note: {content}"); - EventBuilder::text_note(&content) + EventBuilder::new(Kind::TextNote, &content) }; let outcome = self @@ -609,6 +610,7 @@ impl<'a> AppState<'a> { mod tests { use super::*; use crate::domain::{nostr::Profile, text::shorten_npub}; + use color_eyre::eyre::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::time::Duration; @@ -625,9 +627,9 @@ mod tests { } fn create_text_note(keys: &Keys, content: &str, created_at: Timestamp) -> Result { - Ok(EventBuilder::text_note(content) + Ok(EventBuilder::new(Kind::TextNote, content) .custom_created_at(created_at) - .sign_with_keys(keys)?) + .finalize(keys)?) } #[test] @@ -803,7 +805,7 @@ mod tests { let metadata = Metadata::new().name("alice").display_name("Alice"); let metadata_event = EventBuilder::new(Kind::Metadata, metadata.as_json()) .custom_created_at(Timestamp::from(1000)) - .sign_with_keys(&author_keys)?; + .finalize(&author_keys)?; let _ = state.process_nostr_event_for_tab(metadata_event, &FeedKind::Home); @@ -830,7 +832,7 @@ mod tests { let invalid_metadata_event = EventBuilder::new(Kind::Metadata, "not json") .custom_created_at(Timestamp::from(1000)) - .sign_with_keys(&author_keys)?; + .finalize(&author_keys)?; let _ = state.process_nostr_event_for_tab(invalid_metadata_event, &FeedKind::Home); diff --git a/src/domain/collections.rs b/src/domain/collections.rs index 9d41d6a3..dbdb1d34 100644 --- a/src/domain/collections.rs +++ b/src/domain/collections.rs @@ -187,8 +187,9 @@ impl fmt::Display for EventSet { #[cfg(test)] mod tests { use super::*; - use nostr_sdk::nostr::{Kind, Timestamp}; + use color_eyre::eyre::Result; use nostr_sdk::prelude::Signature; + use nostr_sdk::prelude::{Kind, Timestamp}; fn create_test_event(id_suffix: u8, content: &str) -> Result { let mut id_bytes = [0u8; 32]; diff --git a/src/domain/nostr/event.rs b/src/domain/nostr/event.rs index c75a004e..6d1f7ea8 100644 --- a/src/domain/nostr/event.rs +++ b/src/domain/nostr/event.rs @@ -53,25 +53,17 @@ impl Ord for SortableEventId { /// their target event with an `e` tag; when multiple `e` tags are present, the /// last one identifies the target. Returns `None` if there is no `e` tag. pub fn find_event_id_from_last_e_tag(event: &Event) -> Option { - event - .tags - .filter_standardized(TagKind::SingleLetter(SingleLetterTag::lowercase( - Alphabet::E, - ))) - .last() - .and_then(|tag| match tag { - TagStandard::Event { event_id, .. } => Some(*event_id), - _ => None, - }) + event.tags.event_ids().last() } #[cfg(test)] mod tests { use super::*; + use color_eyre::eyre::Result; #[test] fn test_sortable_event_id_creation() { - let event_id = EventId::all_zeros(); + let event_id = EventId::from_byte_array([0; EventId::LEN]); let timestamp = Timestamp::from(1000); let sortable = SortableEventId::new(event_id, timestamp); @@ -85,9 +77,9 @@ mod tests { let keys = Keys::generate(); let timestamp = Timestamp::from(1234567890); - let event = EventBuilder::text_note("test") + let event = EventBuilder::new(Kind::TextNote, "test") .custom_created_at(timestamp) - .sign_with_keys(&keys)?; + .finalize(&keys)?; let sortable = SortableEventId::from_event(&event); @@ -99,7 +91,7 @@ mod tests { #[test] fn test_sortable_event_id_ordering_by_timestamp() { - let event_id1 = EventId::all_zeros(); + let event_id1 = EventId::from_byte_array([0; EventId::LEN]); let event_id2 = EventId::from_slice(&[1u8; 32]).expect("Valid event ID"); let older = SortableEventId::new(event_id1, Timestamp::from(1000)); @@ -126,7 +118,7 @@ mod tests { #[test] fn test_sortable_event_id_equality() { - let event_id = EventId::all_zeros(); + let event_id = EventId::from_byte_array([0; EventId::LEN]); let timestamp = Timestamp::from(1000); let sortable1 = SortableEventId::new(event_id, timestamp); @@ -142,7 +134,10 @@ mod tests { let mut set: ReverseSortedSet = ReverseSortedSet::new(); - let id1 = SortableEventId::new(EventId::all_zeros(), Timestamp::from(1000)); + let id1 = SortableEventId::new( + EventId::from_byte_array([0; EventId::LEN]), + Timestamp::from(1000), + ); let id2 = SortableEventId::new( EventId::from_slice(&[1u8; 32]).expect("Valid"), Timestamp::from(2000), @@ -165,11 +160,11 @@ mod tests { #[test] fn test_find_event_id_from_last_e_tag() { let keys = Keys::generate(); - let target_id = EventId::all_zeros(); + let target_id = EventId::from_byte_array([0; EventId::LEN]); let event = EventBuilder::new(Kind::Reaction, "+") .tags(vec![Tag::event(target_id)]) - .sign_with_keys(&keys) + .finalize(&keys) .expect("Failed to create event"); let found_id = find_event_id_from_last_e_tag(&event); @@ -179,12 +174,12 @@ mod tests { #[test] fn test_find_event_id_from_last_e_tag_multiple_tags() { let keys = Keys::generate(); - let first_id = EventId::all_zeros(); + let first_id = EventId::from_byte_array([0; EventId::LEN]); let last_id = EventId::from_slice(&[1u8; 32]).expect("Valid event ID"); let event = EventBuilder::new(Kind::Reaction, "+") .tags(vec![Tag::event(first_id), Tag::event(last_id)]) - .sign_with_keys(&keys) + .finalize(&keys) .expect("Failed to create event"); // Should return the last 'e' tag @@ -197,7 +192,7 @@ mod tests { let keys = Keys::generate(); let event = EventBuilder::new(Kind::Reaction, "+") - .sign_with_keys(&keys) + .finalize(&keys) .expect("Failed to create event"); let found_id = find_event_id_from_last_e_tag(&event); diff --git a/src/domain/nostr/nip10.rs b/src/domain/nostr/nip10.rs index 9da0d285..3cbd1384 100644 --- a/src/domain/nostr/nip10.rs +++ b/src/domain/nostr/nip10.rs @@ -1,6 +1,89 @@ -use nostr_sdk::nostr::{Alphabet, SingleLetterTag, TagKind, TagStandard}; use nostr_sdk::prelude::*; +enum TagStandard { + Event { + event_id: EventId, + relay_url: Option, + marker: Option, + public_key: Option, + uppercase: bool, + }, + PublicKey { + public_key: PublicKey, + relay_url: Option, + alias: Option, + uppercase: bool, + }, +} + +impl From for Tag { + fn from(tag: TagStandard) -> Self { + match tag { + TagStandard::Event { + event_id, + relay_url, + marker, + public_key, + uppercase, + } => { + let _ = uppercase; + Nip10Tag::Event { + id: event_id, + relay_hint: relay_url, + marker, + public_key, + } + .into() + } + TagStandard::PublicKey { + public_key, + relay_url, + alias, + uppercase, + } => { + let _ = (alias, uppercase); + Nip01Tag::PublicKey { + public_key, + relay_hint: relay_url, + } + .into() + } + } + } +} + +fn standardized(tag: &Tag) -> Option { + match tag.kind() { + "e" => match Nip10Tag::try_from(tag).ok()? { + Nip10Tag::Event { + id, + relay_hint, + marker, + public_key, + } => Some(TagStandard::Event { + event_id: id, + relay_url: relay_hint, + marker, + public_key, + uppercase: false, + }), + }, + "p" => match Nip01Tag::try_from(tag).ok()? { + Nip01Tag::PublicKey { + public_key, + relay_hint, + } => Some(TagStandard::PublicKey { + public_key, + relay_url: relay_hint, + alias: None, + uppercase: false, + }), + _ => None, + }, + _ => None, + } +} + pub struct ReplyTagsBuilder {} impl ReplyTagsBuilder { @@ -9,22 +92,20 @@ impl ReplyTagsBuilder { .tags .iter() .fold((vec![], vec![], vec![]), |mut acc, tag| { - match tag { - tag if tag.kind() - == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)) => - { + match tag.kind() { + "e" => { if let Some(TagStandard::Event { event_id, relay_url, marker, public_key: _, uppercase: _, - }) = tag.as_standardized() + }) = standardized(tag) { if let Some(Marker::Reply) = marker { acc.0.push(Tag::from(TagStandard::Event { - event_id: *event_id, - relay_url: relay_url.clone(), + event_id, + relay_url, marker: None, public_key: None, uppercase: false, @@ -34,11 +115,7 @@ impl ReplyTagsBuilder { } } } - tag if tag.kind() - == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)) => - { - acc.1.push(tag.clone()) - } + "p" => acc.1.push(tag.clone()), _ => acc.2.push(tag.clone()), } @@ -60,15 +137,10 @@ impl ReplyTagsBuilder { })); if !ptags.iter().any(|tag| { - if tag.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)) { - if let Some(TagStandard::PublicKey { public_key, .. }) = tag.as_standardized() { - *public_key == reply_to.pubkey - } else { - false - } - } else { - false - } + matches!( + standardized(tag), + Some(TagStandard::PublicKey { public_key, .. }) if public_key == reply_to.pubkey + ) }) { ptags.push(Tag::from(TagStandard::PublicKey { public_key: reply_to.pubkey, @@ -84,6 +156,7 @@ impl ReplyTagsBuilder { #[cfg(test)] mod tests { + use color_eyre::eyre::Result; use std::str::FromStr; use pretty_assertions::assert_eq; @@ -305,8 +378,8 @@ mod tests { let author_ptag_count = tags .iter() .filter(|tag| { - if let Some(TagStandard::PublicKey { public_key, .. }) = tag.as_standardized() { - *public_key == author_pubkey + if let Some(TagStandard::PublicKey { public_key, .. }) = standardized(tag) { + public_key == author_pubkey } else { false } @@ -343,8 +416,8 @@ mod tests { "4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25", )?; let has_author_ptag = tags.iter().any(|tag| { - if let Some(TagStandard::PublicKey { public_key, .. }) = tag.as_standardized() { - *public_key == author_pubkey + if let Some(TagStandard::PublicKey { public_key, .. }) = standardized(tag) { + public_key == author_pubkey } else { false } @@ -376,8 +449,8 @@ mod tests { // Check that the root tag preserves relay URL let root_tag = tags.iter().find(|tag| { - if let Some(TagStandard::Event { marker, .. }) = tag.as_standardized() { - *marker == Some(Marker::Root) + if let Some(TagStandard::Event { marker, .. }) = standardized(tag) { + marker == Some(Marker::Root) } else { false } @@ -389,9 +462,9 @@ mod tests { let reply_tag = tags.iter().find(|tag| { if let Some(TagStandard::Event { event_id, marker, .. - }) = tag.as_standardized() + }) = standardized(tag) { - *event_id == event.id && *marker == Some(Marker::Reply) + event_id == event.id && marker == Some(Marker::Reply) } else { false } @@ -428,7 +501,7 @@ mod tests { // Count e-tags let etag_count = tags .iter() - .filter(|tag| matches!(tag.as_standardized(), Some(TagStandard::Event { .. }))) + .filter(|tag| matches!(standardized(tag), Some(TagStandard::Event { .. }))) .count(); // The code only processes e-tags without markers or with "reply" markers in a special way @@ -445,7 +518,7 @@ mod tests { // Verify the root tag exists let has_root = tags.iter().any(|tag| { matches!( - tag.as_standardized(), + standardized(tag), Some(TagStandard::Event { marker: Some(Marker::Root), .. @@ -457,7 +530,7 @@ mod tests { // Verify the new reply tag was added let has_reply = tags.iter().any(|tag| { matches!( - tag.as_standardized(), + standardized(tag), Some(TagStandard::Event { marker: Some(Marker::Reply), .. @@ -496,9 +569,9 @@ mod tests { let previous_reply_tag = tags.iter().find(|tag| { if let Some(TagStandard::Event { event_id, marker, .. - }) = tag.as_standardized() + }) = standardized(tag) { - *event_id == reply_id && marker.is_none() + event_id == reply_id && marker.is_none() } else { false } diff --git a/src/domain/nostr/nip38.rs b/src/domain/nostr/nip38.rs index 04fcaaaa..72e85f33 100644 --- a/src/domain/nostr/nip38.rs +++ b/src/domain/nostr/nip38.rs @@ -40,7 +40,7 @@ impl MusicStatus { /// Build a NIP-38 live status event for the currently playing track. pub fn live_status_builder(&self) -> EventBuilder { - EventBuilder::live_status(self.to_live_status(), self.content()) + LiveStatusEvent::new(self.to_live_status(), self.content()).into_event_builder() } } @@ -225,7 +225,7 @@ mod tests { let keys = Keys::generate(); let event = status .live_status_builder() - .sign_with_keys(&keys) + .finalize(&keys) .expect("Failed to sign live status event"); assert_eq!(event.kind, Kind::UserStatus); diff --git a/src/domain/nostr/profile.rs b/src/domain/nostr/profile.rs index dab83ed5..2be6077a 100644 --- a/src/domain/nostr/profile.rs +++ b/src/domain/nostr/profile.rs @@ -52,6 +52,7 @@ impl Profile { #[cfg(test)] mod tests { use super::*; + use color_eyre::eyre::Result; use pretty_assertions::assert_eq; use rstest::*; use std::str::FromStr; @@ -81,7 +82,7 @@ mod tests { #[case] metadata: Metadata, #[case] expected: Option<&String>, ) -> Result<()> { - let key = nostr_sdk::PublicKey::from_str( + let key = PublicKey::from_str( "4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25", )?; let profile = Profile::new(key, Timestamp::now(), metadata); @@ -98,7 +99,7 @@ mod tests { #[case(Metadata::new().name(""), None)] #[case(Metadata::new().name("foo").display_name("foo"), Some("@foo".to_owned()))] fn test_name(#[case] metadata: Metadata, #[case] expected: Option) -> Result<()> { - let key = nostr_sdk::PublicKey::from_str( + let key = PublicKey::from_str( "4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25", )?; let profile = Profile::new(key, Timestamp::now(), metadata); diff --git a/src/infrastructure.rs b/src/infrastructure.rs index 27d94335..aec95a70 100644 --- a/src/infrastructure.rs +++ b/src/infrastructure.rs @@ -8,5 +8,4 @@ //! - Performance monitoring pub mod cli; -pub mod nostr; pub mod subscription; diff --git a/src/infrastructure/nostr.rs b/src/infrastructure/nostr.rs deleted file mode 100644 index ee333fab..00000000 --- a/src/infrastructure/nostr.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::borrow::Cow; -use std::error::Error; - -use nostr_sdk::prelude::*; -use strum::Display; - -#[derive(Debug, Clone, PartialEq, Eq, Display)] -pub enum PublicKeySignerError { - Readonly, -} - -impl Error for PublicKeySignerError {} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PublicKeySigner { - pubkey: PublicKey, -} - -impl PublicKeySigner { - pub fn new(pubkey: PublicKey) -> Self { - Self { pubkey } - } -} - -impl NostrSigner for PublicKeySigner { - fn backend(&self) -> SignerBackend<'_> { - SignerBackend::Custom(Cow::Borrowed("PublicKeySigner")) - } - - fn get_public_key(&self) -> BoxedFuture<'_, Result> { - Box::pin(async { Ok(self.pubkey) }) - } - - fn sign_event(&self, _unsigned: UnsignedEvent) -> BoxedFuture<'_, Result> { - Box::pin(async { Err(SignerError::backend(PublicKeySignerError::Readonly)) }) - } - - fn nip04_encrypt<'a>( - &'a self, - _public_key: &'a PublicKey, - _content: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async { Err(SignerError::backend(PublicKeySignerError::Readonly)) }) - } - - fn nip04_decrypt<'a>( - &'a self, - _public_key: &'a PublicKey, - _encrypted_content: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async { Err(SignerError::backend(PublicKeySignerError::Readonly)) }) - } - - fn nip44_encrypt<'a>( - &'a self, - _public_key: &'a PublicKey, - _content: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async { Err(SignerError::backend(PublicKeySignerError::Readonly)) }) - } - - fn nip44_decrypt<'a>( - &'a self, - _public_key: &'a PublicKey, - _payload: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async { Err(SignerError::backend(PublicKeySignerError::Readonly)) }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use color_eyre::eyre::eyre; - - fn example_pubkey() -> color_eyre::Result { - Ok(PublicKey::from_slice(&[1u8; 32])?) - } - - #[test] - fn backend_is_custom_public_key_signer() -> color_eyre::Result<()> { - let signer = PublicKeySigner::new(example_pubkey()?); - - match signer.backend() { - SignerBackend::Custom(name) => { - assert_eq!(name.as_ref(), "PublicKeySigner"); - } - other => { - return Err(eyre!(format!("unexpected backend: {other:?}"))); - } - } - - Ok(()) - } - - #[tokio::test] - async fn get_public_key_returns_given_key() -> color_eyre::Result<()> { - let pubkey = example_pubkey()?; - let signer = PublicKeySigner::new(pubkey); - - let got = signer.get_public_key().await?; - assert_eq!(got, pubkey); - - Ok(()) - } - - #[tokio::test] - async fn sign_event_returns_readonly_error() -> color_eyre::Result<()> { - let pubkey = example_pubkey()?; - let signer = PublicKeySigner::new(pubkey); - - let unsigned = UnsignedEvent::new(pubkey, Timestamp::now(), Kind::TextNote, [], "hello"); - - let result = signer.sign_event(unsigned).await; - assert_eq!( - result, - Err(SignerError::backend(PublicKeySignerError::Readonly)) - ); - - Ok(()) - } - - #[tokio::test] - async fn nip04_encrypt_returns_readonly_error() -> color_eyre::Result<()> { - let pubkey = example_pubkey()?; - let signer = PublicKeySigner::new(pubkey); - - let result = signer.nip04_encrypt(&pubkey, "hello").await; - assert_eq!( - result, - Err(SignerError::backend(PublicKeySignerError::Readonly)) - ); - - Ok(()) - } - - #[tokio::test] - async fn nip04_decrypt_returns_readonly_error() -> color_eyre::Result<()> { - let pubkey = example_pubkey()?; - let signer = PublicKeySigner::new(pubkey); - - let result = signer.nip04_decrypt(&pubkey, "payload").await; - assert_eq!( - result, - Err(SignerError::backend(PublicKeySignerError::Readonly)) - ); - - Ok(()) - } - - #[tokio::test] - async fn nip44_encrypt_returns_readonly_error() -> color_eyre::Result<()> { - let pubkey = example_pubkey()?; - let signer = PublicKeySigner::new(pubkey); - - let result = signer.nip44_encrypt(&pubkey, "hello").await; - assert_eq!( - result, - Err(SignerError::backend(PublicKeySignerError::Readonly)) - ); - - Ok(()) - } - - #[tokio::test] - async fn nip44_decrypt_returns_readonly_error() -> color_eyre::Result<()> { - let pubkey = example_pubkey()?; - let signer = PublicKeySigner::new(pubkey); - - let result = signer.nip44_decrypt(&pubkey, "payload").await; - assert_eq!( - result, - Err(SignerError::backend(PublicKeySignerError::Readonly)) - ); - - Ok(()) - } -} diff --git a/src/infrastructure/subscription/nostr.rs b/src/infrastructure/subscription/nostr.rs index 74672332..ea55e55c 100644 --- a/src/infrastructure/subscription/nostr.rs +++ b/src/infrastructure/subscription/nostr.rs @@ -1,5 +1,4 @@ use std::sync::Arc; -use std::time::Duration; use futures::{ stream::{self, BoxStream}, @@ -7,7 +6,7 @@ use futures::{ }; use nostr_sdk::prelude::*; use tears::SubscriptionSource; -use tokio::sync::{broadcast, mpsc, RwLock}; +use tokio::sync::{mpsc, RwLock}; use crate::domain::nostr::feed_filter::{ home_feed_filters, home_load_more_filter, mention_feed_filters, mention_load_more_filter, @@ -16,11 +15,11 @@ use crate::domain::nostr::feed_filter::{ use crate::domain::nostr::FeedKind; use crate::model::nostr_gateway::{CommandError, Message, NostrCommand}; -const DEFAULT_CONTACT_LIST_TIMEOUT_SECS: u64 = 10; - #[derive(Debug, Clone)] pub struct NostrEvents { client: Arc, + pubkey: PublicKey, + keys: Option, /// Cached contact list (following) fetched during initialization /// Shared across all instances via `Arc>` contact_list: Arc>>>, @@ -37,16 +36,19 @@ impl NostrEvents { /// /// ``` /// use std::sync::Arc; - /// use nostr_sdk::Client; + /// use nostr_sdk::prelude::Client; /// use nostui::infrastructure::subscription::nostr::NostrEvents; /// /// let client = Arc::new(Client::default()); - /// let nostr_events = NostrEvents::new(Arc::clone(&client)); + /// let keys = nostr_sdk::prelude::Keys::generate(); + /// let nostr_events = NostrEvents::new(Arc::clone(&client), keys.public_key(), Some(keys)); /// ``` #[must_use] - pub fn new(client: Arc) -> Self { + pub fn new(client: Arc, pubkey: PublicKey, keys: Option) -> Self { Self { client, + pubkey, + keys, contact_list: Arc::new(RwLock::new(None)), } } @@ -56,21 +58,22 @@ impl NostrEvents { /// Sends SubscriptionCreated messages for NostrState to track async fn initialize_home_feed( client: &Client, + pubkey: PublicKey, contact_list_cache: Arc>>>, msg_tx: &mpsc::UnboundedSender, - ) -> broadcast::Receiver { + ) -> BoxStream<'static, ClientNotification> { match client - .get_contact_list_public_keys(Duration::from_secs(DEFAULT_CONTACT_LIST_TIMEOUT_SECS)) + .fetch_events(Filter::new().author(pubkey).kind(Kind::ContactList)) .await { - Ok(mut followings) => { + Ok(events) => { + let mut followings: Vec = events + .into_iter() + .flat_map(|event| event.tags.public_keys().collect::>()) + .collect(); // Always include the user's own posts in the home feed, // even if they don't follow themselves. - if let Ok(signer) = client.signer().await { - if let Ok(own_pubkey) = signer.get_public_key().await { - followings = with_own_pubkey(followings, own_pubkey); - } - } + followings = with_own_pubkey(followings, pubkey); // Cache the contact list (including own pubkey) for future use { @@ -83,9 +86,9 @@ impl NostrEvents { // Subscribe to both feed and profile data concurrently let result = tokio::try_join!( - client.subscribe(feed_backward_filter, None), - client.subscribe(feed_forward_filter, None), - client.subscribe(profile_filter, None) + client.subscribe(feed_backward_filter), + client.subscribe(feed_forward_filter), + client.subscribe(profile_filter) ); if let Ok((sub_id1, sub_id2, sub_id3)) = result { @@ -93,15 +96,15 @@ impl NostrEvents { let feed = FeedKind::Home; let _ = msg_tx.send(Message::SubscriptionCreated { feed: feed.clone(), - subscription_id: sub_id1.val, + subscription_id: sub_id1.value, }); let _ = msg_tx.send(Message::SubscriptionCreated { feed: feed.clone(), - subscription_id: sub_id2.val, + subscription_id: sub_id2.value, }); let _ = msg_tx.send(Message::SubscriptionCreated { feed, - subscription_id: sub_id3.val, + subscription_id: sub_id3.value, }); } @@ -118,16 +121,27 @@ impl NostrEvents { async fn handle_command( cmd: NostrCommand, client: &Client, + pubkey: PublicKey, + keys: Option<&Keys>, contact_list_cache: Arc>>>, msg_tx: &mpsc::UnboundedSender, ) { match cmd { NostrCommand::SendEventBuilder { event_builder } => { - if let Err(e) = client.send_event_builder(event_builder).await { + let result: Result<(), String> = match keys { + Some(keys) => match event_builder.finalize(keys) { + Ok(event) => client + .send_event(&event) + .await + .map(|_| ()) + .map_err(|e| e.to_string()), + Err(e) => Err(e.to_string()), + }, + None => Err(String::from("cannot send events in read-only mode")), + }; + if let Err(e) = result { let _ = msg_tx.send(Message::Error { - error: CommandError::SendEventFailed { - error: e.to_string(), - }, + error: CommandError::SendEventFailed { error: e }, }); } } @@ -170,26 +184,16 @@ impl NostrEvents { return; } }, - FeedKind::Mention => { - let Ok(signer) = client.signer().await else { - log::warn!("No signer available, cannot load more mention events"); - return; - }; - let Ok(own_pubkey) = signer.get_public_key().await else { - log::warn!("Failed to get public key, cannot load more mention events"); - return; - }; - mention_load_more_filter(own_pubkey, since) - } + FeedKind::Mention => mention_load_more_filter(pubkey, since), FeedKind::Author(pubkey) => user_load_more_filter(*pubkey, since), }; - match client.subscribe(filter, None).await { + match client.subscribe(filter).await { Ok(sub_id) => { // Send SubscriptionCreated to track this load-more subscription let _ = msg_tx.send(Message::SubscriptionCreated { feed, - subscription_id: sub_id.val, + subscription_id: sub_id.value, }); } Err(e) => { @@ -203,34 +207,23 @@ impl NostrEvents { log::warn!("Home feed should be initialized, not subscribed via command"); } FeedKind::Mention => { - let Ok(signer) = client.signer().await else { - log::error!("No signer available, cannot subscribe to mention feed"); - return; - }; - let Ok(own_pubkey) = signer.get_public_key().await else { - log::error!( - "Failed to get public key, cannot subscribe to mention feed" - ); - return; - }; - let [backward_filter, forward_filter] = - mention_feed_filters(own_pubkey, Timestamp::now()); + mention_feed_filters(pubkey, Timestamp::now()); let result = tokio::try_join!( - client.subscribe(backward_filter, None), - client.subscribe(forward_filter, None) + client.subscribe(backward_filter), + client.subscribe(forward_filter) ); match result { Ok((sub_id1, sub_id2)) => { let _ = msg_tx.send(Message::SubscriptionCreated { feed: feed.clone(), - subscription_id: sub_id1.val, + subscription_id: sub_id1.value, }); let _ = msg_tx.send(Message::SubscriptionCreated { feed, - subscription_id: sub_id2.val, + subscription_id: sub_id2.value, }); } Err(e) => { @@ -245,8 +238,8 @@ impl NostrEvents { // Subscribe to both filters concurrently let result = tokio::try_join!( - client.subscribe(backward_filter, None), - client.subscribe(forward_filter, None) + client.subscribe(backward_filter), + client.subscribe(forward_filter) ); match result { @@ -254,11 +247,11 @@ impl NostrEvents { // Send SubscriptionCreated messages for both subscriptions let _ = msg_tx.send(Message::SubscriptionCreated { feed: feed.clone(), - subscription_id: sub_id1.val, + subscription_id: sub_id1.value, }); let _ = msg_tx.send(Message::SubscriptionCreated { feed, - subscription_id: sub_id2.val, + subscription_id: sub_id2.value, }); } Err(e) => { @@ -274,8 +267,10 @@ impl NostrEvents { subscription_ids.len() ); for sub_id in subscription_ids { - client.unsubscribe(&sub_id).await; - log::info!("Unsubscribed from {sub_id:?}"); + match client.unsubscribe(&sub_id).await { + Ok(_) => log::info!("Unsubscribed from {sub_id:?}"), + Err(e) => log::warn!("Failed to unsubscribe from {sub_id:?}: {e}"), + } } } NostrCommand::Shutdown => { @@ -287,26 +282,29 @@ impl NostrEvents { /// Main subscription loop that processes notifications and commands async fn run_subscription_loop( client: Client, + pubkey: PublicKey, + keys: Option, contact_list_cache: Arc>>>, msg_tx: mpsc::UnboundedSender, mut cmd_rx: mpsc::UnboundedReceiver, ) { // Initialize the home feed subscription let mut notifications = - Self::initialize_home_feed(&client, Arc::clone(&contact_list_cache), &msg_tx).await; + Self::initialize_home_feed(&client, pubkey, Arc::clone(&contact_list_cache), &msg_tx) + .await; loop { tokio::select! { // Handle incoming notifications from relays - notification = notifications.recv() => { + notification = notifications.next() => { match notification { - Ok(notif) => { + Some(notif) => { if msg_tx.send(Message::Notification(Box::new(notif))).is_err() { // Receiver dropped, exit loop break; } } - Err(_) => { + None => { // Notification channel closed, exit loop break; } @@ -321,7 +319,7 @@ impl NostrEvents { break; } Some(cmd) => { - Self::handle_command(cmd, &client, Arc::clone(&contact_list_cache), &msg_tx).await; + Self::handle_command(cmd, &client, pubkey, keys.as_ref(), Arc::clone(&contact_list_cache), &msg_tx).await; } None => { // Command channel closed, exit loop @@ -344,6 +342,8 @@ impl SubscriptionSource for NostrEvents { // Clone the Arc, not the Client itself let client = Arc::clone(&self.client); + let pubkey = self.pubkey; + let keys = self.keys.clone(); let contact_list_cache = Arc::clone(&self.contact_list); tokio::spawn(async move { @@ -355,8 +355,15 @@ impl SubscriptionSource for NostrEvents { // Run the main subscription loop // Dereference Arc to get &Client for the function call - Self::run_subscription_loop((*client).clone(), contact_list_cache, msg_tx, cmd_rx) - .await; + Self::run_subscription_loop( + (*client).clone(), + pubkey, + keys, + contact_list_cache, + msg_tx, + cmd_rx, + ) + .await; }); stream::unfold(msg_rx, |mut rx| async move { @@ -381,7 +388,7 @@ mod tests { #[tokio::test] async fn test_first_message_is_ready() { let client = Arc::new(Client::default()); - let nostr_events = NostrEvents::new(client); + let nostr_events = NostrEvents::new(client, Keys::generate().public_key(), None); let mut stream = nostr_events.stream(); @@ -399,7 +406,8 @@ mod tests { use tears::SubscriptionSource; let client = Arc::new(Client::default()); - let nostr_events1 = NostrEvents::new(Arc::clone(&client)); + let nostr_events1 = + NostrEvents::new(Arc::clone(&client), Keys::generate().public_key(), None); let nostr_events2 = nostr_events1.clone(); // Same Arc should produce same key @@ -419,7 +427,8 @@ mod tests { ); // Reusing the same Arc should produce the same key - let nostr_events3 = NostrEvents::new(Arc::clone(&client)); + let nostr_events3 = + NostrEvents::new(Arc::clone(&client), Keys::generate().public_key(), None); assert_eq!( nostr_events1.key(), nostr_events3.key(), @@ -435,8 +444,10 @@ mod tests { let client1 = Arc::new(Client::default()); let client2 = Arc::new(Client::default()); - let nostr_events1 = NostrEvents::new(Arc::clone(&client1)); - let nostr_events2 = NostrEvents::new(Arc::clone(&client2)); + let nostr_events1 = + NostrEvents::new(Arc::clone(&client1), Keys::generate().public_key(), None); + let nostr_events2 = + NostrEvents::new(Arc::clone(&client2), Keys::generate().public_key(), None); // Different Arc instances should produce different keys assert_ne!( @@ -461,8 +472,12 @@ mod tests { let client = Client::default(); // Creating separate Arc instances produces different keys - let nostr_events1 = NostrEvents::new(Arc::new(client.clone())); - let nostr_events2 = NostrEvents::new(Arc::new(client)); + let nostr_events1 = NostrEvents::new( + Arc::new(client.clone()), + Keys::generate().public_key(), + None, + ); + let nostr_events2 = NostrEvents::new(Arc::new(client), Keys::generate().public_key(), None); // Different Arc instances should produce different keys assert_ne!( diff --git a/src/main.rs b/src/main.rs index bb213d9d..3ce9917c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use tears::{subscription::time::Timer, FrameRate, Runtime}; use nostui::{ application::config::Config, - infrastructure::{cli::Cli, nostr::PublicKeySigner}, + infrastructure::cli::Cli, runtime::{InitFlags, TearsApp}, utils::{initialize_logging, initialize_panic_handler}, }; @@ -58,15 +58,14 @@ async fn tokio_main() -> Result<()> { let config = Config::new()?; // Create Nostr client - let (client, pubkey) = if config.key.expose_secret().starts_with("npub") { + let (client, pubkey, keys) = if config.key.expose_secret().starts_with("npub") { let pubkey = PublicKey::parse(config.key.expose_secret())?; - let signer = PublicKeySigner::new(pubkey); - (Client::new(signer), pubkey) + (Client::new(), pubkey, None) } else { let keys = Keys::parse(config.key.expose_secret()) .or(Keys::parse(config.privatekey.expose_secret()))?; let pubkey = keys.public_key(); - (Client::new(keys), pubkey) + (Client::new(), pubkey, Some(keys)) }; log::info!("Starting nostui with public key: {pubkey}"); @@ -83,6 +82,7 @@ async fn tokio_main() -> Result<()> { // Create initialization flags for TearsApp let init_flags = InitFlags { pubkey, + keys, config, nostr_client: client, tick_timer: tick_timer_from_rate(args.tick_rate)?, diff --git a/src/model/editor.rs b/src/model/editor.rs index 14dfbf54..fe306e95 100644 --- a/src/model/editor.rs +++ b/src/model/editor.rs @@ -118,8 +118,8 @@ mod tests { fn create_test_event() -> Event { let keys = Keys::generate(); - EventBuilder::text_note("test content") - .sign_with_keys(&keys) + EventBuilder::new(Kind::TextNote, "test content") + .finalize(&keys) .expect("Failed to create test event") } diff --git a/src/model/nostr.rs b/src/model/nostr.rs index 2a76e507..90a67ca3 100644 --- a/src/model/nostr.rs +++ b/src/model/nostr.rs @@ -47,7 +47,7 @@ pub struct Nostr { /// Track subscription IDs for each feed /// The home feed has 3 subscriptions (backward, forward, profile) /// Author feeds have 1 subscription - feed_subscriptions: HashMap>, + feed_subscriptions: HashMap>, } impl Nostr { @@ -246,7 +246,7 @@ mod tests { #[test] fn test_update_event_submitted_returns_send_when_ready() { let mut nostr = Nostr::new(); - let event_builder = EventBuilder::text_note("test"); + let event_builder = EventBuilder::new(Kind::TextNote, "test"); let _ = nostr.update(Message::ConnectionReady); @@ -265,7 +265,7 @@ mod tests { #[test] fn test_update_event_submitted_returns_none_when_not_ready() { let mut nostr = Nostr::new(); - let event_builder = EventBuilder::text_note("test"); + let event_builder = EventBuilder::new(Kind::TextNote, "test"); let outcome = nostr.update(Message::EventSubmitted { event_builder }); diff --git a/src/model/nostr_gateway.rs b/src/model/nostr_gateway.rs index e9148c5e..64e0268a 100644 --- a/src/model/nostr_gateway.rs +++ b/src/model/nostr_gateway.rs @@ -32,7 +32,7 @@ pub enum NostrCommand { Subscribe { feed: FeedKind }, /// Unsubscribe from multiple subscriptions Unsubscribe { - subscription_ids: Vec, + subscription_ids: Vec, }, /// Shutdown the subscription and disconnect from all relays Shutdown, @@ -59,12 +59,12 @@ pub enum Message { sender: mpsc::UnboundedSender, }, /// A notification from the relay pool - Notification(Box), + Notification(Box), /// An error occurred during command execution Error { error: CommandError }, /// A subscription was created for a specific tab SubscriptionCreated { feed: FeedKind, - subscription_id: nostr_sdk::SubscriptionId, + subscription_id: SubscriptionId, }, } diff --git a/src/model/timeline.rs b/src/model/timeline.rs index 2d339fc1..32d6d48f 100644 --- a/src/model/timeline.rs +++ b/src/model/timeline.rs @@ -289,27 +289,35 @@ mod tests { id_bytes[31] = id_suffix; // Make each ID unique // Create a basic text note event - EventBuilder::text_note(content) + EventBuilder::new(Kind::TextNote, content) .custom_created_at(Timestamp::from(timestamp)) - .sign_with_keys(&keys) + .finalize(&keys) .expect("Failed to create test event") } // Helper function to create a reaction event fn create_reaction_event(target_event: &Event, timestamp: u64) -> Event { let keys = Keys::generate(); - EventBuilder::reaction(target_event, "+") + EventBuilder::new(Kind::Reaction, "+") + .tags([ + Tag::event(target_event.id), + Tag::public_key(target_event.pubkey), + ]) .custom_created_at(Timestamp::from(timestamp)) - .sign_with_keys(&keys) + .finalize(&keys) .expect("Failed to create reaction event") } // Helper function to create a repost event fn create_repost_event(target_event: &Event, timestamp: u64) -> Event { let keys = Keys::generate(); - EventBuilder::repost(target_event, None) + EventBuilder::new(Kind::Repost, target_event.as_json()) + .tags([ + Tag::event(target_event.id), + Tag::public_key(target_event.pubkey), + ]) .custom_created_at(Timestamp::from(timestamp)) - .sign_with_keys(&keys) + .finalize(&keys) .expect("Failed to create repost event") } @@ -319,7 +327,7 @@ mod tests { EventBuilder::new(Kind::ZapReceipt, "") .tags(vec![Tag::event(target_event_id)]) .custom_created_at(Timestamp::from(timestamp)) - .sign_with_keys(&keys) + .finalize(&keys) .expect("Failed to create zap receipt event") } diff --git a/src/model/timeline/text_note.rs b/src/model/timeline/text_note.rs index fe0cc4f9..ed41c74d 100644 --- a/src/model/timeline/text_note.rs +++ b/src/model/timeline/text_note.rs @@ -60,13 +60,16 @@ impl TextNote { self.reposts.len() } - fn find_amount(&self, ev: &Event) -> Option { - ev.tags.filter_standardized(TagKind::Amount).last().cloned() + fn find_amount(&self, ev: &Event) -> Option { + ev.tags + .iter() + .filter_map(|tag| Nip57Tag::try_from(tag).ok()) + .find(|tag| matches!(tag, Nip57Tag::Amount { .. })) } pub fn zap_amount(&self) -> u64 { self.zap_receipts.iter().fold(0, |acc, ev| { - if let Some(TagStandard::Amount { millisats, .. }) = self.find_amount(ev) { + if let Some(Nip57Tag::Amount { millisats, .. }) = self.find_amount(ev) { acc + millisats } else { acc @@ -74,31 +77,38 @@ impl TextNote { }) } - pub fn find_reply_tag(&self) -> Option<&TagStandard> { - self.event - .tags - .filter_standardized(TagKind::SingleLetter(SingleLetterTag::lowercase( - Alphabet::E, - ))) - .last() + pub fn find_reply_event_id(&self) -> Option { + self.event.tags.event_ids().last() } - pub fn find_client_tag(&self) -> Option<&TagStandard> { - self.event.tags.find_standardized(TagKind::Client) + pub fn find_client_name(&self) -> Option { + self.event + .tags + .iter() + .find_map(|tag| match Nip89Tag::try_from(tag) { + Ok(Nip89Tag::Client { name, .. }) => Some(name), + Err(_) => None, + }) } - pub fn mentioned_pubkeys(&self) -> impl Iterator { + pub fn mentioned_pubkeys(&self) -> impl Iterator + '_ { self.event.tags.public_keys() } /// Build a NIP-25 `+` reaction event targeting this note. pub fn reaction_builder(&self) -> EventBuilder { - EventBuilder::reaction(&self.event, "+") + EventBuilder::new(Kind::Reaction, "+").tags([ + Tag::event(self.event.id), + Tag::public_key(self.event.pubkey), + ]) } /// Build a NIP-18 repost event for this note. pub fn repost_builder(&self) -> EventBuilder { - EventBuilder::repost(&self.event, None) + EventBuilder::new(Kind::Repost, self.event.as_json()).tags([ + Tag::event(self.event.id), + Tag::public_key(self.event.pubkey), + ]) } pub fn update(&mut self, message: Message) { @@ -119,12 +129,12 @@ impl TextNote { #[cfg(test)] mod tests { use super::*; - use nostr_sdk::nostr::Kind; + use nostr_sdk::prelude::Kind; use std::error::Error; fn create_test_event(content: &str) -> Result> { let keys = Keys::generate(); - Ok(EventBuilder::text_note(content).sign_with_keys(&keys)?) + Ok(EventBuilder::new(Kind::TextNote, content).finalize(&keys)?) } fn create_test_event_with_tags( @@ -134,7 +144,7 @@ mod tests { ) -> Result> { let keys = Keys::generate(); let builder = EventBuilder::new(kind, content).tags(tags); - Ok(builder.sign_with_keys(&keys)?) + Ok(builder.finalize(&keys)?) } fn create_zap_receipt_event( @@ -142,7 +152,7 @@ mod tests { millisats: u64, ) -> Result> { let keys = Keys::generate(); - let amount_tag = Tag::from_standardized(TagStandard::Amount { + let amount_tag = Tag::from(Nip57Tag::Amount { millisats, bolt11: None, }); @@ -150,7 +160,7 @@ mod tests { Ok(EventBuilder::new(Kind::ZapReceipt, "") .tags(vec![amount_tag, event_tag]) - .sign_with_keys(&keys)?) + .finalize(&keys)?) } #[test] @@ -246,12 +256,12 @@ mod tests { let text_note = TextNote::new(event.clone()); let keys = Keys::generate(); - let reaction = text_note.reaction_builder().sign_with_keys(&keys)?; + let reaction = text_note.reaction_builder().finalize(&keys)?; assert_eq!(reaction.kind, Kind::Reaction); assert_eq!(reaction.content, "+"); assert_eq!( - reaction.tags.event_ids().copied().collect::>(), + reaction.tags.event_ids().collect::>(), vec![event.id] ); @@ -264,13 +274,10 @@ mod tests { let text_note = TextNote::new(event.clone()); let keys = Keys::generate(); - let repost = text_note.repost_builder().sign_with_keys(&keys)?; + let repost = text_note.repost_builder().finalize(&keys)?; assert_eq!(repost.kind, Kind::Repost); - assert_eq!( - repost.tags.event_ids().copied().collect::>(), - vec![event.id] - ); + assert_eq!(repost.tags.event_ids().collect::>(), vec![event.id]); Ok(()) } @@ -293,7 +300,7 @@ mod tests { } #[test] - fn test_find_reply_tag() -> Result<(), Box> { + fn test_find_reply_event_id() -> Result<(), Box> { let original_event = create_test_event("Original")?; let reply_event = create_test_event_with_tags( "Reply", @@ -303,32 +310,31 @@ mod tests { let text_note = TextNote::new(reply_event); - assert!(matches!( - text_note.find_reply_tag(), - Some(TagStandard::Event { event_id, .. }) if *event_id == original_event.id - )); + assert_eq!(text_note.find_reply_event_id(), Some(original_event.id)); Ok(()) } #[test] - fn test_find_reply_tag_none() -> Result<(), Box> { + fn test_find_reply_event_id_none() -> Result<(), Box> { let event = create_test_event("Not a reply")?; let text_note = TextNote::new(event); - assert_eq!(text_note.find_reply_tag(), None); + assert_eq!(text_note.find_reply_event_id(), None); Ok(()) } #[test] - fn test_find_client_tag() -> Result<(), Box> { - let client_tag = Tag::custom(TagKind::Client, vec!["TestClient", "https://test.com"]); + fn test_find_client_name() -> Result<(), Box> { + let client_tag = Tag::from(Nip89Tag::Client { + name: String::from("TestClient"), + address: None, + }); let event = create_test_event_with_tags("Hello", Kind::TextNote, vec![client_tag])?; let text_note = TextNote::new(event); - let found_client = text_note.find_client_tag(); - assert!(found_client.is_some()); + assert_eq!(text_note.find_client_name().as_deref(), Some("TestClient")); Ok(()) } @@ -344,7 +350,7 @@ mod tests { let pubkeys: Vec<_> = text_note.mentioned_pubkeys().collect(); assert_eq!(pubkeys.len(), 1); - assert_eq!(*pubkeys[0], mentioned_keys.public_key()); + assert_eq!(pubkeys[0], mentioned_keys.public_key()); Ok(()) } diff --git a/src/presentation/components/home/list.rs b/src/presentation/components/home/list.rs index 892b175f..7d337137 100644 --- a/src/presentation/components/home/list.rs +++ b/src/presentation/components/home/list.rs @@ -87,6 +87,7 @@ impl Default for HomeListComponent { mod tests { use crate::domain::nostr::FeedKind; use crate::model::timeline::Message; + use color_eyre::eyre::Result; use super::*; use nostr_sdk::prelude::*; @@ -104,7 +105,7 @@ mod tests { // Create a note with Japanese text let note_keys = Keys::generate(); let japanese_text = "初force pushめでたい"; - let event = EventBuilder::text_note(japanese_text).sign_with_keys(¬e_keys)?; + let event = EventBuilder::new(Kind::TextNote, japanese_text).finalize(¬e_keys)?; let _ = state.timeline.update(Message::NoteAddedToTab { event, feed: FeedKind::Home, diff --git a/src/presentation/widgets/name_with_handle.rs b/src/presentation/widgets/name_with_handle.rs index 6da37434..53876291 100644 --- a/src/presentation/widgets/name_with_handle.rs +++ b/src/presentation/widgets/name_with_handle.rs @@ -1,16 +1,17 @@ +use nostr_sdk::prelude::PublicKey as NostrPublicKey; use ratatui::prelude::*; use crate::{domain::nostr::Profile, presentation::widgets::public_key::PublicKey}; pub struct NameWithHandle { - pubkey: nostr_sdk::PublicKey, + pubkey: NostrPublicKey, display_name: Option, handle: Option, highlighted: bool, } impl NameWithHandle { - pub fn new(pubkey: nostr_sdk::PublicKey, profile: Option<&Profile>, highlighted: bool) -> Self { + pub fn new(pubkey: NostrPublicKey, profile: Option<&Profile>, highlighted: bool) -> Self { Self { pubkey, display_name: profile.and_then(|profile| profile.display_name()).cloned(), diff --git a/src/presentation/widgets/public_key.rs b/src/presentation/widgets/public_key.rs index 57c0a637..caf7a9c7 100644 --- a/src/presentation/widgets/public_key.rs +++ b/src/presentation/widgets/public_key.rs @@ -1,14 +1,14 @@ -use nostr_sdk::ToBech32; +use nostr_sdk::prelude::{PublicKey as NostrPublicKey, ToBech32}; use ratatui::prelude::*; use crate::domain::text::shorten_npub; pub struct PublicKey { - key: nostr_sdk::PublicKey, + key: NostrPublicKey, } impl PublicKey { - pub fn new(key: nostr_sdk::PublicKey) -> Self { + pub fn new(key: NostrPublicKey) -> Self { Self { key } } @@ -37,7 +37,7 @@ mod tests { #[test] fn test_new() -> Result<()> { - let key = nostr_sdk::PublicKey::from_str( + let key = NostrPublicKey::from_str( "4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25", )?; let publickey = PublicKey::new(key); @@ -49,7 +49,7 @@ mod tests { #[test] fn test_shortened() -> Result<()> { - let key = nostr_sdk::PublicKey::from_str( + let key = NostrPublicKey::from_str( "4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25", )?; let publickey = PublicKey::new(key); @@ -60,7 +60,7 @@ mod tests { #[test] fn test_from_public_key_to_text() -> Result<()> { - let key = nostr_sdk::PublicKey::from_str( + let key = NostrPublicKey::from_str( "4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25", )?; let publickey = PublicKey::new(key); @@ -74,7 +74,7 @@ mod tests { #[test] fn test_render_public_key_widget() -> Result<()> { - let key = nostr_sdk::PublicKey::from_str( + let key = NostrPublicKey::from_str( "4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25", )?; let publickey = PublicKey::new(key); @@ -111,7 +111,7 @@ mod tests { ]; for key_str in keys { - let key = nostr_sdk::PublicKey::from_str(key_str)?; + let key = NostrPublicKey::from_str(key_str)?; let publickey = PublicKey::new(key); let shortened = publickey.shortened(); diff --git a/src/presentation/widgets/tab_bar.rs b/src/presentation/widgets/tab_bar.rs index de8d8ae3..1767795a 100644 --- a/src/presentation/widgets/tab_bar.rs +++ b/src/presentation/widgets/tab_bar.rs @@ -51,7 +51,7 @@ mod tests { use crate::domain::nostr::{FeedKind, Profile}; use crate::model::timeline::Message; use crate::model::timeline::Timeline; - use nostr_sdk::nostr::Metadata; + use nostr_sdk::prelude::Metadata; fn create_test_pubkey() -> PublicKey { PublicKey::from_hex("4d39c23b3b03bf99494df5f3a149c7908ae1bc7416807fdd6b34a31886eaae25") diff --git a/src/presentation/widgets/text_note.rs b/src/presentation/widgets/text_note.rs index 13c3b251..282d5dc8 100644 --- a/src/presentation/widgets/text_note.rs +++ b/src/presentation/widgets/text_note.rs @@ -51,7 +51,7 @@ impl<'a> TextNoteWidget<'a> { fn fixed_lines(&self) -> u16 { let base = Self::NAME_LINES + Self::META_LINES + Self::STATS_LINES + Self::SEPARATOR_LINES; - if self.text_note.find_reply_tag().is_some() { + if self.text_note.find_reply_event_id().is_some() { base + Self::REPLY_ANNOTATION_LINES } else { base @@ -64,7 +64,7 @@ impl<'a> TextNoteWidget<'a> { .map(|pubkey| { self.ctx .profiles - .get(pubkey) + .get(&pubkey) .map(|p| p.name()) .unwrap_or_else(|| { let Ok(npub) = pubkey.to_bech32(); @@ -118,7 +118,7 @@ impl<'a> Widget for TextNoteWidget<'a> { // `fixed_lines`; keep the two in sync when changing the layout. let mut text = Text::default(); - if let Some(TagStandard::Event { event_id, .. }) = self.text_note.find_reply_tag() { + if let Some(event_id) = self.text_note.find_reply_event_id() { let mentioned_names = self.mentioned_names(); let reply_text = if mentioned_names.is_empty() { let Ok(note1) = event_id.to_bech32(); @@ -150,8 +150,8 @@ impl<'a> Widget for TextNoteWidget<'a> { text.extend(content); let created_at = self.formatted_created_at(); - let meta = match self.text_note.find_client_tag() { - Some(TagStandard::Client { name, .. }) => format!("{created_at} | via {name}"), + let meta = match self.text_note.find_client_name() { + Some(name) => format!("{created_at} | via {name}"), _ => created_at, }; text.extend(Text::from(Line::styled( @@ -178,19 +178,19 @@ impl<'a> Widget for TextNoteWidget<'a> { #[cfg(test)] mod tests { use super::*; - use nostr_sdk::nostr::{EventBuilder, Keys}; + use nostr_sdk::prelude::{EventBuilder, Keys}; use std::collections::HashMap; use std::error::Error; fn create_test_event(content: &str) -> Result> { let keys = Keys::generate(); - Ok(EventBuilder::text_note(content).sign_with_keys(&keys)?) + Ok(EventBuilder::new(Kind::TextNote, content).finalize(&keys)?) } fn create_test_event_with_tags(content: &str, tags: Vec) -> Result> { let keys = Keys::generate(); - let builder = EventBuilder::text_note(content).tags(tags); - Ok(builder.sign_with_keys(&keys)?) + let builder = EventBuilder::new(Kind::TextNote, content).tags(tags); + Ok(builder.finalize(&keys)?) } fn create_test_profile(name: &str, display_name: Option<&str>) -> Profile { @@ -581,7 +581,10 @@ mod tests { #[test] fn test_render_with_client_tag() -> Result<(), Box> { - let client_tag = Tag::custom(TagKind::Client, vec!["TestClient", "https://test.com"]); + let client_tag = Tag::from(Nip89Tag::Client { + name: String::from("TestClient"), + address: None, + }); let event = create_test_event_with_tags("Test", vec![client_tag])?; let text_note = TextNote::new(event); diff --git a/src/runtime.rs b/src/runtime.rs index c88368a0..13822b41 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -24,6 +24,7 @@ use crate::presentation::components::Components; #[derive(Debug)] pub struct InitFlags { pub pubkey: PublicKey, + pub keys: Option, pub config: Config, pub nostr_client: Client, pub tick_timer: Timer, @@ -41,6 +42,10 @@ pub struct TearsApp<'a> { components: RefCell, /// Nostr client (wrapped in Arc for sharing across subscriptions) nostr_client: Arc, + /// Current account's public key, used for read-only subscriptions. + pubkey: PublicKey, + /// Private keys used to sign outbound events, if the app is not in read-only mode. + keys: Option, /// Configuration (including keybindings) config: Config, /// Timer subscription source for application ticks @@ -69,7 +74,9 @@ impl<'a> Application for TearsApp<'a> { state, components: RefCell::new(components), nostr_client, + pubkey: flags.pubkey, config, + keys: flags.keys, tick_timer: flags.tick_timer, }; @@ -111,8 +118,12 @@ impl<'a> Application for TearsApp<'a> { // NostrEvents subscription - reuse the same Arc across frames // This ensures the subscription ID remains constant and the subscription // is not recreated every frame - Subscription::new(NostrEvents::new(Arc::clone(&self.nostr_client))) - .map(|msg| AppMsg::Nostr(NostrMsg::SubscriptionMessage(msg))), + Subscription::new(NostrEvents::new( + Arc::clone(&self.nostr_client), + self.pubkey, + self.keys.clone(), + )) + .map(|msg| AppMsg::Nostr(NostrMsg::SubscriptionMessage(msg))), // Timer subscription - interval validated during initialization Subscription::new(self.tick_timer.clone()).map(|msg| match msg { TimerEvent::Tick => AppMsg::System(SystemMsg::Tick), @@ -397,7 +408,7 @@ impl<'a> TearsApp<'a> { // to properly route them to the correct tab. // Ideally, we would cache all events globally and use `Event`, but that would // require a significant architectural change. - RelayPoolNotification::Event { + ClientNotification::Event { event, subscription_id, .. @@ -408,13 +419,13 @@ impl<'a> TearsApp<'a> { ); Command::none() } - RelayPoolNotification::Message { message, .. } => { + ClientNotification::Message { message, .. } => { log::debug!("Received relay message: {message:?}"); if let RelayMessage::Event { subscription_id, event, - } = message + } = *message { self.state .route_relay_event(&subscription_id, event.into_owned()) @@ -422,7 +433,7 @@ impl<'a> TearsApp<'a> { Command::none() } } - RelayPoolNotification::Shutdown => self.state.notify_subscription_shutdown(), + ClientNotification::Shutdown => self.state.notify_subscription_shutdown(), }, NostrSubscriptionMessage::Error { error } => { self.state.notify_subscription_error(error) @@ -450,6 +461,7 @@ mod tests { let flags = InitFlags { pubkey: keys.public_key(), + keys: Some(keys), config, nostr_client: client, tick_timer: Timer::new(NonZeroU64::new(62).expect("non-zero")), @@ -465,8 +477,8 @@ mod tests { // Add a test note to allow selection let keys = Keys::generate(); - let event = EventBuilder::text_note("test note") - .sign_with_keys(&keys) + let event = EventBuilder::new(Kind::TextNote, "test note") + .finalize(&keys) .expect("Failed to sign test event"); let _ = app .state @@ -499,8 +511,8 @@ mod tests { // Add a test note to allow selection let keys = Keys::generate(); - let event = EventBuilder::text_note("test note") - .sign_with_keys(&keys) + let event = EventBuilder::new(Kind::TextNote, "test note") + .finalize(&keys) .expect("Failed to sign test event"); let _ = app .state @@ -557,11 +569,11 @@ mod tests { // Add test notes to timeline let keys = Keys::generate(); - let event1 = EventBuilder::text_note("test note 1") - .sign_with_keys(&keys) + let event1 = EventBuilder::new(Kind::TextNote, "test note 1") + .finalize(&keys) .expect("Failed to sign test event"); - let event2 = EventBuilder::text_note("test note 2") - .sign_with_keys(&keys) + let event2 = EventBuilder::new(Kind::TextNote, "test note 2") + .finalize(&keys) .expect("Failed to sign test event"); let _ = app .state @@ -589,11 +601,11 @@ mod tests { // Add test notes to timeline let keys = Keys::generate(); - let event1 = EventBuilder::text_note("test note 1") - .sign_with_keys(&keys) + let event1 = EventBuilder::new(Kind::TextNote, "test note 1") + .finalize(&keys) .expect("Failed to sign test event"); - let event2 = EventBuilder::text_note("test note 2") - .sign_with_keys(&keys) + let event2 = EventBuilder::new(Kind::TextNote, "test note 2") + .finalize(&keys) .expect("Failed to sign test event"); let _ = app .state @@ -622,8 +634,8 @@ mod tests { // Add a test note let keys = Keys::generate(); - let event = EventBuilder::text_note("test note") - .sign_with_keys(&keys) + let event = EventBuilder::new(Kind::TextNote, "test note") + .finalize(&keys) .expect("Failed to sign test event"); let _ = app .state @@ -642,11 +654,11 @@ mod tests { // Add test notes let keys = Keys::generate(); - let event1 = EventBuilder::text_note("test note 1") - .sign_with_keys(&keys) + let event1 = EventBuilder::new(Kind::TextNote, "test note 1") + .finalize(&keys) .expect("Failed to sign test event"); - let event2 = EventBuilder::text_note("test note 2") - .sign_with_keys(&keys) + let event2 = EventBuilder::new(Kind::TextNote, "test note 2") + .finalize(&keys) .expect("Failed to sign test event"); let _ = app .state From e6b4eff8b4ac3d3f8d6bccd7eaf564e89bbda511 Mon Sep 17 00:00:00 2001 From: Akiomi Kamakura Date: Tue, 11 Aug 2026 22:17:59 +0900 Subject: [PATCH 3/3] fix: bound contact list lookup --- src/infrastructure/subscription/nostr.rs | 49 +++++++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/infrastructure/subscription/nostr.rs b/src/infrastructure/subscription/nostr.rs index ea55e55c..36ac7b26 100644 --- a/src/infrastructure/subscription/nostr.rs +++ b/src/infrastructure/subscription/nostr.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{collections::BTreeSet, sync::Arc, time::Duration}; use futures::{ stream::{self, BoxStream}, @@ -15,6 +15,15 @@ use crate::domain::nostr::feed_filter::{ use crate::domain::nostr::FeedKind; use crate::model::nostr_gateway::{CommandError, Message, NostrCommand}; +const CONTACT_LIST_TIMEOUT: Duration = Duration::from_secs(10); + +fn followings_from_latest_contact_list(events: &BTreeSet) -> Vec { + events + .first() + .map(|event| event.tags.public_keys().collect()) + .unwrap_or_default() +} + #[derive(Debug, Clone)] pub struct NostrEvents { client: Arc, @@ -62,15 +71,18 @@ impl NostrEvents { contact_list_cache: Arc>>>, msg_tx: &mpsc::UnboundedSender, ) -> BoxStream<'static, ClientNotification> { + let filter = Filter::new() + .author(pubkey) + .kind(Kind::ContactList) + .limit(1); + match client - .fetch_events(Filter::new().author(pubkey).kind(Kind::ContactList)) + .fetch_events(filter) + .timeout(CONTACT_LIST_TIMEOUT) .await { Ok(events) => { - let mut followings: Vec = events - .into_iter() - .flat_map(|event| event.tags.public_keys().collect::>()) - .collect(); + let mut followings = followings_from_latest_contact_list(&events); // Always include the user's own posts in the home feed, // even if they don't follow themselves. followings = with_own_pubkey(followings, pubkey); @@ -385,6 +397,31 @@ mod tests { use super::*; use futures::StreamExt; + #[test] + fn followings_use_only_the_latest_contact_list() { + let author = Keys::generate(); + let unfollowed = Keys::generate(); + let following = Keys::generate(); + + let old_contact_list = EventBuilder::new(Kind::ContactList, "") + .tags([Tag::public_key(unfollowed.public_key())]) + .custom_created_at(Timestamp::from(1)) + .finalize(&author) + .expect("valid contact list event"); + let latest_contact_list = EventBuilder::new(Kind::ContactList, "") + .tags([Tag::public_key(following.public_key())]) + .custom_created_at(Timestamp::from(2)) + .finalize(&author) + .expect("valid contact list event"); + + let events = BTreeSet::from([old_contact_list, latest_contact_list]); + + assert_eq!( + followings_from_latest_contact_list(&events), + vec![following.public_key()] + ); + } + #[tokio::test] async fn test_first_message_is_ready() { let client = Arc::new(Client::default());