Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/bin/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ async fn main() -> Result<(), WhitenoiseError> {
group_description,
None,
None,
vec![RelayUrl::parse("ws://localhost:8080/").unwrap()],
vec![RelayUrl::parse("ws://localhost:8080").unwrap()],
),
)
.await?;
Expand Down Expand Up @@ -377,6 +377,9 @@ async fn main() -> Result<(), WhitenoiseError> {
account4.pubkey.to_hex()
);

// Wait for the account to fully initialize and publish its events
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;

// Get initial group member count
let initial_members = whitenoise
.fetch_group_members(&account1, &test_group.mls_group_id)
Expand All @@ -391,10 +394,14 @@ async fn main() -> Result<(), WhitenoiseError> {

// Add account4 as a new member to the test group (account1 is admin)
let new_members = vec![account4.pubkey];
tracing::info!("Adding account4 as contact to account1...");
whitenoise
.add_contact(&account1, account4.pubkey)
.await
.unwrap();
tracing::info!("✓ Contact added successfully");

tracing::info!("Adding account4 as member to group...");
whitenoise
.add_members_to_group(&account1, &test_group.mls_group_id, new_members)
.await?;
Expand Down
21 changes: 6 additions & 15 deletions src/nostr_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,6 @@ impl NostrManager {
Ok(())
}

pub(crate) async fn connect_to_relay(&self, relay: RelayUrl) -> Result<()> {
let newly_added = self.client.add_relay(relay).await?;
if newly_added {
tokio::spawn({
let client = self.client.clone();
async move {
client.connect().await;
}
});
}
Ok(())
}

pub(crate) fn default_timeout() -> Duration {
Duration::from_secs(5)
}
Expand Down Expand Up @@ -219,8 +206,8 @@ impl NostrManager {

/// Publishes a Nostr event (which is already signed) to the specified relays.
///
/// This method allows publishing an event to a list of relay URLs. It uses the client's
/// built-in relay handling to send the event to the specified relays.
/// This method allows publishing an event to a list of relay URLs. It ensures that the client
/// is connected to all specified relays before attempting to publish the event.
///
/// # Arguments
///
Expand All @@ -235,6 +222,10 @@ impl NostrManager {
event: Event,
relays: &BTreeSet<RelayUrl>,
) -> Result<Output<EventId>> {
// Ensure we're connected to all target relays before publishing
self.ensure_relays_connected(relays.iter().cloned().collect())
.await?;

Ok(self.client.send_event_to(relays, &event).await?)
}

Expand Down
12 changes: 6 additions & 6 deletions src/nostr_manager/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,19 @@ impl NostrManager {
relay_type: RelayType,
nip65_relays: DashSet<RelayUrl>,
) -> Result<DashSet<RelayUrl>> {
let filter = Filter::new().author(pubkey).kind(relay_type.into());
let filter = Filter::new()
.author(pubkey)
.kind(relay_type.into())
.limit(1);
let relay_events = self
.client
.fetch_events_from(nip65_relays, filter.clone(), self.timeout)
.await?;
tracing::debug!("Fetched relay events {:?}", relay_events);

match relay_events
.into_iter()
.max_by_key(|event| event.created_at)
{
match relay_events.first() {
None => Ok(DashSet::new()),
Some(event) => Ok(Self::relay_urls_from_event(event)),
Some(event) => Ok(Self::relay_urls_from_event(event.clone())),
}
}

Expand Down
14 changes: 2 additions & 12 deletions src/whitenoise/accounts/contacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,14 +528,9 @@ mod tests {
#[tokio::test]
async fn test_add_contact_logic() {
let (whitenoise, _data_temp, _logs_temp) = create_mock_whitenoise().await;
let (account, keys) = create_test_account();
let account = whitenoise.create_identity().await.unwrap();
let contact_pubkey = create_test_keys().public_key();

// Store account keys
whitenoise.secrets_store.store_private_key(&keys).unwrap();
let log_account = whitenoise.login(keys.secret_key().to_secret_hex()).await;
assert!(log_account.is_ok());

// Test the logic of adding a contact (without actual network calls)
// Load current contact list (will be empty in test environment)
let current_contacts = whitenoise.fetch_contacts(&account.pubkey).await.unwrap();
Expand Down Expand Up @@ -641,12 +636,7 @@ mod tests {
#[tokio::test]
async fn test_contact_validation_logic() {
let (whitenoise, _data_temp, _logs_temp) = create_mock_whitenoise().await;
let (account, keys) = create_test_account();

// Store account keys
whitenoise.secrets_store.store_private_key(&keys).unwrap();
let log_account = whitenoise.login(keys.secret_key().to_secret_hex()).await;
assert!(log_account.is_ok());
let account = whitenoise.create_identity().await.unwrap();

let contact_pubkey = create_test_keys().public_key();

Expand Down
143 changes: 88 additions & 55 deletions src/whitenoise/accounts/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::whitenoise::accounts::Account;
use crate::whitenoise::error::{Result, WhitenoiseError};
use crate::whitenoise::Whitenoise;
use nostr_mls::prelude::*;
use std::time::Duration;

impl Whitenoise {
/// Creates a new MLS group with the specified members and settings
Expand Down Expand Up @@ -254,8 +255,13 @@ impl Whitenoise {
// Fetch key packages for all members
for pk in members.iter() {
let contact = self.load_contact(pk).await?;
let relays_to_use = if contact.key_package_relays.is_empty() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice catch!

Account::default_relays()
} else {
contact.key_package_relays.clone()
};
let some_event = self
.fetch_key_package_event_from(contact.key_package_relays.clone(), *pk)
.fetch_key_package_event_from(relays_to_use, *pk)
.await?;
let event = some_event.ok_or(WhitenoiseError::NostrMlsError(
nostr_mls::Error::KeyPackage("Does not exist".to_owned()),
Expand Down Expand Up @@ -300,52 +306,73 @@ impl Whitenoise {
)));
}

let result = self
.nostr
.publish_event_to(evolution_event, &group_relays)
.await;

match result {
Ok(_event_id) => {
// Evolution event published successfully
// Fan out the welcome message to all members
for (welcome_rumor, contact) in welcome_rumors.iter().zip(contacts) {
// Get the public key of the member from the key package event
let key_package_event_id =
welcome_rumor
.tags
.event_ids()
.next()
.ok_or(WhitenoiseError::Other(anyhow::anyhow!(
"No event ID found in welcome rumor"
)))?;

let member_pubkey = key_package_events
.iter()
.find(|event| event.id == *key_package_event_id)
.map(|event| event.pubkey)
.ok_or(WhitenoiseError::Other(anyhow::anyhow!(
"No public key found in key package event"
)))?;

// Create a timestamp 1 month in the future
use std::ops::Add;
let one_month_future = Timestamp::now().add(30 * 24 * 60 * 60);
self.nostr
.publish_gift_wrap_with_signer(
&member_pubkey,
welcome_rumor.clone(),
vec![Tag::expiration(one_month_future)],
contact.inbox_relays,
keys.clone(),
)
.await
.map_err(WhitenoiseError::from)?;
}
}
Err(e) => {
return Err(WhitenoiseError::NostrManager(e));
// Check if we have any relays to publish to and publish the evolution event
if group_relays.is_empty() {
tracing::warn!(
target: "whitenoise::add_members_to_group",
"Group has no relays configured, using account's default relays"
);
// Use the account's default relays as fallback
let fallback_relays: std::collections::BTreeSet<RelayUrl> = account
.nip65_relays
.iter()
.map(|relay_ref| relay_ref.clone())
.collect();
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, &group_relays)
.await?;
}

// Evolution event published successfully
// Fan out the welcome message to all members
for (welcome_rumor, contact) in welcome_rumors.iter().zip(contacts) {
// Get the public key of the member from the key package event
let key_package_event_id =
welcome_rumor
.tags
.event_ids()
.next()
.ok_or(WhitenoiseError::Other(anyhow::anyhow!(
"No event ID found in welcome rumor"
)))?;

let member_pubkey = key_package_events
.iter()
.find(|event| event.id == *key_package_event_id)
.map(|event| event.pubkey)
.ok_or(WhitenoiseError::Other(anyhow::anyhow!(
"No public key found in key package event"
)))?;

// Create a timestamp 1 month in the future
let one_month_future = Timestamp::now() + Duration::from_secs(30 * 24 * 60 * 60);

// Use fallback relays if contact has no inbox relays configured
let relays_to_use = if contact.inbox_relays.is_empty() {
Account::default_relays()
} else {
contact.inbox_relays
};

self.nostr
.publish_gift_wrap_with_signer(
&member_pubkey,
welcome_rumor.clone(),
vec![Tag::expiration(one_month_future)],
relays_to_use,
keys.clone(),
)
.await
.map_err(WhitenoiseError::from)?;
}

Ok(())
Expand Down Expand Up @@ -420,22 +447,28 @@ mod tests {

#[tokio::test]
async fn test_create_group() {
let whitenoise = test_get_whitenoise().await;
let (whitenoise, _data_temp, _logs_temp) = create_mock_whitenoise().await;

// Setup creator account
let (creator_account, _creator_keys) = setup_login_account(whitenoise).await;
let creator_account = whitenoise.create_identity().await.unwrap();

// Setup member accounts
let member_accounts = setup_multiple_test_accounts(whitenoise, &creator_account, 2).await;
let member_pubkeys: Vec<PublicKey> =
member_accounts.iter().map(|(acc, _)| acc.pubkey).collect();
let mut member_pubkeys = Vec::new();
for _ in 0..2 {
let member_account = whitenoise.create_identity().await.unwrap();
whitenoise
.add_contact(&creator_account, member_account.pubkey)
.await
.unwrap();
member_pubkeys.push(member_account.pubkey);
}

// Setup admin accounts (creator + one member as admin)
let admin_pubkeys = vec![creator_account.pubkey, member_pubkeys[0]];

// Test for success case
case_create_group_success(
whitenoise,
&whitenoise,
&creator_account,
member_pubkeys.clone(),
admin_pubkeys.clone(),
Expand All @@ -445,7 +478,7 @@ mod tests {
// // Test case: Key package fetch fails (invalid member)
// let invalid_member_pubkey = create_test_keys().public_key();
// case_create_group_key_package_fetch_fails(
// whitenoise,
// &whitenoise,
// &creator_account,
// vec![invalid_member_pubkey],
// admin_pubkeys.clone(),
Expand All @@ -454,7 +487,7 @@ mod tests {

// Test case: Empty admin list
case_create_group_empty_admin_list(
whitenoise,
&whitenoise,
&creator_account,
member_pubkeys.clone(),
vec![], // Empty admin list
Expand All @@ -464,7 +497,7 @@ mod tests {
// Test case: Invalid admin pubkey (not a member)
let non_member_pubkey = create_test_keys().public_key();
case_create_group_invalid_admin_pubkey(
whitenoise,
&whitenoise,
&creator_account,
member_pubkeys.clone(),
vec![creator_account.pubkey, non_member_pubkey],
Expand Down
Loading
Loading