@@ -5,6 +5,8 @@ use once_cell::sync::OnceCell;
55use tokio:: sync:: Mutex ;
66use tauri:: { AppHandle , Emitter , Manager , Runtime , WebviewWindowBuilder , WebviewUrl } ;
77use tauri_plugin_notification:: NotificationExt ;
8+ use rand:: { thread_rng, Rng } ;
9+ use rand:: distributions:: Alphanumeric ;
810
911mod crypto;
1012
@@ -65,6 +67,14 @@ static TAURI_APP: OnceCell<AppHandle> = OnceCell::new();
6567// TODO: REMOVE AFTER SEVERAL UPDATES - This static is only needed for the one-time migration from nonce-based to hash-based storage
6668static PENDING_MIGRATION : OnceCell < std:: collections:: HashMap < String , ( String , String ) > > = OnceCell :: new ( ) ;
6769
70+ #[ derive( Clone ) ]
71+ struct PendingInviteAcceptance {
72+ invite_code : String ,
73+ inviter_pubkey : PublicKey ,
74+ }
75+
76+ static PENDING_INVITE : OnceCell < PendingInviteAcceptance > = OnceCell :: new ( ) ;
77+
6878
6979
7080
@@ -887,6 +897,8 @@ async fn migrate_nonce_files_to_hash<R: Runtime>(handle: &AppHandle<R>) -> Resul
887897/// Pre-fetch the configs from our preferred NIP-96 servers to speed up uploads
888898#[ tauri:: command]
889899async fn warmup_nip96_servers ( ) -> bool {
900+ use nostr_sdk:: nips:: nip96:: get_server_config;
901+
890902 // Public Fileserver
891903 if PUBLIC_NIP96_CONFIG . get ( ) . is_none ( ) {
892904 let _ = match get_server_config ( Url :: parse ( TRUSTED_PUBLIC_NIP96 ) . unwrap ( ) , None ) . await {
@@ -2014,6 +2026,37 @@ async fn encrypt(input: String, password: Option<String>) -> String {
20142026 _ => ( )
20152027 }
20162028
2029+ // Check if we have a pending invite acceptance to broadcast
2030+ if let Some ( pending_invite) = PENDING_INVITE . get ( ) {
2031+ // Get the Nostr client
2032+ if let Some ( client) = NOSTR_CLIENT . get ( ) {
2033+ // Clone the data we need before the async block
2034+ let invite_code = pending_invite. invite_code . clone ( ) ;
2035+ let inviter_pubkey = pending_invite. inviter_pubkey . clone ( ) ;
2036+
2037+ // Spawn the broadcast in a separate task to avoid blocking
2038+ tokio:: spawn ( async move {
2039+ // Create and publish the acceptance event
2040+ let event_builder = EventBuilder :: new ( Kind :: ApplicationSpecificData , "vector_invite_accepted" )
2041+ . tag ( Tag :: custom ( TagKind :: Custom ( "l" . into ( ) ) , vec ! [ "vector" ] ) )
2042+ . tag ( Tag :: custom ( TagKind :: Custom ( "d" . into ( ) ) , vec ! [ invite_code. as_str( ) ] ) )
2043+ . tag ( Tag :: public_key ( inviter_pubkey) ) ;
2044+
2045+ // Build the event
2046+ match client. sign_event_builder ( event_builder) . await {
2047+ Ok ( event) => {
2048+ // Send only to trusted relay
2049+ match client. send_event_to ( [ TRUSTED_RELAY ] , & event) . await {
2050+ Ok ( _) => println ! ( "Successfully broadcast invite acceptance to trusted relay" ) ,
2051+ Err ( e) => eprintln ! ( "Failed to broadcast invite acceptance: {}" , e) ,
2052+ }
2053+ }
2054+ Err ( e) => eprintln ! ( "Failed to sign invite acceptance event: {}" , e) ,
2055+ }
2056+ } ) ;
2057+ }
2058+ }
2059+
20172060 res
20182061}
20192062
@@ -2214,6 +2257,199 @@ async fn download_whisper_model<R: Runtime>(_handle: AppHandle<R>, _model_name:
22142257 Err ( "Whisper model download is not supported on this platform" . to_string ( ) )
22152258}
22162259
2260+ /// Generate a random alphanumeric invite code
2261+ fn generate_invite_code ( ) -> String {
2262+ thread_rng ( )
2263+ . sample_iter ( & Alphanumeric )
2264+ . take ( 8 )
2265+ . map ( char:: from)
2266+ . collect :: < String > ( )
2267+ . to_uppercase ( )
2268+ }
2269+
2270+ /// Generate or retrieve existing invite code for the current user
2271+ #[ tauri:: command]
2272+ async fn get_or_create_invite_code ( ) -> Result < String , String > {
2273+ let handle = TAURI_APP . get ( ) . ok_or ( "App handle not initialized" ) ?;
2274+
2275+ // Check if we already have a stored invite code
2276+ if let Ok ( Some ( existing_code) ) = db:: get_invite_code ( handle. clone ( ) ) {
2277+ return Ok ( existing_code) ;
2278+ }
2279+
2280+ // No local code found, check the network
2281+ let client = NOSTR_CLIENT . get ( ) . ok_or ( "Nostr client not initialized" ) ?;
2282+
2283+ // Get our public key
2284+ let signer = client. signer ( ) . await . map_err ( |e| e. to_string ( ) ) ?;
2285+ let my_public_key = signer. get_public_key ( ) . await . map_err ( |e| e. to_string ( ) ) ?;
2286+
2287+ // Check if we've already published an invite on the network
2288+ let filter = Filter :: new ( )
2289+ . author ( my_public_key)
2290+ . kind ( Kind :: ApplicationSpecificData )
2291+ . custom_tag ( SingleLetterTag :: lowercase ( Alphabet :: D ) , "vector" )
2292+ . limit ( 100 ) ;
2293+
2294+ let events = client
2295+ . fetch_events ( filter, std:: time:: Duration :: from_secs ( 10 ) )
2296+ . await
2297+ . map_err ( |e| e. to_string ( ) ) ?;
2298+
2299+ // Look for existing invite events
2300+ for event in events {
2301+ if event. content == "vector_invite" {
2302+ // Extract the r tag (invite code)
2303+ if let Some ( r_tag) = event. tags . find ( TagKind :: Custom ( Cow :: Borrowed ( "r" ) ) ) {
2304+ if let Some ( code) = r_tag. content ( ) {
2305+ // Store it locally
2306+ db:: set_invite_code ( handle. clone ( ) , code. to_string ( ) )
2307+ . map_err ( |e| e. to_string ( ) ) ?;
2308+ return Ok ( code. to_string ( ) ) ;
2309+ }
2310+ }
2311+ }
2312+ }
2313+
2314+ // No existing invite found anywhere, generate a new one
2315+ let new_code = generate_invite_code ( ) ;
2316+
2317+ // Create and publish the invite event
2318+ let event_builder = EventBuilder :: new ( Kind :: ApplicationSpecificData , "vector_invite" )
2319+ . tag ( Tag :: custom ( TagKind :: d ( ) , vec ! [ "vector" ] ) )
2320+ . tag ( Tag :: custom ( TagKind :: Custom ( "r" . into ( ) ) , vec ! [ new_code. as_str( ) ] ) ) ;
2321+
2322+ // Build the event
2323+ let event = client. sign_event_builder ( event_builder) . await . map_err ( |e| e. to_string ( ) ) ?;
2324+
2325+ // Send only to trusted relay
2326+ client. send_event_to ( [ TRUSTED_RELAY ] , & event) . await . map_err ( |e| e. to_string ( ) ) ?;
2327+
2328+ // Store locally
2329+ db:: set_invite_code ( handle. clone ( ) , new_code. clone ( ) )
2330+ . map_err ( |e| e. to_string ( ) ) ?;
2331+
2332+ Ok ( new_code)
2333+ }
2334+
2335+ /// Accept an invite code from another user (deferred until after encryption setup)
2336+ #[ tauri:: command]
2337+ async fn accept_invite_code ( invite_code : String ) -> Result < String , String > {
2338+ let client = NOSTR_CLIENT . get ( ) . ok_or ( "Nostr client not initialized" ) ?;
2339+
2340+ // Validate invite code format (8 alphanumeric characters)
2341+ if invite_code. len ( ) != 8 || !invite_code. chars ( ) . all ( |c| c. is_alphanumeric ( ) ) {
2342+ return Err ( "Invalid invite code format" . to_string ( ) ) ;
2343+ }
2344+
2345+ // Search for the invite event
2346+ let filter = Filter :: new ( )
2347+ . kind ( Kind :: ApplicationSpecificData )
2348+ . custom_tag ( SingleLetterTag :: lowercase ( Alphabet :: D ) , "vector" )
2349+ . custom_tag ( SingleLetterTag :: lowercase ( Alphabet :: R ) , & invite_code)
2350+ . limit ( 1 ) ;
2351+
2352+ let events = client
2353+ . fetch_events ( filter, std:: time:: Duration :: from_secs ( 10 ) )
2354+ . await
2355+ . map_err ( |e| e. to_string ( ) ) ?;
2356+
2357+ // Find the invite event
2358+ let invite_event = events
2359+ . into_iter ( )
2360+ . find ( |e| e. content == "vector_invite" )
2361+ . ok_or ( "Invite code not found" ) ?;
2362+
2363+ // Get the inviter's public key
2364+ let inviter_pubkey = invite_event. pubkey ;
2365+ let inviter_npub = inviter_pubkey. to_bech32 ( ) . map_err ( |e| e. to_string ( ) ) ?;
2366+
2367+ // Get our public key
2368+ let signer = client. signer ( ) . await . map_err ( |e| e. to_string ( ) ) ?;
2369+ let my_public_key = signer. get_public_key ( ) . await . map_err ( |e| e. to_string ( ) ) ?;
2370+
2371+ // Check if we're trying to accept our own invite
2372+ if inviter_pubkey == my_public_key {
2373+ return Err ( "Cannot accept your own invite code" . to_string ( ) ) ;
2374+ }
2375+
2376+ // Store the pending invite acceptance (will be broadcast after encryption setup)
2377+ let pending_invite = PendingInviteAcceptance {
2378+ invite_code : invite_code. clone ( ) ,
2379+ inviter_pubkey : inviter_pubkey. clone ( ) ,
2380+ } ;
2381+
2382+ // Try to set the pending invite, ignore if already set
2383+ let _ = PENDING_INVITE . set ( pending_invite) ;
2384+
2385+ // Return the inviter's npub so the frontend can initiate a chat
2386+ Ok ( inviter_npub)
2387+ }
2388+
2389+ /// Get the count of unique users who accepted invites from a given npub
2390+ #[ tauri:: command]
2391+ async fn get_invited_users ( npub : String ) -> Result < u32 , String > {
2392+ let client = NOSTR_CLIENT . get ( ) . ok_or ( "Nostr client not initialized" ) ?;
2393+
2394+ // Convert npub to PublicKey
2395+ let inviter_pubkey = PublicKey :: from_bech32 ( & npub) . map_err ( |e| e. to_string ( ) ) ?;
2396+
2397+ // First, get the inviter's invite code from the trusted relay
2398+ let filter = Filter :: new ( )
2399+ . author ( inviter_pubkey)
2400+ . kind ( Kind :: ApplicationSpecificData )
2401+ . custom_tag ( SingleLetterTag :: lowercase ( Alphabet :: D ) , "vector" )
2402+ . limit ( 100 ) ;
2403+
2404+ let events = client
2405+ . fetch_events_from ( vec ! [ TRUSTED_RELAY ] , filter, std:: time:: Duration :: from_secs ( 10 ) )
2406+ . await
2407+ . map_err ( |e| e. to_string ( ) ) ?;
2408+
2409+ // Find the invite event and extract the invite code
2410+ let invite_code = events
2411+ . iter ( )
2412+ . find ( |e| e. content == "vector_invite" )
2413+ . and_then ( |e| e. tags . find ( TagKind :: Custom ( Cow :: Borrowed ( "r" ) ) ) )
2414+ . and_then ( |tag| tag. content ( ) )
2415+ . ok_or ( "No invite code found for this user" ) ?;
2416+
2417+ // Now fetch all acceptance events for this invite code from the trusted relay
2418+ let acceptance_filter = Filter :: new ( )
2419+ . kind ( Kind :: ApplicationSpecificData )
2420+ . custom_tag ( SingleLetterTag :: lowercase ( Alphabet :: D ) , invite_code)
2421+ . limit ( 1000 ) ; // Allow fetching many acceptances
2422+
2423+ let acceptance_events = client
2424+ . fetch_events_from ( vec ! [ TRUSTED_RELAY ] , acceptance_filter, std:: time:: Duration :: from_secs ( 10 ) )
2425+ . await
2426+ . map_err ( |e| e. to_string ( ) ) ?;
2427+
2428+ // Filter for acceptance events that reference our inviter and collect unique acceptors
2429+ let mut unique_acceptors = std:: collections:: HashSet :: new ( ) ;
2430+
2431+ for event in acceptance_events {
2432+ if event. content == "vector_invite_accepted" {
2433+ // Check if this acceptance references our inviter
2434+ let references_inviter = event. tags
2435+ . iter ( )
2436+ . any ( |tag| {
2437+ if let Some ( TagStandard :: PublicKey { public_key, .. } ) = tag. as_standardized ( ) {
2438+ * public_key == inviter_pubkey
2439+ } else {
2440+ false
2441+ }
2442+ } ) ;
2443+
2444+ if references_inviter {
2445+ unique_acceptors. insert ( event. pubkey ) ;
2446+ }
2447+ }
2448+ }
2449+
2450+ Ok ( unique_acceptors. len ( ) as u32 )
2451+ }
2452+
22172453#[ cfg_attr( mobile, tauri:: mobile_entry_point) ]
22182454pub fn run ( ) {
22192455 #[ cfg( target_os = "linux" ) ]
@@ -2383,6 +2619,11 @@ pub fn run() {
23832619 get_platform_features,
23842620 transcribe,
23852621 download_whisper_model,
2622+ get_or_create_invite_code,
2623+ accept_invite_code,
2624+ get_invited_users,
2625+ db:: get_invite_code,
2626+ db:: set_invite_code,
23862627 #[ cfg( all( not( target_os = "android" ) , feature = "whisper" ) ) ]
23872628 whisper:: delete_whisper_model,
23882629 #[ cfg( all( not( target_os = "android" ) , feature = "whisper" ) ) ]
0 commit comments