mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
feat(georelay): fetch daily from remote CSV with fallback to bundled; cache to app support; prefetch on app load
This commit is contained in:
@@ -64,10 +64,6 @@
|
|||||||
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */; };
|
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */; };
|
||||||
38EDDC049FD56B1BB1F14C91 /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BA20BC0F123F1507C5C247 /* IdentityModels.swift */; };
|
38EDDC049FD56B1BB1F14C91 /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BA20BC0F123F1507C5C247 /* IdentityModels.swift */; };
|
||||||
3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; };
|
3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; };
|
||||||
E0A1B2C3D4E5F6012345678B /* GeoRelayDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */; };
|
|
||||||
E0A1B2C3D4E5F6012345678C /* GeoRelayDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */; };
|
|
||||||
E0A1B2C3D4E5F6012345678D /* online_relays_gps.csv in Resources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F6012345678A /* online_relays_gps.csv */; };
|
|
||||||
E0A1B2C3D4E5F6012345678E /* online_relays_gps.csv in Resources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F6012345678A /* online_relays_gps.csv */; };
|
|
||||||
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
||||||
501BC56B1A08C0327A09AAF1 /* NoiseEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */; };
|
501BC56B1A08C0327A09AAF1 /* NoiseEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */; };
|
||||||
5C93B4FDD0C448C3EDDBF8AE /* FavoritesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */; };
|
5C93B4FDD0C448C3EDDBF8AE /* FavoritesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */; };
|
||||||
@@ -130,6 +126,10 @@
|
|||||||
D691938B4029A04CC905FDC8 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */; };
|
D691938B4029A04CC905FDC8 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */; };
|
||||||
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */; };
|
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */; };
|
||||||
D782AB596DDB5C846554F7C3 /* NostrIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8043995007F0D84438EDD9 /* NostrIdentity.swift */; };
|
D782AB596DDB5C846554F7C3 /* NostrIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8043995007F0D84438EDD9 /* NostrIdentity.swift */; };
|
||||||
|
E0A1B2C3D4E5F6012345678B /* GeoRelayDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */; };
|
||||||
|
E0A1B2C3D4E5F6012345678C /* GeoRelayDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */; };
|
||||||
|
E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F6012345678A /* relays/online_relays_gps.csv */; };
|
||||||
|
E0A1B2C3D4E5F6012345678E /* relays/online_relays_gps.csv in Resources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F6012345678A /* relays/online_relays_gps.csv */; };
|
||||||
E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */; };
|
E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */; };
|
||||||
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */; };
|
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */; };
|
||||||
EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7EFB209C86BBD956B749EC /* SecureLogger.swift */; };
|
EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7EFB209C86BBD956B749EC /* SecureLogger.swift */; };
|
||||||
@@ -225,8 +225,6 @@
|
|||||||
5318B743C64628A125261163 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = "<group>"; };
|
5318B743C64628A125261163 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = "<group>"; };
|
||||||
5BC5AB43F4A8FB62C935CD74 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = "<group>"; };
|
5BC5AB43F4A8FB62C935CD74 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = "<group>"; };
|
||||||
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrIdentity.swift; sourceTree = "<group>"; };
|
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrIdentity.swift; sourceTree = "<group>"; };
|
||||||
E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoRelayDirectory.swift; sourceTree = "<group>"; };
|
|
||||||
E0A1B2C3D4E5F6012345678A /* online_relays_gps.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = relays/online_relays_gps.csv; sourceTree = "<group>"; };
|
|
||||||
61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
|
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
|
||||||
78595178957244CBDF7E79B6 /* NostrRelayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRelayManager.swift; sourceTree = "<group>"; };
|
78595178957244CBDF7E79B6 /* NostrRelayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRelayManager.swift; sourceTree = "<group>"; };
|
||||||
@@ -249,6 +247,8 @@
|
|||||||
C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBluetoothMeshService.swift; sourceTree = "<group>"; };
|
C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBluetoothMeshService.swift; sourceTree = "<group>"; };
|
||||||
D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicChatE2ETests.swift; sourceTree = "<group>"; };
|
D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicChatE2ETests.swift; sourceTree = "<group>"; };
|
||||||
D69A18D27F9A565FD6041E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D69A18D27F9A565FD6041E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoRelayDirectory.swift; sourceTree = "<group>"; };
|
||||||
|
E0A1B2C3D4E5F6012345678A /* relays/online_relays_gps.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = relays/online_relays_gps.csv; sourceTree = "<group>"; };
|
||||||
E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; };
|
E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; };
|
||||||
E95DBE6A48626C5AE287245E /* NoiseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocolTests.swift; sourceTree = "<group>"; };
|
E95DBE6A48626C5AE287245E /* NoiseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocolTests.swift; sourceTree = "<group>"; };
|
||||||
EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2F82C5FC8433F4064F079D1F /* bitchat */,
|
2F82C5FC8433F4064F079D1F /* bitchat */,
|
||||||
E0A1B2C3D4E5F6012345678A /* online_relays_gps.csv */,
|
E0A1B2C3D4E5F6012345678A /* relays/online_relays_gps.csv */,
|
||||||
A2E8C336FA1ADBEC03261DFD /* bitchatShareExtension */,
|
A2E8C336FA1ADBEC03261DFD /* bitchatShareExtension */,
|
||||||
C3D98EB3E1B455E321F519F4 /* bitchatTests */,
|
C3D98EB3E1B455E321F519F4 /* bitchatTests */,
|
||||||
9F37F9F2C353B58AC809E93B /* Products */,
|
9F37F9F2C353B58AC809E93B /* Products */,
|
||||||
@@ -328,8 +328,8 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */,
|
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */,
|
||||||
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */,
|
|
||||||
527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */,
|
527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */,
|
||||||
|
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */,
|
||||||
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */,
|
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */,
|
||||||
EA706D8E5097785414646A8E /* Info.plist */,
|
EA706D8E5097785414646A8E /* Info.plist */,
|
||||||
95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */,
|
95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */,
|
||||||
@@ -526,7 +526,7 @@
|
|||||||
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */,
|
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */,
|
||||||
2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */,
|
2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */,
|
||||||
78595178957244CBDF7E79B6 /* NostrRelayManager.swift */,
|
78595178957244CBDF7E79B6 /* NostrRelayManager.swift */,
|
||||||
E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */,
|
E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */,
|
||||||
);
|
);
|
||||||
path = Nostr;
|
path = Nostr;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -691,7 +691,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */,
|
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */,
|
||||||
E0A1B2C3D4E5F6012345678D /* online_relays_gps.csv in Resources */,
|
E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -701,7 +701,7 @@
|
|||||||
files = (
|
files = (
|
||||||
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */,
|
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */,
|
||||||
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */,
|
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */,
|
||||||
E0A1B2C3D4E5F6012345678E /* online_relays_gps.csv in Resources */,
|
E0A1B2C3D4E5F6012345678E /* relays/online_relays_gps.csv in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -766,7 +766,7 @@
|
|||||||
D111988977C3BC246AB27FA4 /* SecureLogger.swift in Sources */,
|
D111988977C3BC246AB27FA4 /* SecureLogger.swift in Sources */,
|
||||||
8DE687D2EB5EB120868DBFB5 /* BLEService.swift in Sources */,
|
8DE687D2EB5EB120868DBFB5 /* BLEService.swift in Sources */,
|
||||||
AA11BB22CC33DD44EE55FF66 /* MessageTextHelpers.swift in Sources */,
|
AA11BB22CC33DD44EE55FF66 /* MessageTextHelpers.swift in Sources */,
|
||||||
E0A1B2C3D4E5F6012345678B /* GeoRelayDirectory.swift in Sources */,
|
E0A1B2C3D4E5F6012345678B /* GeoRelayDirectory.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -820,7 +820,7 @@
|
|||||||
EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */,
|
EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */,
|
||||||
C165DD35BB8E9C327A3C2DA4 /* BLEService.swift in Sources */,
|
C165DD35BB8E9C327A3C2DA4 /* BLEService.swift in Sources */,
|
||||||
AA11BB22CC33DD44EE55FF67 /* MessageTextHelpers.swift in Sources */,
|
AA11BB22CC33DD44EE55FF67 /* MessageTextHelpers.swift in Sources */,
|
||||||
E0A1B2C3D4E5F6012345678C /* GeoRelayDirectory.swift in Sources */,
|
E0A1B2C3D4E5F6012345678C /* GeoRelayDirectory.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ struct BitchatApp: App {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
|
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
|
||||||
|
// Warm up georelay directory and refresh if stale (once/day)
|
||||||
|
GeoRelayDirectory.shared.prefetchIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Directory of online Nostr relays with approximate GPS locations, used for geohash routing.
|
/// Directory of online Nostr relays with approximate GPS locations, used for geohash routing.
|
||||||
struct GeoRelayDirectory {
|
@MainActor
|
||||||
|
final class GeoRelayDirectory {
|
||||||
struct Entry: Hashable {
|
struct Entry: Hashable {
|
||||||
let host: String
|
let host: String
|
||||||
let lat: Double
|
let lat: Double
|
||||||
@@ -9,10 +10,17 @@ struct GeoRelayDirectory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static let shared = GeoRelayDirectory()
|
static let shared = GeoRelayDirectory()
|
||||||
private let entries: [Entry]
|
private(set) var entries: [Entry] = []
|
||||||
|
private let cacheFileName = "georelays_cache.csv"
|
||||||
|
private let lastFetchKey = "georelay.lastFetchAt"
|
||||||
|
private let remoteURL = URL(string: "https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv")!
|
||||||
|
private let fetchInterval: TimeInterval = 60 * 60 * 24 // 24h
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
self.entries = GeoRelayDirectory.loadEntries()
|
// Load cached or bundled data synchronously
|
||||||
|
self.entries = self.loadLocalEntries()
|
||||||
|
// Fire-and-forget remote refresh if stale
|
||||||
|
prefetchIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns up to `count` relay URLs (wss://) closest to the geohash center.
|
/// Returns up to `count` relay URLs (wss://) closest to the geohash center.
|
||||||
@@ -32,23 +40,72 @@ struct GeoRelayDirectory {
|
|||||||
return sorted.map { "wss://\($0.host)" }
|
return sorted.map { "wss://\($0.host)" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Loading
|
// MARK: - Remote Fetch
|
||||||
private static func loadEntries() -> [Entry] {
|
func prefetchIfNeeded() {
|
||||||
// Try bundled resource first
|
let now = Date()
|
||||||
if let url = Bundle.main.url(forResource: "online_relays_gps", withExtension: "csv") ??
|
let last = UserDefaults.standard.object(forKey: lastFetchKey) as? Date ?? .distantPast
|
||||||
Bundle.main.url(forResource: "online_relays_gps", withExtension: "csv", subdirectory: "relays"),
|
guard now.timeIntervalSince(last) >= fetchInterval else { return }
|
||||||
let data = try? Data(contentsOf: url),
|
fetchRemote()
|
||||||
let text = String(data: data, encoding: .utf8) {
|
}
|
||||||
return parseCSV(text)
|
|
||||||
|
private func fetchRemote() {
|
||||||
|
let req = URLRequest(url: remoteURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 15)
|
||||||
|
let task = URLSession.shared.dataTask(with: req) { [weak self] data, _, error in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let data = data, error == nil, let text = String(data: data, encoding: .utf8) {
|
||||||
|
let parsed = GeoRelayDirectory.parseCSV(text)
|
||||||
|
if !parsed.isEmpty {
|
||||||
|
Task { @MainActor in
|
||||||
|
self.entries = parsed
|
||||||
|
self.persistCache(text)
|
||||||
|
UserDefaults.standard.set(Date(), forKey: self.lastFetchKey)
|
||||||
|
SecureLogger.log("GeoRelayDirectory: refreshed \(parsed.count) relays from remote", category: SecureLogger.session, level: .info)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SecureLogger.log("GeoRelayDirectory: remote fetch failed; keeping local entries", category: SecureLogger.session, level: .warning)
|
||||||
}
|
}
|
||||||
// Try filesystem path (development/test environments)
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func persistCache(_ text: String) {
|
||||||
|
guard let url = cacheURL() else { return }
|
||||||
|
do {
|
||||||
|
try text.data(using: .utf8)?.write(to: url, options: .atomic)
|
||||||
|
} catch {
|
||||||
|
SecureLogger.log("GeoRelayDirectory: failed to write cache: \(error)", category: SecureLogger.session, level: .warning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Loading
|
||||||
|
private func loadLocalEntries() -> [Entry] {
|
||||||
|
// Prefer cached file if present
|
||||||
|
if let cache = self.cacheURL(),
|
||||||
|
let data = try? Data(contentsOf: cache),
|
||||||
|
let text = String(data: data, encoding: .utf8) {
|
||||||
|
let arr = parseCSV(text)
|
||||||
|
if !arr.isEmpty { return arr }
|
||||||
|
}
|
||||||
|
// Try bundled resource(s)
|
||||||
|
let bundleCandidates = [
|
||||||
|
Bundle.main.url(forResource: "nostr_relays", withExtension: "csv"),
|
||||||
|
Bundle.main.url(forResource: "online_relays_gps", withExtension: "csv"),
|
||||||
|
Bundle.main.url(forResource: "online_relays_gps", withExtension: "csv", subdirectory: "relays")
|
||||||
|
].compactMap { $0 }
|
||||||
|
for url in bundleCandidates {
|
||||||
|
if let data = try? Data(contentsOf: url), let text = String(data: data, encoding: .utf8) {
|
||||||
|
let arr = parseCSV(text)
|
||||||
|
if !arr.isEmpty { return arr }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Try filesystem path (development/test)
|
||||||
if let cwd = FileManager.default.currentDirectoryPath as String?,
|
if let cwd = FileManager.default.currentDirectoryPath as String?,
|
||||||
let data = try? Data(contentsOf: URL(fileURLWithPath: cwd).appendingPathComponent("relays/online_relays_gps.csv")),
|
let data = try? Data(contentsOf: URL(fileURLWithPath: cwd).appendingPathComponent("relays/online_relays_gps.csv")),
|
||||||
let text = String(data: data, encoding: .utf8) {
|
let text = String(data: data, encoding: .utf8) {
|
||||||
return parseCSV(text)
|
return parseCSV(text)
|
||||||
}
|
}
|
||||||
// No data available
|
SecureLogger.log("GeoRelayDirectory: no local CSV found; entries empty", category: SecureLogger.session, level: .warning)
|
||||||
SecureLogger.log("GeoRelayDirectory: relay CSV not found; falling back to default relay set", category: SecureLogger.session, level: .warning)
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +119,26 @@ struct GeoRelayDirectory {
|
|||||||
if idx == 0 && line.lowercased().contains("relay url") { continue }
|
if idx == 0 && line.lowercased().contains("relay url") { continue }
|
||||||
let parts = line.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) }
|
let parts = line.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) }
|
||||||
guard parts.count >= 3 else { continue }
|
guard parts.count >= 3 else { continue }
|
||||||
let host = parts[0].replacingOccurrences(of: "wss://", with: "")
|
var host = parts[0]
|
||||||
|
host = host.replacingOccurrences(of: "https://", with: "")
|
||||||
|
host = host.replacingOccurrences(of: "http://", with: "")
|
||||||
|
host = host.replacingOccurrences(of: "wss://", with: "")
|
||||||
|
host = host.replacingOccurrences(of: "ws://", with: "")
|
||||||
|
host = host.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||||
guard let lat = Double(parts[1]), let lon = Double(parts[2]) else { continue }
|
guard let lat = Double(parts[1]), let lon = Double(parts[2]) else { continue }
|
||||||
result.insert(Entry(host: host, lat: lat, lon: lon))
|
result.insert(Entry(host: host, lat: lat, lon: lon))
|
||||||
}
|
}
|
||||||
return Array(result)
|
return Array(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func cacheURL() -> URL? {
|
||||||
|
do {
|
||||||
|
let base = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||||
|
let dir = base.appendingPathComponent("bitchat", isDirectory: true)
|
||||||
|
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||||
|
return dir.appendingPathComponent(cacheFileName)
|
||||||
|
} catch { return nil }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Distance
|
// MARK: - Distance
|
||||||
|
|||||||
Reference in New Issue
Block a user