@@ -35,10 +35,12 @@ final class NostrRelayManager: ObservableObject {
3535 " wss://nostr21.com "
3636 // For local testing, you can add: "ws://localhost:8080"
3737 ]
38+ private static let defaultRelaySet = Set ( defaultRelays)
3839
3940 @Published private( set) var relays : [ Relay ] = [ ]
4041 @Published private( set) var isConnected = false
4142
43+ private var allowDefaultRelays : Bool = false
4244 private var connections : [ String : URLSessionWebSocketTask ] = [ : ]
4345 private var subscriptions : [ String : Set < String > ] = [ : ] // relay URL -> active subscription IDs
4446 private var pendingSubscriptions : [ String : [ String : String ] ] = [ : ] // relay URL -> (subscription id -> encoded REQ JSON)
@@ -65,6 +67,8 @@ final class NostrRelayManager: ObservableObject {
6567 private let messageQueueLock = NSLock ( )
6668 private let encoder = JSONEncoder ( )
6769 private let decoder = JSONDecoder ( )
70+ private var networkService : NetworkActivationService { NetworkActivationService . shared }
71+ private var shouldUseTor : Bool { networkService. userTorEnabled }
6872
6973 // Exponential backoff configuration
7074 private let initialBackoffInterval : TimeInterval = TransportConfig . nostrRelayInitialBackoffSeconds
@@ -78,29 +82,45 @@ final class NostrRelayManager: ObservableObject {
7882 private var connectionGeneration : Int = 0
7983
8084 init ( ) {
81- // Initialize with default relays
82- self . relays = Self . defaultRelays. map { Relay ( url: $0) }
85+ let hasMutual = !FavoritesPersistenceService. shared. mutualFavorites. isEmpty
86+ allowDefaultRelays = hasMutual
87+ if hasMutual {
88+ self . relays = Self . defaultRelays. map { Relay ( url: $0) }
89+ }
8390 // Deterministic JSON shape for outbound requests
8491 self . encoder. outputFormatting = . sortedKeys
92+ FavoritesPersistenceService . shared. $mutualFavorites
93+ . receive ( on: DispatchQueue . main)
94+ . sink { [ weak self] favorites in
95+ self ? . updateDefaultRelayPolicy ( hasMutual: !favorites. isEmpty)
96+ }
97+ . store ( in: & cancellables)
8598 }
8699
87100 /// Connect to all configured relays
88101 func connect( ) {
89102 // Global network policy gate
90- if !TorManager. shared. isAutoStartAllowed ( ) { return }
91- // Ensure Tor is started early and wait for readiness off-main; then hop back to connect.
92- Task . detached {
93- let ready = await TorManager . shared. awaitReady ( )
94- await MainActor . run {
95- if !ready {
96- SecureLogger . error ( " ❌ Tor not ready; aborting relay connections (fail-closed) " , category: . session)
97- return
98- }
99- SecureLogger . debug ( " 🌐 Connecting to \( self . relays. count) Nostr relays (via Tor) " , category: . session)
100- for relay in self . relays {
101- self . connectToRelay ( relay. url)
103+ guard networkService. activationAllowed else { return }
104+ if shouldUseTor {
105+ // Ensure Tor is started early and wait for readiness off-main; then hop back to connect.
106+ Task . detached {
107+ let ready = await TorManager . shared. awaitReady ( )
108+ await MainActor . run {
109+ if !ready {
110+ SecureLogger . error ( " ❌ Tor not ready; aborting relay connections (fail-closed) " , category: . session)
111+ return
112+ }
113+ SecureLogger . debug ( " 🌐 Connecting to \( self . relays. count) Nostr relays (via Tor) " , category: . session)
114+ for relay in self . relays {
115+ self . connectToRelay ( relay. url)
116+ }
102117 }
103118 }
119+ } else {
120+ SecureLogger . debug ( " 🌐 Connecting to \( self . relays. count) Nostr relays (direct) " , category: . session)
121+ for relay in self . relays {
122+ connectToRelay ( relay. url)
123+ }
104124 }
105125 }
106126
@@ -120,8 +140,10 @@ final class NostrRelayManager: ObservableObject {
120140 /// Ensure connections exist to the given relay URLs (idempotent).
121141 func ensureConnections( to relayUrls: [ String ] ) {
122142 // Global network policy gate
123- if !TorManager. shared. isAutoStartAllowed ( ) { return }
124- if TorManager . shared. torEnforced && !TorManager. shared. isReady {
143+ guard networkService. activationAllowed else { return }
144+ let targets = allowedRelayList ( from: relayUrls)
145+ guard !targets. isEmpty else { return }
146+ if shouldUseTor && TorManager . shared. torEnforced && !TorManager. shared. isReady {
125147 // Defer until Tor is fully ready; avoid queuing connection attempts early
126148 Task . detached { [ weak self] in
127149 guard let self = self else { return }
@@ -130,22 +152,21 @@ final class NostrRelayManager: ObservableObject {
130152 }
131153 return
132154 }
133- let existing = Set ( relays. map { $0. url } )
134- for url in Set ( relayUrls) {
135- if !existing. contains ( url) {
136- relays. append ( Relay ( url: url) )
137- }
138- if connections [ url] == nil {
139- connectToRelay ( url)
140- }
155+ var existing = Set ( relays. map { $0. url } )
156+ for url in targets where !existing. contains ( url) {
157+ relays. append ( Relay ( url: url) )
158+ existing. insert ( url)
159+ }
160+ for url in targets where connections [ url] == nil {
161+ connectToRelay ( url)
141162 }
142163 }
143164
144165 /// Send an event to specified relays (or all if none specified)
145166 func sendEvent( _ event: NostrEvent , to relayUrls: [ String ] ? = nil ) {
146167 // Global network policy gate
147- if !TorManager . shared . isAutoStartAllowed ( ) { return }
148- if TorManager . shared. torEnforced && !TorManager. shared. isReady {
168+ guard networkService . activationAllowed else { return }
169+ if shouldUseTor && TorManager . shared. torEnforced && !TorManager. shared. isReady {
149170 // Defer sends until Tor is ready to avoid premature queueing
150171 Task . detached { [ weak self] in
151172 guard let self = self else { return }
@@ -154,7 +175,9 @@ final class NostrRelayManager: ObservableObject {
154175 }
155176 return
156177 }
157- let targetRelays = relayUrls ?? Self . defaultRelays
178+ let requestedRelays = relayUrls ?? Self . defaultRelays
179+ let targetRelays = allowedRelayList ( from: requestedRelays)
180+ guard !targetRelays. isEmpty else { return }
158181 ensureConnections ( to: targetRelays)
159182
160183 // Attempt immediate send to relays with active connections; queue the rest
@@ -220,7 +243,7 @@ final class NostrRelayManager: ObservableObject {
220243 onEOSE: ( ( ) -> Void ) ? = nil
221244 ) {
222245 // Global network policy gate
223- if !TorManager . shared . isAutoStartAllowed ( ) { return }
246+ guard networkService . activationAllowed else { return }
224247 // Coalesce rapid duplicate subscribe requests only if a handler already exists
225248 let now = Date ( )
226249 if messageHandlers [ id] != nil {
@@ -229,7 +252,7 @@ final class NostrRelayManager: ObservableObject {
229252 }
230253 }
231254 subscribeCoalesce [ id] = now
232- if TorManager . shared. torEnforced && !TorManager. shared. isReady {
255+ if shouldUseTor && TorManager . shared. torEnforced && !TorManager. shared. isReady {
233256 // Defer subscription setup until Tor is ready; avoid queuing subs early
234257 Task . detached { [ weak self] in
235258 guard let self = self else { return }
@@ -257,32 +280,37 @@ final class NostrRelayManager: ObservableObject {
257280
258281 // Target specific relays if provided; else default. Filter permanently failed relays.
259282 let baseUrls = relayUrls ?? Self . defaultRelays
260- let urls = baseUrls. filter { !isPermanentlyFailed( $0) }
283+ let candidateUrls = baseUrls. filter { !isPermanentlyFailed( $0) }
284+ let urls = allowedRelayList ( from: candidateUrls)
261285 // Always queue subscriptions; sending happens when a relay reports connected
262286 let existingSet = Set ( relays. map { $0. url } )
263287 for url in urls where !existingSet. contains ( url) {
264288 relays. append ( Relay ( url: url) )
265289 }
266- for url in urls {
290+ for url in candidateUrls {
267291 var map = self . pendingSubscriptions [ url] ?? [ : ]
268292 map [ id] = messageString
269293 self . pendingSubscriptions [ url] = map
270294 }
271295 // Initialize EOSE tracking if requested
272296 if let onEOSE = onEOSE {
273- var tracker = EOSETracker ( pendingRelays: Set ( urls) , callback: onEOSE, timer: nil )
274- // Fallback timeout to avoid hanging if a relay never sends EOSE
275- tracker. timer = Timer . scheduledTimer ( withTimeInterval: 2.0 , repeats: false ) { [ weak self] _ in
276- Task { @MainActor in
277- guard let self = self else { return }
278- if let t = self . eoseTrackers [ id] {
279- t. timer? . invalidate ( )
280- self . eoseTrackers. removeValue ( forKey: id)
281- onEOSE ( )
297+ if urls. isEmpty {
298+ onEOSE ( )
299+ } else {
300+ var tracker = EOSETracker ( pendingRelays: Set ( urls) , callback: onEOSE, timer: nil )
301+ // Fallback timeout to avoid hanging if a relay never sends EOSE
302+ tracker. timer = Timer . scheduledTimer ( withTimeInterval: 2.0 , repeats: false ) { [ weak self] _ in
303+ Task { @MainActor in
304+ guard let self = self else { return }
305+ if let t = self . eoseTrackers [ id] {
306+ t. timer? . invalidate ( )
307+ self . eoseTrackers. removeValue ( forKey: id)
308+ onEOSE ( )
309+ }
282310 }
283311 }
312+ eoseTrackers [ id] = tracker
284313 }
285- eoseTrackers [ id] = tracker
286314 }
287315 SecureLogger . debug ( " 📋 Queued subscription id= \( id) for \( urls. count) relay(s) " , category: . session)
288316 // Ensure we actually have sockets opening to these relays so queued REQs can flush
@@ -297,6 +325,54 @@ final class NostrRelayManager: ObservableObject {
297325 SecureLogger . error ( " ❌ Failed to encode subscription request: \( error) " , category: . session)
298326 }
299327 }
328+
329+ private func updateDefaultRelayPolicy( hasMutual: Bool ) {
330+ guard hasMutual != allowDefaultRelays else { return }
331+ allowDefaultRelays = hasMutual
332+ if hasMutual {
333+ var existing = Set ( relays. map { $0. url } )
334+ for url in Self . defaultRelays where !existing. contains ( url) {
335+ relays. append ( Relay ( url: url) )
336+ existing. insert ( url)
337+ }
338+ if networkService. activationAllowed {
339+ ensureConnections ( to: Self . defaultRelays)
340+ }
341+ } else {
342+ for url in Self . defaultRelays {
343+ if let connection = connections [ url] {
344+ connection. cancel ( with: . goingAway, reason: nil )
345+ }
346+ connections. removeValue ( forKey: url)
347+ subscriptions. removeValue ( forKey: url)
348+ }
349+ messageQueueLock. lock ( )
350+ for index in ( 0 ..< messageQueue. count) . reversed ( ) {
351+ var item = messageQueue [ index]
352+ item. pendingRelays. subtract ( Self . defaultRelaySet)
353+ if item. pendingRelays. isEmpty {
354+ messageQueue. remove ( at: index)
355+ } else {
356+ messageQueue [ index] = item
357+ }
358+ }
359+ messageQueueLock. unlock ( )
360+ relays. removeAll { Self . defaultRelaySet. contains ( $0. url) }
361+ updateConnectionStatus ( )
362+ }
363+ }
364+
365+ private func allowedRelayList( from urls: [ String ] ) -> [ String ] {
366+ var seen = Set < String > ( )
367+ var result : [ String ] = [ ]
368+ for url in urls {
369+ if !allowDefaultRelays && Self . defaultRelaySet. contains ( url) { continue }
370+ if seen. insert ( url) . inserted {
371+ result. append ( url)
372+ }
373+ }
374+ return result
375+ }
300376
301377 /// Unsubscribe from a subscription
302378 func unsubscribe( id: String ) {
@@ -326,14 +402,14 @@ final class NostrRelayManager: ObservableObject {
326402
327403 private func connectToRelay( _ urlString: String ) {
328404 // Global network policy gate
329- if !TorManager . shared . isAutoStartAllowed ( ) { return }
405+ guard networkService . activationAllowed else { return }
330406 guard let url = URL ( string: urlString) else {
331407 SecureLogger . warning ( " Invalid relay URL: \( urlString) " , category: . session)
332408 return
333409 }
334410
335411 // Avoid initiating connections while app is backgrounded; we'll reconnect on foreground
336- if TorManager . shared. torEnforced && !TorManager. shared. isForeground ( ) {
412+ if shouldUseTor && TorManager . shared. torEnforced && !TorManager. shared. isForeground ( ) {
337413 return
338414 }
339415
@@ -348,7 +424,7 @@ final class NostrRelayManager: ObservableObject {
348424 // Attempting to connect to Nostr relay via the proxied session
349425
350426 // If Tor is enforced but not ready, delay connection until it is.
351- if TorManager . shared. torEnforced && !TorManager. shared. isReady {
427+ if shouldUseTor && TorManager . shared. torEnforced && !TorManager. shared. isReady {
352428 Task . detached { [ weak self] in
353429 guard let self = self else { return }
354430 let ready = await TorManager . shared. awaitReady ( )
@@ -534,7 +610,7 @@ final class NostrRelayManager: ObservableObject {
534610
535611 private func handleDisconnection( relayUrl: String , error: Error ) {
536612 // If networking is disallowed, do not schedule reconnection
537- if !TorManager . shared . isAutoStartAllowed ( ) {
613+ if !networkService . activationAllowed {
538614 connections. removeValue ( forKey: relayUrl)
539615 subscriptions. removeValue ( forKey: relayUrl)
540616 updateRelayStatus ( relayUrl, isConnected: false , error: error)
0 commit comments