Feat: File-attachment and custom wrapped messages#29
Conversation
- Add ChatAttachment model and attachment field on DisputeChatMessage - Parse image_encrypted/file_encrypted JSON in apply_admin_chat_updates; set attachment_toast for new attachments - Blossom module: blossom_url_to_https, fetch_blob, ChaCha20-Poly1305 decrypt, save_attachment_to_disk, spawn_save_attachment - Ctrl+S in dispute chat saves selected attachment to ~/.mostrix/downloads/ (optional decrypt when key provided) - UI: chat block title file count, transient toast, footer hint "Ctrl+S: Save file", yellow styling for attachment lines - Transcript placeholder for attachments; clear toast on key press - Docs: ADMIN_DISPUTES and MESSAGE_FLOW_AND_PROTOCOL updated - Deps: chacha20poly1305 (reqwest already present)
Derive per-dispute shared keys via generate_shared_key(admin_secret, counterparty_pubkey) and persist them in admin_disputes. Admin chat messages are now addressed to the shared key pubkey instead of trade pubkeys, mirroring the mostro-chat model.
- Admin chat: custom NIP-44 gift wrap (ephemeral key, PoW from settings) for send; unwrap uses NIP-44 only to match mostro protocol chat - Attachments: derive shared key in blossom (ECDH admin_sk + sender_pubkey); Ctrl+S auto-derives decryption key from dispute buyer/seller pubkey when missing - Helpers: support hex (24 chars) or base64 nonce for attachment JSON - UI: Seller chat messages use Magenta instead of Red for readability - Models: log error when buyer/seller pubkeys differ but derived shared keys are identical (guards against bad relay data) - chat_utils: add unit test for different users -> different shared keys; move test module to end of file (clippy items_after_test_module) - Deps: reqwest http2 feature - Docs: ADMIN_DISPUTES (Magenta, shared-key validation); KEY_MANAGEMENT (validation) Co-authored-by: Cursor <cursoragent@cursor.com>
WalkthroughThis PR moves admin chat to per-dispute ECDH-derived shared keys stored in the DB, adds ChaCha20-Poly1305 Blossom attachment download/decrypt/save flow, updates migrations/models/UI/input to handle shared-key chat and attachments, and wires a Ctrl+S save flow with a background save task. Changes
Sequence Diagram(s)sequenceDiagram
participant Admin
participant App as App State
participant DB as Database
participant Utils as Chat Utils
participant Relay as Relay Network
participant Blossom as Blossom Server
Admin->>App: create/load AdminDispute
App->>Utils: derive_shared_keys(admin_keys, party_pubkey)
Utils->>DB: store buyer/seller_shared_key_hex
Admin->>App: input chat message
App->>Utils: send_admin_chat_message_via_shared_key(shared_key_hex)
Utils->>Relay: publish GiftWrap addressed to shared_key_pubkey
App->>Utils: fetch_admin_chat_updates(disputes, last_seen)
Utils->>DB: rebuild Keys from shared_key_hex
Utils->>Relay: query GiftWraps for shared_key_pubkey
Relay->>Utils: events
Utils->>App: AdminChatUpdate (messages, attachments)
Admin->>App: Ctrl+S on selected attachment
App->>Blossom: fetch_blob(blossom_url)
Blossom->>App: encrypted blob
App->>Utils: decrypt_blob(shared_key, blob)
Utils->>App: decrypted file
App->>DB: save_attachment_to_disk (~/.mostrix/downloads)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docs/KEY_MANAGEMENT.md (1)
79-99:⚠️ Potential issue | 🟡 MinorDuplicate
## Database Persistencesection header.Lines 79 and 99 both use
## Database Persistence. The second instance (line 99) covers an entirely separate topic (the critical mnemonic/index persistence warning). The duplicate heading breaks Markdown anchor navigation and makes both sections hard to link to.📝 Suggested fix
-## Database Persistence - -Maintaining the state of trade indices is **critical**. If the `trade_index` associated with an order is lost… +## Critical: Trade Index and Mnemonic Persistence + +Maintaining the state of trade indices is **critical**. If the `trade_index` associated with an order is lost…🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/KEY_MANAGEMENT.md` around lines 79 - 99, The second identical section header "## Database Persistence" duplicates the earlier one and should be renamed to a distinct, descriptive title (e.g., "## Mnemonic and Index Persistence Warning" or similar) so anchors and links don't collide; update any internal links or references that point to that header accordingly and verify the surrounding content mentioning the critical mnemonic/index persistence warning still reads correctly.docs/TUI_INTERFACE.md (1)
217-222:⚠️ Potential issue | 🟡 Minor
DisputeChatMessagedoc snippet is missing the newattachmentfield.This PR adds
attachment: Option<ChatAttachment>toDisputeChatMessage, but the struct shown in this documentation block still reflects the old shape. Consider adding it so the docs stay in sync.Suggested update
pub struct DisputeChatMessage { pub sender: ChatSender, pub content: String, pub timestamp: i64, pub target_party: Option<ChatParty>, // For Admin messages: which party this was sent to + pub attachment: Option<ChatAttachment>, // Parsed file/image attachment metadata }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/TUI_INTERFACE.md` around lines 217 - 222, The docs snippet for DisputeChatMessage is out of sync: update the struct shown in TUI_INTERFACE.md to include the new field attachment: Option<ChatAttachment>. Locate the DisputeChatMessage example in the docs and add the attachment field with the correct type and optionality (matching the code's DisputeChatMessage and ChatAttachment symbols) so the documentation reflects the current struct shape.src/models.rs (1)
520-527:⚠️ Potential issue | 🟠 MajorAdd
WHERE status = ?clause to filter by InProgress disputes.The SQL query contains no
?placeholder, so the.bind(DisputeStatus::InProgress.to_string())call on line 525 has no effect. The function returns all disputes instead of only InProgress ones. Evidence suggests filtering was intended: the result is assigned toapp.admin_disputes_in_progress(lines 192 and 270 in src/main.rs), andDisputeStatus::InProgressis explicitly bound. AddWHERE status = ?to the query to fix this.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/models.rs` around lines 520 - 527, In get_all (pub async fn get_all(pool: &SqlitePool) -> Result<Vec<AdminDispute>>), the SQL query passed to sqlx::query_as::<_, AdminDispute> is missing a WHERE clause so the subsequent .bind(DisputeStatus::InProgress.to_string()) is ignored; update the query string to include "WHERE status = ?" before the "ORDER BY taken_at DESC" so the bind filters rows to DisputeStatus::InProgress and then keep the existing .bind(...) and .fetch_all(pool).await? handling.
🧹 Nitpick comments (5)
Cargo.toml (1)
32-32: Consider bumping reqwest minimum version spec to"0.13.2".The latest published reqwest version is
0.13.2. The current"0.13.1"spec resolves to^0.13.1so Cargo.lock will already pick up 0.13.2, but aligning the spec with the actual minimum tested version avoids regressing to a known-older patch in edge-case resolution scenarios.The
native-tls+http2feature combination is valid: reqwest 0.13 madenative-tlsimply ALPN automatically, so most users will now get HTTP/2 upgrades. Thehttp2feature is a default feature in reqwest 0.13, so it must be listed explicitly whendefault-features = false, which this declaration correctly does.✏️ Optional: bump minimum version
-reqwest = { version = "0.13.1", default-features = false, features = ["native-tls", "json", "http2"] } +reqwest = { version = "0.13.2", default-features = false, features = ["native-tls", "json", "http2"] }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Cargo.toml` at line 32, Update the reqwest dependency spec in Cargo.toml from "0.13.1" to "0.13.2" by changing the line defining reqwest = { version = "0.13.1", default-features = false, features = ["native-tls", "json", "http2"] } so the minimum required reqwest version matches the latest published 0.13.2.src/util/chat_utils.rs (1)
229-231:_admin_keysis a dead parameter in the public API surface.The parameter is never read in the function body. Since this is a
pub async fn, the dead parameter pollutes the call-site signature and every caller must pass a reference they know is ignored. Consider removing it or replacing it with_: &Keysif it must be kept for interface stability.♻️ Proposed removal
pub async fn fetch_admin_chat_updates( client: &Client, - _admin_keys: &Keys, disputes: &[AdminDispute], admin_chat_last_seen: &HashMap<(String, ChatParty), AdminChatLastSeen>, ) -> Result<Vec<AdminChatUpdate>, anyhow::Error> {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/util/chat_utils.rs` around lines 229 - 231, The public async function fetch_admin_chat_updates currently accepts an unused parameter _admin_keys; either remove this parameter from the function signature (and update all callers to stop passing it) or explicitly mark it as ignored by renaming to _: &Keys to preserve the API while signaling it's unused; update the function declaration for fetch_admin_chat_updates and any call sites accordingly, and run a build to ensure no remaining references or warnings.src/ui/key_handler/mod.rs (1)
168-211: Ctrl+S save-attachment handler looks correct; consider logging when derivation is skipped.The nested guard structure is clear. The ECDH fallback derivation for missing decryption keys (lines 182–203) correctly maps
ChatSender::Buyer→dispute.buyer_pubkeyandChatSender::Seller→dispute.seller_pubkey, skippingAdminsenders.One minor observation: if the pubkey string is present but fails to parse (line 195), or if
derive_shared_keyfails (line 196), the attachment is still sent to the channel without a decryption key, resulting in an.encfile being saved without decryption. This is the intended fallback per the PR description, but there's no user-visible indication that key derivation failed — only theblossom.rssave path will name the file.enc. Consider adding alog::warn!in the error branches so the admin can diagnose why a file wasn't decrypted.🔧 Optional: add logging for derivation failures
if let Ok(sender_pk) = PublicKey::parse(pk_str) { if let Ok(shared) = crate::util::blossom::derive_shared_key( admin_keys, &sender_pk, ) { attachment.decryption_key = Some(shared.to_vec()); + } else { + log::warn!("Failed to derive shared key for attachment in dispute {}", dispute_id_key); } + } else { + log::warn!("Failed to parse sender pubkey for attachment decryption: {}", pk_str); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/key_handler/mod.rs` around lines 168 - 211, The handler sends attachments even when pubkey parsing (PublicKey::parse for pk_str) or ECDH key derivation (crate::util::blossom::derive_shared_key using admin_keys) fails; add explicit logging in those failure branches so admins see why decryption_key remained None. Specifically, inside the block handling attachment.decryption_key.is_none() (the match on msg.sender and the nested if-let Ok(sender_pk) / if-let Ok(shared) branches), add log::warn! messages when PublicKey::parse(pk_str) returns Err and when derive_shared_key returns Err (include dispute.dispute_id and msg.sender/context), then proceed to send via tx.send((dispute_id_key, attachment)) as before.src/ui/tabs/disputes_in_progress_tab.rs (1)
461-468: Side effect in render function — acceptable but worth noting.Clearing
attachment_toastinsiderender_disputes_in_progressis a side effect in what is conceptually a render pass. This works fine here sinceappis&mut, but if you ever refactor toward a stricter model/view separation, this expiry logic would belong in the main loop or a dedicated tick handler.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/tabs/disputes_in_progress_tab.rs` around lines 461 - 468, The render_disputes_in_progress function currently mutates app by clearing app.attachment_toast when the toast has expired, which is a side effect that belongs in your main update/tick path; move the expiry logic out of render_disputes_in_progress and into a centralized timer/tick handler (e.g., the main event loop or a new handle_tick/update_timers function) that runs before rendering, and have render_disputes_in_progress only read app.attachment_toast; locate the code that checks app.attachment_toast and ATTACHMENT_TOAST_DURATION and extract it into that tick/update function (or call a small helper like expire_attachment_toast(&mut app)) so behavior is preserved without mutating state during render.src/ui/helpers.rs (1)
271-316: Consider logging when attachment JSON is partially valid but unparseable.If the JSON has a recognized
"type"(image_encrypted/file_encrypted) but is missing a required field (e.g.,blossom_url,nonce,filename), the function silently returnsNoneand the raw JSON blob is shown to the user as a plain chat message. Alog::warnbefore returningNoneafter matching the type would help diagnose relay-side or sender-side issues.Sketch
let (file_type, icon) = match msg_type { "image_encrypted" => (ChatAttachmentType::Image, "🖼"), "file_encrypted" => (ChatAttachmentType::File, "📎"), _ => return None, }; - let blossom_url = obj.get("blossom_url")?.as_str()?.to_string(); - let nonce_str = obj.get("nonce")?.as_str()?; - let nonce = decode_nonce_12(nonce_str)?; - let filename = obj.get("filename")?.as_str()?.to_string(); + let blossom_url = match obj.get("blossom_url").and_then(|v| v.as_str()) { + Some(u) => u.to_string(), + None => { log::warn!("Attachment JSON missing blossom_url"); return None; } + }; + let nonce_str = match obj.get("nonce").and_then(|v| v.as_str()) { + Some(n) => n, + None => { log::warn!("Attachment JSON missing nonce"); return None; } + }; + let nonce = match decode_nonce_12(nonce_str) { + Some(n) => n, + None => { log::warn!("Attachment JSON has invalid nonce: {}", nonce_str); return None; } + }; + let filename = match obj.get("filename").and_then(|v| v.as_str()) { + Some(f) => f.to_string(), + None => { log::warn!("Attachment JSON missing filename"); return None; } + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/helpers.rs` around lines 271 - 316, In try_parse_attachment_message, when msg_type matches "image_encrypted" or "file_encrypted" but subsequent required fields (blossom_url, nonce, filename, or key decoding) are missing causing the function to return None, add a log::warn that includes the msg_type and the original content (or which key was missing) so partial/invalid attachment JSON is visible in logs; place this warning immediately before returning None in those failure paths after extracting msg_type (reference symbols: try_parse_attachment_message, msg_type, blossom_url, nonce, filename, decryption_key).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/ADMIN_DISPUTES.md`:
- Around line 691-692: Update the stale color reference in the message-header
description: change the "Red for Seller" phrase in the "Sender - date - time"
line to "Magenta for Seller" so it matches the earlier "Seller (Magenta)"
wording (i.e., keep "Cyan for Admin, Green for Buyer, Magenta for Seller"
consistent).
In `@README.md`:
- Line 123: Update the keyboard shortcuts list entry that currently reads "Use
Tab to switch chat view, Shift+I to enable/disable chat input, PageUp/ PageDown
to scroll, End to jump to latest. Press Shift+F to open the finalization popup."
to include the new Ctrl+S shortcut; explicitly document that pressing Ctrl+S
saves the selected attachment to ~/.mostrix/downloads/ (e.g., add ", Ctrl+S to
save selected attachment to ~/.mostrix/downloads/" to that sentence) so readers
of the README can discover the feature.
In `@src/ui/chat.rs`:
- Line 47: ChatAttachment currently has a dead field nonce: Vec<u8> that is
never used in decryption (decrypt_blob reads nonce from the downloaded blob) and
is discarded by spawn_save_attachment; either remove the nonce field from the
ChatAttachment struct or explicitly document it as metadata-only (add a doc
comment above ChatAttachment::nonce) so intent is clear; update any construction
sites that set nonce (e.g., places that build ChatAttachment) to stop providing
it if removed, and run cargo check to ensure no remaining references to
ChatAttachment::nonce remain.
In `@src/util/blossom.rs`:
- Around line 64-83: The code currently calls res.bytes().await which buffers
the entire response before checking max_bytes; change to first inspect
res.headers().get("content-length") and error if it exceeds max_bytes, and then
stream the body via res.bytes_stream() (or equivalent) and consume chunks
incrementally, summing their lengths and returning an error if the running total
exceeds max_bytes; update the error mapping (where map_err is used) to return
the same anyhow! style on streaming/early-termination failure and keep the
existing variables client, url, timeout, and max_bytes to locate the callsite.
In `@src/util/chat_utils.rs`:
- Around line 212-215: The loop in chat_utils.rs currently drops messages when
ts <= last_seen which can permanently skip messages that share the same
timestamp; change the boundary check to use ts < last_seen so messages with ts
== last_seen are retained (they may be deduplicated later by the UI), and as a
follow-up consider deduplicating by event id (use the message/event id field)
instead of relying on timestamps for correctness.
- Around line 153-161: The hardcoded limit(100) on the Filter (created via
Filter::new() with Kind::GiftWrap, pubkey shared_keys.public_key(), since
Timestamp::from(wide_since)) can silently drop messages; replace this single
fixed batch with either a larger configurable batch size constant or,
preferably, implement pagination: request events in repeated batches (using the
same Filter parameters but advancing the time window or cursor between requests)
and accumulate results until a batch is smaller than the requested size,
ensuring no events within the wide_since window are missed; make the batch size
configurable and document/update places that rely on the previous 100-item
behavior.
---
Outside diff comments:
In `@docs/KEY_MANAGEMENT.md`:
- Around line 79-99: The second identical section header "## Database
Persistence" duplicates the earlier one and should be renamed to a distinct,
descriptive title (e.g., "## Mnemonic and Index Persistence Warning" or similar)
so anchors and links don't collide; update any internal links or references that
point to that header accordingly and verify the surrounding content mentioning
the critical mnemonic/index persistence warning still reads correctly.
In `@docs/TUI_INTERFACE.md`:
- Around line 217-222: The docs snippet for DisputeChatMessage is out of sync:
update the struct shown in TUI_INTERFACE.md to include the new field attachment:
Option<ChatAttachment>. Locate the DisputeChatMessage example in the docs and
add the attachment field with the correct type and optionality (matching the
code's DisputeChatMessage and ChatAttachment symbols) so the documentation
reflects the current struct shape.
In `@src/models.rs`:
- Around line 520-527: In get_all (pub async fn get_all(pool: &SqlitePool) ->
Result<Vec<AdminDispute>>), the SQL query passed to sqlx::query_as::<_,
AdminDispute> is missing a WHERE clause so the subsequent
.bind(DisputeStatus::InProgress.to_string()) is ignored; update the query string
to include "WHERE status = ?" before the "ORDER BY taken_at DESC" so the bind
filters rows to DisputeStatus::InProgress and then keep the existing .bind(...)
and .fetch_all(pool).await? handling.
---
Nitpick comments:
In `@Cargo.toml`:
- Line 32: Update the reqwest dependency spec in Cargo.toml from "0.13.1" to
"0.13.2" by changing the line defining reqwest = { version = "0.13.1",
default-features = false, features = ["native-tls", "json", "http2"] } so the
minimum required reqwest version matches the latest published 0.13.2.
In `@src/ui/helpers.rs`:
- Around line 271-316: In try_parse_attachment_message, when msg_type matches
"image_encrypted" or "file_encrypted" but subsequent required fields
(blossom_url, nonce, filename, or key decoding) are missing causing the function
to return None, add a log::warn that includes the msg_type and the original
content (or which key was missing) so partial/invalid attachment JSON is visible
in logs; place this warning immediately before returning None in those failure
paths after extracting msg_type (reference symbols:
try_parse_attachment_message, msg_type, blossom_url, nonce, filename,
decryption_key).
In `@src/ui/key_handler/mod.rs`:
- Around line 168-211: The handler sends attachments even when pubkey parsing
(PublicKey::parse for pk_str) or ECDH key derivation
(crate::util::blossom::derive_shared_key using admin_keys) fails; add explicit
logging in those failure branches so admins see why decryption_key remained
None. Specifically, inside the block handling
attachment.decryption_key.is_none() (the match on msg.sender and the nested
if-let Ok(sender_pk) / if-let Ok(shared) branches), add log::warn! messages when
PublicKey::parse(pk_str) returns Err and when derive_shared_key returns Err
(include dispute.dispute_id and msg.sender/context), then proceed to send via
tx.send((dispute_id_key, attachment)) as before.
In `@src/ui/tabs/disputes_in_progress_tab.rs`:
- Around line 461-468: The render_disputes_in_progress function currently
mutates app by clearing app.attachment_toast when the toast has expired, which
is a side effect that belongs in your main update/tick path; move the expiry
logic out of render_disputes_in_progress and into a centralized timer/tick
handler (e.g., the main event loop or a new handle_tick/update_timers function)
that runs before rendering, and have render_disputes_in_progress only read
app.attachment_toast; locate the code that checks app.attachment_toast and
ATTACHMENT_TOAST_DURATION and extract it into that tick/update function (or call
a small helper like expire_attachment_toast(&mut app)) so behavior is preserved
without mutating state during render.
In `@src/util/chat_utils.rs`:
- Around line 229-231: The public async function fetch_admin_chat_updates
currently accepts an unused parameter _admin_keys; either remove this parameter
from the function signature (and update all callers to stop passing it) or
explicitly mark it as ignored by renaming to _: &Keys to preserve the API while
signaling it's unused; update the function declaration for
fetch_admin_chat_updates and any call sites accordingly, and run a build to
ensure no remaining references or warnings.
…chat logging/toast expiry, and align docs with shared-key chat model
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
docs/ADMIN_DISPUTES.md (1)
588-588:⚠️ Potential issue | 🟡 MinorStale color reference in the seller chat flow diagram.
Line 588 reads
TUI->>Admin: Display seller response in red, but the rest of the document (lines 63, 691–692) consistently uses Magenta for seller messages. This mermaid diagram step should say "magenta" for consistency.📝 Suggested fix
- TUI->>Admin: Display seller response in red + TUI->>Admin: Display seller response in magenta🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ADMIN_DISPUTES.md` at line 588, Update the mermaid diagram step that currently reads "TUI->>Admin: Display seller response in red" to say "magenta" instead of "red" so it matches the rest of the seller-message color convention; specifically locate the diagram arrow text "TUI->>Admin: Display seller response in red" and change the color word to "magenta" to keep the document consistent with the other seller message entries.src/ui/helpers.rs (1)
625-635:⚠️ Potential issue | 🟡 MinorSeller header color is
Color::Redbut documentation specifies Magenta.Line 628 uses
Color::Redfor the Seller sender_color. The documentation atdocs/ADMIN_DISPUTES.mdline 63 explicitly states "Color-coded messages (Cyan=Admin, Green=Buyer, Magenta=Seller)", requiring Magenta instead. Update the code to match the documented color scheme.Proposed fix
- ChatSender::Seller => ("Seller", Color::Red, true), + ChatSender::Seller => ("Seller", Color::Magenta, true),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/helpers.rs` around lines 625 - 635, The Seller message color currently uses Color::Red in the sender tuple (see sender_label, sender_color, is_right_aligned) for the ChatSender::Seller branch; update that branch to use Color::Magenta instead of Color::Red so the code (sender_color for ChatSender::Seller) matches the documentation; keep the attachment-based content_color logic unchanged (it should still override with Color::Yellow when msg.attachment.is_some()).
🧹 Nitpick comments (4)
src/util/chat_utils.rs (1)
56-95: Custom wrap event bypasses the NIP-59 seal layer — document the deviation.
build_custom_wrap_eventskips the standard NIP-59 three-layer structure (rumor → seal → gift wrap) and instead directly encrypts a signedtext_notewith an ephemeral key using NIP-44. The function name makes the custom nature explicit, and the comment references the Mostro protocol chat spec, but consider adding a brief doc comment explaining why the seal layer is omitted (e.g., "Mostro protocol chat uses a simplified two-layer wrap: signed inner event encrypted with ephemeral-to-shared-key NIP-44, then wrapped as Kind::GiftWrap"). This will help future maintainers understand the intentional deviation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/util/chat_utils.rs` around lines 56 - 95, Add a short doc comment to build_custom_wrap_event explaining the intentional deviation from NIP-59: state that instead of the standard three-layer rumor→seal→gift-wrap flow the function creates a signed inner Event (via EventBuilder::text_note), encrypts it directly with an ephemeral key using nip44::encrypt, and wraps it as Kind::GiftWrap; mention why (e.g., Mostro uses a simplified two-layer approach) and reference the relevant symbols (build_custom_wrap_event, EventBuilder::text_note, nip44::encrypt, Kind::GiftWrap, SETTINGS) so maintainers know this is deliberate and not a bug.src/ui/helpers.rs (1)
264-306: Nonce field from attachment JSON is silently ignored.
try_parse_attachment_messageextractsblossom_url,filename,key, andmime_typefrom the JSON but does not extract thenoncefield. The decryption path inblossom::decrypt_blobreads the nonce from the first 12 bytes of the downloaded blob, which is correct for the stated blob layout[nonce:12][ciphertext][tag:16].However, if the JSON
nonceand the blob-embedded nonce ever diverge (e.g., a sender includes a separate nonce in JSON but does not prepend it to the blob), decryption will silently fail. Consider logging a warning if the JSON nonce is present but doesn't match the blob prefix, or at least documenting the assumption.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/helpers.rs` around lines 264 - 306, The function try_parse_attachment_message currently ignores a JSON "nonce" field which can cause silent decryption failures; update try_parse_attachment_message to parse the "nonce" field (base64-decode it, verify length == 12) and store it on the ChatAttachment (add an Option<[u8;12]> or Vec<u8> field if missing) instead of dropping it, and then ensure callers of ChatAttachment (e.g., wherever blossom::decrypt_blob is invoked) compare the JSON nonce to the blob-prefixed nonce and log a warning if both are present and differ; additionally validate/handle invalid base64 or wrong-length nonces by treating them as None and logging a debug/warn message.src/util/blossom.rs (1)
148-175: A newreqwest::Clientis created per save — consider reusing.
save_attachment_to_diskconstructs a freshreqwest::Clienton line 157 for each invocation.Client::new()allocates a connection pool and TLS state. Since saves are infrequent this is not a blocker, but if the caller already holds a client (e.g., inspawn_save_attachment), passing it in would avoid repeated setup.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/util/blossom.rs` around lines 148 - 175, The function save_attachment_to_disk creates a new reqwest::Client per call which is expensive; change its signature to accept a shared Client (e.g., client: &reqwest::Client or client: Arc<Client>) instead of constructing one inside, update call sites such as spawn_save_attachment to pass the existing client through, and remove Client::new() from save_attachment_to_disk so fetch_blob(&client, ...) uses the provided client; ensure lifetimes/ownership are adjusted (or wrap in Arc) and update imports/signatures accordingly.src/ui/key_handler/mod.rs (1)
167-233: Two shared-key derivation paths are intentionally used for different cryptographic purposes—document the distinction to prevent accidental swapping.
blossom::derive_shared_key(line 197) derives keys vianostr_sdk::secp256k1::ecdh::shared_secret_pointfor ChaCha20-Poly1305 file decryption, whilechat_utils::derive_shared_keysusesnostr_sdk::util::generate_shared_keyfor NIP-59 message wrapping. Both approaches are documented at their respective module levels (blossom.rsmodule doc andchat_utils.rsfunction doc), but there is no cross-reference explaining why both coexist. Adding a comment inkey_handler(near line 197) clarifying thatblossom::derive_shared_keyis specifically for file decryption would prevent future maintainers from accidentally substitutingchat_utils::derive_shared_keysor consolidating the two paths.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/key_handler/mod.rs` around lines 167 - 233, Add a short clarifying comment immediately above the blossom::derive_shared_key usage in key_handler::handle_key_event (where the attachment decryption_key is derived) stating that blossom::derive_shared_key derives a ChaCha20-Poly1305 file-decryption key via nostr_sdk::secp256k1::ecdh::shared_secret_point and must not be swapped with chat_utils::derive_shared_keys, which uses nostr_sdk::util::generate_shared_key for NIP-59 message wrapping; mention both functions by name and their distinct cryptographic purpose to prevent future accidental consolidation or substitution.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@docs/ADMIN_DISPUTES.md`:
- Line 588: Update the mermaid diagram step that currently reads "TUI->>Admin:
Display seller response in red" to say "magenta" instead of "red" so it matches
the rest of the seller-message color convention; specifically locate the diagram
arrow text "TUI->>Admin: Display seller response in red" and change the color
word to "magenta" to keep the document consistent with the other seller message
entries.
In `@src/ui/helpers.rs`:
- Around line 625-635: The Seller message color currently uses Color::Red in the
sender tuple (see sender_label, sender_color, is_right_aligned) for the
ChatSender::Seller branch; update that branch to use Color::Magenta instead of
Color::Red so the code (sender_color for ChatSender::Seller) matches the
documentation; keep the attachment-based content_color logic unchanged (it
should still override with Color::Yellow when msg.attachment.is_some()).
---
Nitpick comments:
In `@src/ui/helpers.rs`:
- Around line 264-306: The function try_parse_attachment_message currently
ignores a JSON "nonce" field which can cause silent decryption failures; update
try_parse_attachment_message to parse the "nonce" field (base64-decode it,
verify length == 12) and store it on the ChatAttachment (add an Option<[u8;12]>
or Vec<u8> field if missing) instead of dropping it, and then ensure callers of
ChatAttachment (e.g., wherever blossom::decrypt_blob is invoked) compare the
JSON nonce to the blob-prefixed nonce and log a warning if both are present and
differ; additionally validate/handle invalid base64 or wrong-length nonces by
treating them as None and logging a debug/warn message.
In `@src/ui/key_handler/mod.rs`:
- Around line 167-233: Add a short clarifying comment immediately above the
blossom::derive_shared_key usage in key_handler::handle_key_event (where the
attachment decryption_key is derived) stating that blossom::derive_shared_key
derives a ChaCha20-Poly1305 file-decryption key via
nostr_sdk::secp256k1::ecdh::shared_secret_point and must not be swapped with
chat_utils::derive_shared_keys, which uses nostr_sdk::util::generate_shared_key
for NIP-59 message wrapping; mention both functions by name and their distinct
cryptographic purpose to prevent future accidental consolidation or
substitution.
In `@src/util/blossom.rs`:
- Around line 148-175: The function save_attachment_to_disk creates a new
reqwest::Client per call which is expensive; change its signature to accept a
shared Client (e.g., client: &reqwest::Client or client: Arc<Client>) instead of
constructing one inside, update call sites such as spawn_save_attachment to pass
the existing client through, and remove Client::new() from
save_attachment_to_disk so fetch_blob(&client, ...) uses the provided client;
ensure lifetimes/ownership are adjusted (or wrap in Arc) and update
imports/signatures accordingly.
In `@src/util/chat_utils.rs`:
- Around line 56-95: Add a short doc comment to build_custom_wrap_event
explaining the intentional deviation from NIP-59: state that instead of the
standard three-layer rumor→seal→gift-wrap flow the function creates a signed
inner Event (via EventBuilder::text_note), encrypts it directly with an
ephemeral key using nip44::encrypt, and wraps it as Kind::GiftWrap; mention why
(e.g., Mostro uses a simplified two-layer approach) and reference the relevant
symbols (build_custom_wrap_event, EventBuilder::text_note, nip44::encrypt,
Kind::GiftWrap, SETTINGS) so maintainers know this is deliberate and not a bug.
SUMMARY
send; unwrap uses NIP-44 only to match mostro protocol chat
Ctrl+S auto-derives decryption key from dispute buyer/seller pubkey when missing
are identical (guards against bad relay data)
move test module to end of file (clippy items_after_test_module)
Summary by CodeRabbit
New Features
Enhancements
Style