Skip to content

Conversation

@erskingardner
Copy link
Member

@erskingardner erskingardner commented Aug 24, 2025

Summary by CodeRabbit

  • New Features

    • Update group data/metadata via a new action; updates are merged and propagated to relevant relays.
    • Enhanced Direct Message group handling with correct types, admins, and members.
  • Refactor

    • Streamlined group creation: admin/creator keys are supplied inside group configuration rather than separate inputs.
    • Public exports adjusted to make group information and types more directly available.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 24, 2025

Walkthrough

Reworks group admin handling by embedding admin pubkeys into NostrGroupConfigData, simplifies Whitenoise::create_group signature, adds Whitenoise::update_group_data, updates tests and event handlers, adjusts public re-exports for GroupType/GroupInformation, and aligns several nostr-related dependency references in Cargo.toml to a different remote repository.

Changes

Cohort / File(s) Summary
Dependency reference update
Cargo.toml
Repointed six nostr-related git dependencies (nostr, nostr-mls, nostr-mls-sqlite-storage, nwc, nostr-blossom, nostr-sdk) to https://github.com/erskingardner/rust-nostr at commit 03934fc8...; versions/features preserved.
Public re-exports
src/lib.rs
Added pub use whitenoise::group_information::{GroupInformation, GroupType}; and removed GroupType from the nostr_mls::prelude::group_types re-export (leaving Group and GroupState).
Groups API surface
src/whitenoise/groups.rs
Simplified create_group(...) by removing the separate admin_pubkeys parameter (admins now come from NostrGroupConfigData); added pub async fn update_group_data(...) to apply MLS metadata updates and publish evolution events. Tests expanded to cover admin handling, DM groups, member ops, updates and filtering.
Test/config builder change
src/whitenoise/mod.rs, src/whitenoise/test_utils.rs
create_nostr_group_config_data(...) signature changed to accept admins: Vec<PublicKey> and populate NostrGroupConfigData.admins. Test helpers updated accordingly.
Event handlers & tests updated
src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs, src/whitenoise/event_processor/event_handlers/handle_mls_message.rs, src/whitenoise/welcomes.rs, src/integration_tests/.../create_group.rs
All call sites/tests updated to pass admin pubkeys via the config builder (create_nostr_group_config_data(vec![...])) and to match the new create_group signature. nostr_mls.create_group usage adjusted to the new parameter order.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant WN as Whitenoise
  participant MLS as nostr_mls
  participant DB as Storage
  participant Relays as Relays

  Note over Caller,WN: Create Group (admins embedded in config)
  Caller->>WN: create_group(creator, members, config, group_type?)
  WN->>MLS: create_group(creator, members, config, group_type?)
  MLS-->>WN: Group + PendingCommit
  WN->>DB: Persist group/state
  WN->>Relays: Publish creation/evolution events
  Relays-->>WN: Ack/No-ack (best effort)
  WN-->>Caller: Group
Loading
sequenceDiagram
  autonumber
  actor Caller
  participant WN as Whitenoise
  participant MLS as nostr_mls
  participant DB as Storage
  participant Relays as Relays

  Note over Caller,WN: Update Group Data
  Caller->>WN: update_group_data(account, group_id, dataUpdate)
  WN->>MLS: update_group_data(account, group_id, dataUpdate)
  MLS-->>WN: evolution_event + PendingCommit
  WN->>DB: Merge/persist commit and metadata
  WN->>Relays: Publish evolution_event to group's relays
  WN-->>Caller: Result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • jgmontoya
  • delcin-raj

Poem

I nibble configs, snug and small,
Admins tucked in data's hall.
Groups hop out with lighter calls,
Updates echo down the rails.
A rabbit cheers—merge and all! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch updates-for-nostr-mls

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@erskingardner erskingardner force-pushed the updates-for-nostr-mls branch from a10e2f1 to 7d7ea57 Compare August 24, 2025 06:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (11)
src/lib.rs (1)

24-24: No internal references to the old MLS GroupType path found

  • Verified via rg that there are no remaining imports or fully-qualified uses of nostr_mls::prelude::group_types::GroupType anywhere in src/.
  • The crate root now publicly re-exports its own GroupType from src/lib.rs:24 (pub use whitenoise::group_information::{GroupInformation, GroupType};), replacing the MLS-provided enum with a local definition.
  • This constitutes a breaking change for downstream consumers who previously relied on the identity of nostr_mls’s GroupType. To smooth migration:
    • Call out this change explicitly under “Breaking changes” in your next release notes.
    • Optionally add a temporary alias in src/lib.rs, for example:
      #[deprecated(
          since = "X.Y.Z",
          note = "Use `whitenoise::GroupType`; this alias will be removed in the next major version"
      )]
      pub use whitenoise::group_information::GroupType as MlsGroupType;
src/whitenoise/groups.rs (9)

61-61: Typo in log message

“Succefully” → “Successfully”.

-        tracing::debug!("Succefully fetched the key packages of members");
+        tracing::debug!("Successfully fetched the key packages of members");

65-71: Admins now come from config — consider early validation for clearer errors

Passing config directly to nostr_mls.create_group is correct. For better UX, validate that:

  • config.admins is non-empty
  • All admins are among creator + member_pubkeys

Failing fast here yields clearer errors than the underlying MLS error.

-        let create_group_result = nostr_mls.create_group(
+        // Optional pre-validation for clearer errors
+        if config.admins.is_empty() {
+            return Err(WhitenoiseError::NostrMlsError(nostr_mls::Error::Group(
+                "At least one admin is required".to_owned(),
+            )));
+        }
+        let mut participants = member_pubkeys.clone();
+        participants.push(creator_account.pubkey);
+        if !config.admins.iter().all(|a| participants.contains(a)) {
+            return Err(WhitenoiseError::NostrMlsError(nostr_mls::Error::Group(
+                "Admin must be a member".to_owned(),
+            )));
+        }
+        let create_group_result = nostr_mls.create_group(
             &creator_account.pubkey,
             key_package_events.clone(),
             config
         )?;

106-111: Prefer Duration addition for readability and consistency

Elsewhere you use Duration math. Keep it consistent.

-        use std::ops::Add;
-        let one_month_future = Timestamp::now().add(30 * 24 * 60 * 60);
+        let one_month_future = Timestamp::now() + Duration::from_secs(30 * 24 * 60 * 60);

123-137: Empty group_relays leads to no subscriptions; add fallback like add_members_to_group

If config.relays is empty, setup_group_messages_subscriptions_with_signer receives an empty relay list and won’t subscribe. Mirror the fallback you implemented in add_members_to_group by using the account’s NIP-65 relays.

-        let mut relays = HashSet::new();
-        for relay_url in group_relays {
-            let db_relay = self.find_or_create_relay_by_url(&relay_url).await?;
-            relays.insert(db_relay);
-        }
-
-        self.nostr
-            .setup_group_messages_subscriptions_with_signer(
-                creator_account.pubkey,
-                &relays.into_iter().collect::<Vec<_>>(),
-                &group_ids,
-                keys,
-            )
-            .await
-            .map_err(WhitenoiseError::from)?;
+        let relays_vec = if group_relays.is_empty() {
+            // Fallback to account relays
+            let fallback = account.nip65_relays(self).await?;
+            if fallback.is_empty() {
+                return Err(WhitenoiseError::Other(anyhow::anyhow!(
+                    "No relays available for subscribing to group messages"
+                )));
+            }
+            fallback
+        } else {
+            let mut relays = HashSet::new();
+            for relay_url in group_relays {
+                let db_relay = self.find_or_create_relay_by_url(&relay_url).await?;
+                relays.insert(db_relay);
+            }
+            relays.into_iter().collect::<Vec<_>>()
+        };
+
+        self.nostr
+            .setup_group_messages_subscriptions_with_signer(
+                creator_account.pubkey,
+                &relays_vec,
+                &group_ids,
+                keys,
+            )
+            .await
+            .map_err(WhitenoiseError::from)?;

311-324: TODO note: consider implementing the same fallback here

You’ve already done the fallback logic in add_members_to_group above (when group_relays is empty). It would be consistent to adopt that here for welcome fan-out when a contact has no inbox relays configured.


365-391: New: update_group_data — good, but mirror relay fallback and DRY publishing

Function works, but:

  • If group_relays is empty, publishing will silently do nothing. Use the same fallback to account relays as in add_members_to_group.
  • The “resolve relays + publish evolution event” pattern is duplicated across add/remove/update. Consider extracting to a helper to reduce drift.
-        let group_relays = nostr_mls.get_relays(group_id)?;
-        let evolution_event = update_result.evolution_event;
-        let mut relays = HashSet::new();
-        for relay_url in group_relays {
-            let db_relay = self.find_or_create_relay_by_url(&relay_url).await?;
-            relays.insert(db_relay);
-        }
-        self.nostr
-            .publish_event_to(evolution_event, &relays.into_iter().collect::<Vec<_>>())
-            .await?;
+        let group_relays = nostr_mls.get_relays(group_id)?;
+        let evolution_event = update_result.evolution_event;
+
+        let relays_vec = if group_relays.is_empty() {
+            let fallback = account.nip65_relays(self).await?;
+            if fallback.is_empty() {
+                return Err(WhitenoiseError::Other(anyhow::anyhow!(
+                    "No relays available for publishing evolution event"
+                )));
+            }
+            fallback
+        } else {
+            let mut relays = HashSet::new();
+            for relay_url in group_relays {
+                let db_relay = self.find_or_create_relay_by_url(&relay_url).await?;
+                relays.insert(db_relay);
+            }
+            relays.into_iter().collect::<Vec<_>>()
+        };
+
+        self.nostr.publish_event_to(evolution_event, &relays_vec).await?;

Optionally, extract the “collect group relays or fallback to account relays” into a helper to use here and in add/remove.


470-526: Tests: use of clones and comments

  • config.clone() is unnecessary where config isn’t reused; remove the clone to keep tests tidy.
  • The comment “Verify group state and type… valid state” could become an assertion if GroupState is re-exported. If not, leaving it as-is is fine.
-            .create_group(
-                creator_account,
-                member_pubkeys.clone(),
-                config.clone(),
-                None,
-            )
+            .create_group(creator_account, member_pubkeys.clone(), config, None)

657-705: End-to-end membership tests look solid; minor flakiness risk due to relay timing

Given real relay use in CI, these can be timing-sensitive. If flakiness appears, consider:

  • Using wait/retry helpers instead of fixed sleeps
  • Mocking NostrManager for unit tests and leaving real relays to integration tests

If you want, I can draft a small test helper that polls for key package availability with a timeout to replace fixed sleeps.


708-751: update_group_data test covers name/description/image fields well

Solid assertions after update. You might also add a negative-path test (e.g., non-admin attempting update) if MLS layer returns a distinct error for permissions.

src/whitenoise/welcomes.rs (1)

175-177: Small cleanup: avoid unnecessary clone when building config

create_nostr_group_config_data takes ownership of the Vec<PublicKey>, so you can pass admin_pubkeys directly without cloning.

-        let admin_pubkeys = vec![creator_account.pubkey, member_pubkeys[0]];
-        let config = create_nostr_group_config_data(admin_pubkeys.clone());
+        let admin_pubkeys = vec![creator_account.pubkey, member_pubkeys[0]];
+        let config = create_nostr_group_config_data(admin_pubkeys);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 14fe58c and 7d7ea57.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml (1 hunks)
  • src/lib.rs (2 hunks)
  • src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (1 hunks)
  • src/whitenoise/event_processor/event_handlers/handle_mls_message.rs (2 hunks)
  • src/whitenoise/groups.rs (10 hunks)
  • src/whitenoise/mod.rs (1 hunks)
  • src/whitenoise/welcomes.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-17T19:35:53.520Z
Learnt from: jmcorgan
PR: parres-hq/whitenoise#318
File: src/whitenoise/accounts/welcomes.rs:53-53
Timestamp: 2025-08-17T19:35:53.520Z
Learning: In the Whitenoise codebase, Account::create_nostr_mls should have its errors mapped to WhitenoiseError::NostrMlsError instead of using anyhow::anyhow!() wrapping, as this preserves type safety and specific error information.

Applied to files:

  • src/whitenoise/event_processor/event_handlers/handle_mls_message.rs
📚 Learning: 2025-08-17T19:34:30.333Z
Learnt from: jmcorgan
PR: parres-hq/whitenoise#318
File: src/whitenoise/accounts/messages.rs:193-195
Timestamp: 2025-08-17T19:34:30.333Z
Learning: In the whitenoise codebase, `Account::create_nostr_mls()` returns `Result<NostrMls<NostrMlsSqliteStorage>, AccountError>`, and `WhitenoiseError` has an `Account(#[from] AccountError)` variant that automatically converts. When handling MLS creation errors, use the `?` operator to let `AccountError` convert naturally to `WhitenoiseError::Account` rather than manually mapping to `WhitenoiseError::Configuration`.

Applied to files:

  • src/whitenoise/event_processor/event_handlers/handle_mls_message.rs
🧬 Code graph analysis (5)
src/lib.rs (1)
src/whitenoise/groups.rs (1)
  • nostr_mls (72-76)
src/whitenoise/event_processor/event_handlers/handle_mls_message.rs (1)
src/whitenoise/mod.rs (1)
  • create_nostr_group_config_data (521-530)
src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (1)
src/whitenoise/mod.rs (1)
  • create_nostr_group_config_data (521-530)
src/whitenoise/welcomes.rs (1)
src/whitenoise/mod.rs (1)
  • create_nostr_group_config_data (521-530)
src/whitenoise/groups.rs (3)
src/whitenoise/accounts.rs (4)
  • create_nostr_mls (250-257)
  • create_nostr_mls (891-893)
  • relays (85-93)
  • new (51-70)
src/whitenoise/database/users.rs (1)
  • relays (157-189)
src/whitenoise/mod.rs (2)
  • new (52-66)
  • create_nostr_group_config_data (521-530)
🪛 GitHub Actions: CI
Cargo.toml

[error] 1-1: Command 'cargo check --all-targets --all-features' failed. Could not load dependency 'nostr' for package 'whitenoise v0.1.0'. Missing file: /Users/runner/work/whitenoise/rust-nostr/crates/nostr/Cargo.toml (No such file or directory).

🔇 Additional comments (5)
src/lib.rs (1)

65-65: Re-export now excludes GroupType — consistent with the new API surface

Re-exporting only Group and GroupState from nostr_mls keeps the crate-level API focused and avoids the previous type collision with your own GroupType. LGTM.

src/whitenoise/mod.rs (1)

521-529: Admins now embedded in NostrGroupConfigData (test util) — good alignment with create_group API

The helper signature change keeps tests aligned with the new contract. One small tweak: accept an Into<Vec<PublicKey>> to allow future call sites to pass slices (or other collections) without extra allocations. I ran the grep check and confirmed that today all tests pass a Vec<PublicKey>, so this change is fully backward-compatible.

-pub(crate) fn create_nostr_group_config_data(admins: Vec<PublicKey>) -> NostrGroupConfigData {
+pub(crate) fn create_nostr_group_config_data(admins: impl Into<Vec<PublicKey>>) -> NostrGroupConfigData {
     NostrGroupConfigData {
         name: "Test group".to_owned(),
         description: "test description".to_owned(),
         image_url: Some("http://test_blossom:53232/fake_img.png".to_owned()),
         image_key: Some(b"fake key to encrypt image".to_vec()),
         relays: vec![RelayUrl::parse("ws://localhost:8080/").unwrap()],
-        admins,
+        admins: admins.into(),
     }
 }

• All existing call sites invoke with a Vec<PublicKey> (admin_pubkeys.clone(), vec![…], etc.), so no test breaks will occur.
• Future calls can now also pass slices (&[PublicKey]) by adding a trivial From impl or using to_vec() implicitly via Into.

src/whitenoise/event_processor/event_handlers/handle_mls_message.rs (1)

63-66: Tests updated to embed admins in config — matches the new create_group signature

Both tests correctly build config via create_nostr_group_config_data(vec![creator_account.pubkey]) and drop the separate creators/admin parameter. The error-path assertion expecting WhitenoiseError::NostrMlsError remains valid with the current mapping.

Also applies to: 112-115

src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (1)

113-118: Admins moved into config: test update aligns with API change

Passing the creator’s pubkey into create_nostr_group_config_data(vec![creator_account.pubkey]) correctly reflects the removal of the separate admins parameter and ensures the creator is part of the admin set at group creation time. Looks good.

src/whitenoise/welcomes.rs (1)

175-183: ✅ Signature update sanity check passed

All create_nostr_group_config_data and .create_group call-sites have been verified against the updated signatures:

  • No lingering no-argument calls to create_nostr_group_config_data() were found.
  • The create_group definition at src/whitenoise/groups.rs reflects the new signature.
  • Every invocation of .create_group(...) across the codebase and tests supplies the required parameters, with no deprecated usages detected.

No further changes are needed here.

@erskingardner
Copy link
Member Author

@coderabbitai, please do a full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 24, 2025

@erskingardner I'll conduct a comprehensive review of all the changes in this PR, including the API refactoring, dependency updates, and test modifications.

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/whitenoise/groups.rs (2)

103-106: Bug: invalid Timestamp addition (likely compile-time error).

Timestamp::now().add(…) with a bare integer is not the common API and differs from the rest of the file where Duration is used. This should use + Duration::from_secs(...). Also drop the function-scoped use std::ops::Add;.

Apply:

-            // Create a timestamp 1 month in the future
-            use std::ops::Add;
-            let one_month_future = Timestamp::now().add(30 * 24 * 60 * 60);
+            // Create a timestamp ~1 month in the future
+            let one_month_future = Timestamp::now() + Duration::from_secs(30 * 24 * 60 * 60);

350-359: Missing relay fallback when publishing evolution event after removals.

Unlike add_members_to_group, this path doesn’t handle the “no group relays configured” case. Use account relays as fallback, or return a clear error. Otherwise, updates can be dropped.

Apply:

-        self.nostr
-            .publish_event_to(evolution_event, &relays.into_iter().collect::<Vec<_>>())
-            .await?;
+        if relays.is_empty() {
+            tracing::warn!(
+                target: "whitenoise::remove_members_from_group",
+                "Group has no relays configured, using account's default relays"
+            );
+            let fallback_relays = account.nip65_relays(self).await?;
+            if fallback_relays.is_empty() {
+                return Err(WhitenoiseError::Other(anyhow::anyhow!(
+                    "No relays available for publishing evolution event - both group relays and account relays are empty"
+                )));
+            }
+            self.nostr.publish_event_to(evolution_event, &fallback_relays).await?;
+        } else {
+            self.nostr
+                .publish_event_to(evolution_event, &relays.into_iter().collect::<Vec<_>>())
+                .await?;
+        }
🧹 Nitpick comments (11)
src/whitenoise/mod.rs (1)

521-529: Move-admins-to-config is correct; tighten helper API to avoid unnecessary clones

The new helper correctly embeds admins into NostrGroupConfigData. To reduce call-site friction and avoid extra Vec clones in tests, accept any Into<Vec> (or even impl IntoIterator<Item=PublicKey>) and convert internally. Also add a brief doc comment to signal that admins must be non-empty.

Apply:

-    pub(crate) fn create_nostr_group_config_data(admins: Vec<PublicKey>) -> NostrGroupConfigData {
+    /// Build a minimal NostrGroupConfigData for tests.
+    /// Note: `admins` must be non-empty.
+    pub(crate) fn create_nostr_group_config_data(admins: impl Into<Vec<PublicKey>>) -> NostrGroupConfigData {
-        NostrGroupConfigData {
+        NostrGroupConfigData {
             name: "Test group".to_owned(),
             description: "test description".to_owned(),
             image_url: Some("http://test_blossom:53232/fake_img.png".to_owned()),
             image_key: Some(b"fake key to encrypt image".to_vec()),
             relays: vec![RelayUrl::parse("ws://localhost:8080/").unwrap()],
-            admins,
+            admins: admins.into(),
         }
     }

If you adopt this, update the two test call sites to pass the vector directly without cloning (see comments in welcomes.rs and handle_giftwrap.rs).

src/lib.rs (1)

24-24: Public API shift: GroupType re-export moved; add a compatibility alias

Re-exporting GroupType from whitenoise::group_information is fine, but it’s a breaking change for users who previously imported it from nostr_mls::prelude::group_types. Consider a deprecated alias to ease migration.

 pub use whitenoise::group_information::{GroupInformation, GroupType};
+#[deprecated(note = "Use whitenoise::group_information::GroupType instead")]
+pub type MlsGroupType = GroupType;
Cargo.toml (1)

31-36: Use the official rust-nostr repo for production deps; reserve your fork for local patches only

Pinning by commit guarantees reproducibility, but Cargo.toml currently points core Nostr crates at your personal fork (github.com/erskingardner/rust-nostr), which raises supply-chain and CI stability risks if that repo ever becomes unavailable. Instead:

• Restore the upstream remotes under version 0.43.0 (github.com/rust-nostr/nostr), pinned to the same commit
• Move your fork overrides into a VCS-ignored [patch] section in .cargo/config.toml, so CI and other developers stay on the official source

Impacted locations:

  • Cargo.toml – lines 31–36 (nostr, nostr-mls, nostr-mls-sqlite-storage, nwc, nostr-blossom, nostr-sdk)
  • Cargo.toml – lines 48–49 (additional nostr-sdk features)

Proposed Cargo.toml diff:

-nostr                  = { version = "0.43.0", git = "https://github.com/erskingardner/rust-nostr", rev = "03934fc8e1e1e6c831f282cde9e13589be15c21e", features = ["std"] }
-nostr-mls              = { version = "0.43.0", git = "https://github.com/erskingardner/rust-nostr", rev = "03934fc8e1e1e6c831f282cde9e13589be15c21e" }
-nostr-mls-sqlite-storage = { version = "0.43.0", git = "https://github.com/erskingardner/rust-nostr", rev = "03934fc8e1e1e6c831f282cde9e13589be15c21e" }
-nwc                    = { version = "0.43.0", git = "https://github.com/erskingardner/rust-nostr", rev = "03934fc8e1e1e6c831f282cde9e13589be15c21e" }
-nostr-blossom          = { version = "0.43.0", git = "https://github.com/erskingardner/rust-nostr", rev = "03934fc8e1e1e6c831f282cde9e13589be15c21e" }
-nostr-sdk              = { version = "0.43.0", git = "https://github.com/erskingardner/rust-nostr", rev = "03934fc8e1e1e6c831f282cde9e13589be15c21e", features = [
+nostr                  = { version = "0.43.0", git = "https://github.com/rust-nostr/nostr", rev = "d70bed548d2e1399b758ad8bb7da9f058a716064", features = ["std"] }
+nostr-mls              = { version = "0.43.0", git = "https://github.com/rust-nostr/nostr", rev = "d70bed548d2e1399b758ad8bb7da9f058a716064" }
+nostr-mls-sqlite-storage = { version = "0.43.0", git = "https://github.com/rust-nostr/nostr", rev = "d70bed548d2e1399b758ad8bb7da9f058a716064" }
+nwc                    = { version = "0.43.0", git = "https://github.com/rust-nostr/nostr", rev = "d70bed548d2e1399b758ad8bb7da9f058a716064" }
+nostr-blossom          = { version = "0.43.0", git = "https://github.com/rust-nostr/nostr", rev = "d70bed548d2e1399b758ad8bb7da9f058a716064" }
+nostr-sdk              = { version = "0.43.0", git = "https://github.com/rust-nostr/nostr", rev = "d70bed548d2e1399b758ad8bb7da9f058a716064", features = [
     "lmdb",
     "nip04",
     "nip44",
     "nip47",
     "nip59",
 ] }

Example local override (.cargo/config.toml, uncommitted):

[patch."https://github.com/rust-nostr/nostr"]
nostr                       = { path = "../rust-nostr/crates/nostr" }
nostr-mls                   = { path = "../rust-nostr/mls/nostr-mls" }
nostr-mls-sqlite-storage    = { path = "../rust-nostr/mls/nostr-mls-sqlite-storage" }
nwc                         = { path = "../rust-nostr/crates/nwc" }
nostr-blossom               = { path = "../rust-nostr/rfs/nostr-blossom" }
nostr-sdk                   = { path = "../rust-nostr/crates/nostr-sdk" }
src/whitenoise/welcomes.rs (1)

176-180: Tests: avoid cloning Vecs; pass admins/members by value

Now that admins live in the config and create_group no longer needs a separate admins param, you can drop the clones here to keep tests lean.

-        let config = create_nostr_group_config_data(admin_pubkeys.clone());
+        let config = create_nostr_group_config_data(admin_pubkeys);

-        let group = whitenoise
-            .create_group(&creator_account, member_pubkeys.clone(), config, None)
+        let group = whitenoise
+            .create_group(&creator_account, member_pubkeys, config, None)
             .await;

If you adopt the Into<Vec> helper signature suggested in mod.rs, this call site stays the same.

src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (1)

117-118: Good: config now carries admins for MLS group creation

Switching the nostr_mls::create_group call to pass NostrGroupConfigData with admins embedded matches the refactor’s goal. Consider using the generalized helper signature (impl Into<Vec>) to stay flexible in tests, but the current usage is fine.

If you adopt the helper refactor:

-                create_nostr_group_config_data(vec![creator_account.pubkey]),
+                create_nostr_group_config_data([creator_account.pubkey]),
src/integration_tests/test_cases/shared/create_group.rs (2)

53-60: Prefer centralizing test relay URL and consider a helper for config construction.

  • The hard-coded relay URL (ws://localhost:8080) appears in multiple tests; factor it into a test constant or helper to avoid drift.
  • Since all options are passed through NostrGroupConfigData::new(...), consider a small test helper that fills image fields with None and sets the default relays/admins to reduce repetition at call sites.

Apply this small refactor within your test utils:

+pub fn default_test_relays() -> Vec<RelayUrl> {
+    vec![RelayUrl::parse("ws://localhost:8080").unwrap()]
+}

Then in this test:

-                NostrGroupConfigData::new(
+                NostrGroupConfigData::new(
                     self.group_name.clone(),
                     self.group_description.clone(),
-                    None, // image_url
-                    None, // image_key
-                    vec![RelayUrl::parse("ws://localhost:8080").unwrap()],
+                    None, // image_url
+                    None, // image_key
+                    default_test_relays(),
                     admin_pubkeys,
                 ),

65-66: Reduce flakiness: poll for condition instead of fixed sleep.

Replace the fixed 100ms sleep with a short wait-until helper (with timeout) that checks for the group’s presence or expected membership count.

Here’s a sketch you can add to test utils, then use here:

pub async fn wait_until<F, Fut>(timeout_ms: u64, mut cond: F) -> bool
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = bool>,
{
    let start = std::time::Instant::now();
    let step = std::time::Duration::from_millis(25);
    while start.elapsed().as_millis() < timeout_ms as u128 {
        if cond().await {
            return true;
        }
        tokio::time::sleep(step).await;
    }
    false
}

Usage here:

let ok = wait_until(500, || async {
    // e.g., try fetching groups and see if the new one appears
    !context.whitenoise.groups(creator, true).await.unwrap().is_empty()
}).await;
assert!(ok, "Group did not appear within timeout");
src/whitenoise/groups.rs (1)

61-61: Typo in log message.

“Succefully” → “Successfully”.

-        tracing::debug!("Succefully fetched the key packages of members");
+        tracing::debug!("Successfully fetched the key packages of members");
src/whitenoise/event_processor/event_handlers/handle_mls_message.rs (3)

56-56: Mitigate timing flakiness in tests.

Replace fixed sleeps with a short “wait-until” helper (polling for key package availability or group presence) to reduce intermittent failures on slow CI.


107-115: Same here: avoid fixed sleep where possible.

Use the same helper proposed above to wait for relay propagation.


137-140: Intentional corruption is fine; consider adding a brief comment.

Changing the outer event kind to TextNote ensures MLS processing rejects it. Add a clarifying comment so future readers don’t confuse inner vs. outer event kinds.

-        // Corrupt it by changing the kind so MLS processing fails
+        // Corrupt the OUTER event kind so MLS processing rejects it (inner content remains a TextNote)
         let mut bad_event = valid_event.clone();
         bad_event.kind = Kind::TextNote;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 14fe58c and d4c298d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • Cargo.toml (2 hunks)
  • src/integration_tests/test_cases/shared/create_group.rs (1 hunks)
  • src/lib.rs (2 hunks)
  • src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (1 hunks)
  • src/whitenoise/event_processor/event_handlers/handle_mls_message.rs (2 hunks)
  • src/whitenoise/groups.rs (10 hunks)
  • src/whitenoise/mod.rs (1 hunks)
  • src/whitenoise/welcomes.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-17T19:35:53.520Z
Learnt from: jmcorgan
PR: parres-hq/whitenoise#318
File: src/whitenoise/accounts/welcomes.rs:53-53
Timestamp: 2025-08-17T19:35:53.520Z
Learning: In the Whitenoise codebase, Account::create_nostr_mls should have its errors mapped to WhitenoiseError::NostrMlsError instead of using anyhow::anyhow!() wrapping, as this preserves type safety and specific error information.

Applied to files:

  • src/whitenoise/event_processor/event_handlers/handle_mls_message.rs
📚 Learning: 2025-08-17T19:34:30.333Z
Learnt from: jmcorgan
PR: parres-hq/whitenoise#318
File: src/whitenoise/accounts/messages.rs:193-195
Timestamp: 2025-08-17T19:34:30.333Z
Learning: In the whitenoise codebase, `Account::create_nostr_mls()` returns `Result<NostrMls<NostrMlsSqliteStorage>, AccountError>`, and `WhitenoiseError` has an `Account(#[from] AccountError)` variant that automatically converts. When handling MLS creation errors, use the `?` operator to let `AccountError` convert naturally to `WhitenoiseError::Account` rather than manually mapping to `WhitenoiseError::Configuration`.

Applied to files:

  • src/whitenoise/event_processor/event_handlers/handle_mls_message.rs
🔇 Additional comments (7)
src/lib.rs (1)

65-65: No stale GroupType imports detected
I ran a repo-wide search for any remaining imports of nostr_mls::prelude::group_types::GroupType and found none. All GroupType usages in whitenoise/groups.rs correctly reference crate::whitenoise::group_information::GroupType, so nothing is broken by dropping the re-export. No further changes are needed.

src/integration_tests/test_cases/shared/create_group.rs (1)

46-46: Good: admins now flow via config (no separate arg).

Setting admin_pubkeys to include the creator aligns with the API change and maintains a valid admin set.

src/whitenoise/groups.rs (3)

66-68: API change looks correct: config now carries admins.

Passing config (with embedded admins) to nostr_mls.create_group matches the refactor and keeps the surface smaller. Good change.


24-30: No stale create_group call sites detected

All instances of create_group now consistently pass admin_pubkeys via NostrGroupConfigData and the group_type parameter. No lingering calls using the old signature remain.


308-318: I’ll wait for the extracted signatures and context to draft the precise fallback implementation.

src/whitenoise/event_processor/event_handlers/handle_mls_message.rs (2)

17-37: Error mapping is correct and consistent with project guidance.

Account::create_nostr_mls(...)? leverages From<AccountError> for WhitenoiseError, and MLS processing errors are mapped to WhitenoiseError::NostrMlsError(e). Good separation of concerns and logs.


59-65: Tests correctly updated to pass admins via config.

The updated call to create_group reflects the new API and keeps test intent unchanged.

Comment on lines +370 to +393
pub async fn update_group_data(
&self,
account: &Account,
group_id: &GroupId,
group_data: NostrGroupDataUpdate,
) -> Result<()> {
let nostr_mls = Account::create_nostr_mls(account.pubkey, &self.config.data_dir)?;
let update_result = nostr_mls.update_group_data(group_id, group_data)?;
nostr_mls.merge_pending_commit(group_id)?;
let group_relays = nostr_mls.get_relays(group_id)?;

let evolution_event = update_result.evolution_event;

let mut relays = HashSet::new();
for relay_url in group_relays {
let db_relay = self.find_or_create_relay_by_url(&relay_url).await?;
relays.insert(db_relay);
}

self.nostr
.publish_event_to(evolution_event, &relays.into_iter().collect::<Vec<_>>())
.await?;
Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add relay fallback to update_group_data for consistency and robustness.

Mirror the fallback behavior used in add_members_to_group so metadata updates don’t get lost when group relays are empty.

-        self.nostr
-            .publish_event_to(evolution_event, &relays.into_iter().collect::<Vec<_>>())
-            .await?;
+        if relays.is_empty() {
+            tracing::warn!(
+                target: "whitenoise::update_group_data",
+                "Group has no relays configured, using account's default relays"
+            );
+            let fallback_relays = account.nip65_relays(self).await?;
+            if fallback_relays.is_empty() {
+                return Err(WhitenoiseError::Other(anyhow::anyhow!(
+                    "No relays available for publishing evolution event - both group relays and account relays are empty"
+                )));
+            }
+            self.nostr.publish_event_to(evolution_event, &fallback_relays).await?;
+        } else {
+            self.nostr
+                .publish_event_to(evolution_event, &relays.into_iter().collect::<Vec<_>>())
+                .await?;
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub async fn update_group_data(
&self,
account: &Account,
group_id: &GroupId,
group_data: NostrGroupDataUpdate,
) -> Result<()> {
let nostr_mls = Account::create_nostr_mls(account.pubkey, &self.config.data_dir)?;
let update_result = nostr_mls.update_group_data(group_id, group_data)?;
nostr_mls.merge_pending_commit(group_id)?;
let group_relays = nostr_mls.get_relays(group_id)?;
let evolution_event = update_result.evolution_event;
let mut relays = HashSet::new();
for relay_url in group_relays {
let db_relay = self.find_or_create_relay_by_url(&relay_url).await?;
relays.insert(db_relay);
}
self.nostr
.publish_event_to(evolution_event, &relays.into_iter().collect::<Vec<_>>())
.await?;
Ok(())
}
pub async fn update_group_data(
&self,
account: &Account,
group_id: &GroupId,
group_data: NostrGroupDataUpdate,
) -> Result<()> {
let nostr_mls = Account::create_nostr_mls(account.pubkey, &self.config.data_dir)?;
let update_result = nostr_mls.update_group_data(group_id, group_data)?;
nostr_mls.merge_pending_commit(group_id)?;
let group_relays = nostr_mls.get_relays(group_id)?;
let evolution_event = update_result.evolution_event;
let mut relays = HashSet::new();
for relay_url in group_relays {
let db_relay = self.find_or_create_relay_by_url(&relay_url).await?;
relays.insert(db_relay);
}
if relays.is_empty() {
tracing::warn!(
target: "whitenoise::update_group_data",
"Group has no relays configured, using account's default relays"
);
let fallback_relays = account.nip65_relays(self).await?;
if fallback_relays.is_empty() {
return Err(WhitenoiseError::Other(anyhow::anyhow!(
"No relays available for publishing evolution event - both group relays and account relays are empty"
)));
}
self.nostr.publish_event_to(evolution_event, &fallback_relays).await?;
} else {
self.nostr
.publish_event_to(evolution_event, &relays.into_iter().collect::<Vec<_>>())
.await?;
}
Ok(())
}
🤖 Prompt for AI Agents
In src/whitenoise/groups.rs around lines 370 to 393, update update_group_data to
mirror the relay fallback used in add_members_to_group: after retrieving
group_relays, if that list is empty replace it with the same fallback relay list
used in add_members_to_group (e.g., the account's relays or
self.config.default_relays), then continue creating/finding DB relay entries
from that final list and publish to those relays; implement the empty-check and
assignment before the loop so metadata updates are sent when no group relays
exist.

Copy link
Contributor

@jgmontoya jgmontoya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a minor thing

Comment on lines +383 to +387
let mut relays = HashSet::new();
for relay_url in group_relays {
let db_relay = self.find_or_create_relay_by_url(&relay_url).await?;
relays.insert(db_relay);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to deduplicate here because nostr_mls.get_relays already returns a set (BTreeSet<RelayUrl>)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more about making sure the relays are actually created in our db rather than deduplication.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        let mut relays = Vec::new();
        for relay_url in group_relays {
            let db_relay = self.find_or_create_relay_by_url(&relay_url).await?;
            relays.push(db_relay);
        }

        self.nostr
            .publish_event_to(evolution_event, &relays)
            .await?;

This version is cleaner.

let one_month_future = Timestamp::now() + Duration::from_secs(30 * 24 * 60 * 60);

// Use fallback relays if contact has no inbox relays configured
// TODO: Maybe need to use fallback relays if contact has no inbox relays configured
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see that when a new user is created, their relays are not inserted by default.
Reference fn find_or_create_by_pubkey

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - actually - we need to be really careful about this don't we. When we're doing lots of different operations we first need to get user's and also make sure we have their relays etc. I think this deserves another issue that we tackle separately to ensure that we're checking every location where we find or create a user to ensure that we're also trying to fetch their relays and metadata.

) -> Result<()> {
let nostr_mls = Account::create_nostr_mls(account.pubkey, &self.config.data_dir)?;
let update_result = nostr_mls.update_group_data(group_id, group_data)?;
nostr_mls.merge_pending_commit(group_id)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done in nostr-mls?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, needs to happen here. The client should be in control of when to call it because they have to be the ones to publish the actual event to relays

Copy link
Contributor

@delcin-raj delcin-raj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments and questions.
The tests look comprehensive!

@erskingardner erskingardner merged commit ec615d7 into master Aug 24, 2025
4 checks passed
@erskingardner erskingardner deleted the updates-for-nostr-mls branch August 24, 2025 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants