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
107 changes: 107 additions & 0 deletions src/nostr_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,111 @@ impl NostrManager {
}
}
}

/// Counts active subscriptions for a specific account by checking subscription IDs.
/// Returns the number of subscriptions that contain the account's hashed pubkey.
pub(crate) async fn count_subscriptions_for_account(&self, pubkey: &PublicKey) -> usize {
let hash = self.create_pubkey_hash(pubkey);
let prefix = format!("{}_", hash);
self.client
.subscriptions()
.await
.keys()
.filter(|id| id.as_str().starts_with(&prefix))
.count()
}

/// Counts active global subscriptions by checking for subscription IDs that start with "global_users_".
pub(crate) async fn count_global_subscriptions(&self) -> usize {
self.client
.subscriptions()
.await
.keys()
.filter(|id| id.as_str().starts_with("global_users_"))
.count()
}

/// Checks if at least one relay in the provided list is connected or connecting.
/// Returns true if any relay is in Connected or Connecting state.
pub(crate) async fn has_any_relay_connected(&self, relay_urls: &[RelayUrl]) -> bool {
for relay_url in relay_urls {
match self.get_relay_status(relay_url).await {
Ok(RelayStatus::Connected | RelayStatus::Connecting) => return true,
_ => continue,
}
}
false
}
}

#[cfg(test)]
mod subscription_monitoring_tests {
use super::*;
use crate::whitenoise::event_tracker::NoEventTracker;
use std::sync::Arc;
use tokio::sync::mpsc;

#[tokio::test]
async fn test_count_subscriptions_for_account_empty() {
let (event_sender, _receiver) = mpsc::channel(100);
let event_tracker = Arc::new(NoEventTracker);
let nostr_manager =
NostrManager::new(event_sender, event_tracker, NostrManager::default_timeout())
.await
.unwrap();

let pubkey = Keys::generate().public_key();
let count = nostr_manager.count_subscriptions_for_account(&pubkey).await;

// Should return 0 when no subscriptions exist
assert_eq!(count, 0);
}

#[tokio::test]
async fn test_count_global_subscriptions_empty() {
let (event_sender, _receiver) = mpsc::channel(100);
let event_tracker = Arc::new(NoEventTracker);
let nostr_manager =
NostrManager::new(event_sender, event_tracker, NostrManager::default_timeout())
.await
.unwrap();

let count = nostr_manager.count_global_subscriptions().await;

// Should return 0 when no global subscriptions exist
assert_eq!(count, 0);
}

#[tokio::test]
async fn test_has_any_relay_connected_empty_list() {
let (event_sender, _receiver) = mpsc::channel(100);
let event_tracker = Arc::new(NoEventTracker);
let nostr_manager =
NostrManager::new(event_sender, event_tracker, NostrManager::default_timeout())
.await
.unwrap();

let relay_urls: Vec<RelayUrl> = vec![];
let result = nostr_manager.has_any_relay_connected(&relay_urls).await;

// Should return false with empty relay list
assert!(!result);
}

#[tokio::test]
async fn test_has_any_relay_connected_disconnected() {
let (event_sender, _receiver) = mpsc::channel(100);
let event_tracker = Arc::new(NoEventTracker);
let nostr_manager =
NostrManager::new(event_sender, event_tracker, NostrManager::default_timeout())
.await
.unwrap();

// Create a relay URL that doesn't exist in the client
let relay_url = RelayUrl::parse("wss://relay.example.com").unwrap();
let result = nostr_manager.has_any_relay_connected(&[relay_url]).await;

// Should return false when relay is not in the client pool
assert!(!result);
}
}
2 changes: 1 addition & 1 deletion src/nostr_manager/subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::nostr_manager::{
impl NostrManager {
/// Create a short hash from a pubkey for use in subscription IDs
/// Uses first 12 characters of SHA256 hash for privacy and collision resistance, salted per session
fn create_pubkey_hash(&self, pubkey: &PublicKey) -> String {
pub(crate) fn create_pubkey_hash(&self, pubkey: &PublicKey) -> String {
let mut hasher = Sha256::new();
hasher.update(self.session_salt());
hasher.update(pubkey.to_bytes());
Expand Down
78 changes: 77 additions & 1 deletion src/whitenoise/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ impl Whitenoise {
}

/// Extract group data including relay URLs and group IDs for subscription setup.
async fn extract_groups_relays_and_ids(
pub(crate) async fn extract_groups_relays_and_ids(
&self,
account: &Account,
) -> Result<(Vec<RelayUrl>, Vec<String>)> {
Expand Down Expand Up @@ -1409,4 +1409,80 @@ mod tests {
assert_eq!(published_metadata.about, new_metadata.about);
}
}

#[tokio::test]
async fn test_extract_groups_relays_and_ids_no_groups() {
let (whitenoise, _data_temp, _logs_temp) = create_mock_whitenoise().await;
let account = whitenoise.create_identity().await.unwrap();

let (relays, group_ids) = whitenoise
.extract_groups_relays_and_ids(&account)
.await
.unwrap();

assert!(
relays.is_empty(),
"Should have no relays when account has no groups"
);
assert!(
group_ids.is_empty(),
"Should have no group IDs when account has no groups"
);
}

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

// Create creator and member accounts
let creator_account = whitenoise.create_identity().await.unwrap();
let member_account = whitenoise.create_identity().await.unwrap();

// Allow time for key packages to be published
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;

let relay1 = RelayUrl::parse("ws://localhost:8080").unwrap();
let relay2 = RelayUrl::parse("ws://localhost:7777").unwrap();

// Create a group with specific relays
let config = NostrGroupConfigData::new(
"Test Group".to_string(),
"Test Description".to_string(),
None,
None,
None,
vec![relay1.clone(), relay2.clone()],
vec![creator_account.pubkey],
);

let group = whitenoise
.create_group(&creator_account, vec![member_account.pubkey], config, None)
.await
.unwrap();

// Extract groups relays and IDs
let (relays, group_ids) = whitenoise
.extract_groups_relays_and_ids(&creator_account)
.await
.unwrap();

// Verify relays were extracted
assert!(!relays.is_empty(), "Should have relays from the group");
assert!(
relays.contains(&relay1),
"Should contain relay1 from group config"
);
assert!(
relays.contains(&relay2),
"Should contain relay2 from group config"
);

// Verify group ID was extracted
assert_eq!(group_ids.len(), 1, "Should have one group ID");
assert_eq!(
group_ids[0],
hex::encode(group.nostr_group_id),
"Group ID should match the created group"
);
}
}
Loading