mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:25:19 +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 */; };
|
||||
38EDDC049FD56B1BB1F14C91 /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BA20BC0F123F1507C5C247 /* IdentityModels.swift */; };
|
||||
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 */; };
|
||||
501BC56B1A08C0327A09AAF1 /* NoiseEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.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 */; };
|
||||
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27328EE574221395B2B8E87 /* MockBluetoothMeshService.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 */; };
|
||||
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
@@ -249,6 +247,8 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -308,7 +308,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F82C5FC8433F4064F079D1F /* bitchat */,
|
||||
E0A1B2C3D4E5F6012345678A /* online_relays_gps.csv */,
|
||||
E0A1B2C3D4E5F6012345678A /* relays/online_relays_gps.csv */,
|
||||
A2E8C336FA1ADBEC03261DFD /* bitchatShareExtension */,
|
||||
C3D98EB3E1B455E321F519F4 /* bitchatTests */,
|
||||
9F37F9F2C353B58AC809E93B /* Products */,
|
||||
@@ -328,8 +328,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */,
|
||||
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */,
|
||||
527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */,
|
||||
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */,
|
||||
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */,
|
||||
EA706D8E5097785414646A8E /* Info.plist */,
|
||||
95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */,
|
||||
@@ -526,7 +526,7 @@
|
||||
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */,
|
||||
2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */,
|
||||
78595178957244CBDF7E79B6 /* NostrRelayManager.swift */,
|
||||
E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */,
|
||||
E0A1B2C3D4E5F60123456789 /* GeoRelayDirectory.swift */,
|
||||
);
|
||||
path = Nostr;
|
||||
sourceTree = "<group>";
|
||||
@@ -691,7 +691,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */,
|
||||
E0A1B2C3D4E5F6012345678D /* online_relays_gps.csv in Resources */,
|
||||
E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -701,7 +701,7 @@
|
||||
files = (
|
||||
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */,
|
||||
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */,
|
||||
E0A1B2C3D4E5F6012345678E /* online_relays_gps.csv in Resources */,
|
||||
E0A1B2C3D4E5F6012345678E /* relays/online_relays_gps.csv in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -766,7 +766,7 @@
|
||||
D111988977C3BC246AB27FA4 /* SecureLogger.swift in Sources */,
|
||||
8DE687D2EB5EB120868DBFB5 /* BLEService.swift in Sources */,
|
||||
AA11BB22CC33DD44EE55FF66 /* MessageTextHelpers.swift in Sources */,
|
||||
E0A1B2C3D4E5F6012345678B /* GeoRelayDirectory.swift in Sources */,
|
||||
E0A1B2C3D4E5F6012345678B /* GeoRelayDirectory.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -820,7 +820,7 @@
|
||||
EC5241969D2550B97629EBD0 /* SecureLogger.swift in Sources */,
|
||||
C165DD35BB8E9C327A3C2DA4 /* BLEService.swift in Sources */,
|
||||
AA11BB22CC33DD44EE55FF67 /* MessageTextHelpers.swift in Sources */,
|
||||
E0A1B2C3D4E5F6012345678C /* GeoRelayDirectory.swift in Sources */,
|
||||
E0A1B2C3D4E5F6012345678C /* GeoRelayDirectory.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,8 @@ struct BitchatApp: App {
|
||||
|
||||
init() {
|
||||
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
|
||||
// Warm up georelay directory and refresh if stale (once/day)
|
||||
GeoRelayDirectory.shared.prefetchIfNeeded()
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
/// Directory of online Nostr relays with approximate GPS locations, used for geohash routing.
|
||||
struct GeoRelayDirectory {
|
||||
@MainActor
|
||||
final class GeoRelayDirectory {
|
||||
struct Entry: Hashable {
|
||||
let host: String
|
||||
let lat: Double
|
||||
@@ -9,10 +10,17 @@ struct 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() {
|
||||
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.
|
||||
@@ -32,23 +40,72 @@ struct GeoRelayDirectory {
|
||||
return sorted.map { "wss://\($0.host)" }
|
||||
}
|
||||
|
||||
// MARK: - Loading
|
||||
private static func loadEntries() -> [Entry] {
|
||||
// Try bundled resource first
|
||||
if let url = Bundle.main.url(forResource: "online_relays_gps", withExtension: "csv") ??
|
||||
Bundle.main.url(forResource: "online_relays_gps", withExtension: "csv", subdirectory: "relays"),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let text = String(data: data, encoding: .utf8) {
|
||||
return parseCSV(text)
|
||||
// MARK: - Remote Fetch
|
||||
func prefetchIfNeeded() {
|
||||
let now = Date()
|
||||
let last = UserDefaults.standard.object(forKey: lastFetchKey) as? Date ?? .distantPast
|
||||
guard now.timeIntervalSince(last) >= fetchInterval else { return }
|
||||
fetchRemote()
|
||||
}
|
||||
|
||||
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?,
|
||||
let data = try? Data(contentsOf: URL(fileURLWithPath: cwd).appendingPathComponent("relays/online_relays_gps.csv")),
|
||||
let text = String(data: data, encoding: .utf8) {
|
||||
return parseCSV(text)
|
||||
}
|
||||
// No data available
|
||||
SecureLogger.log("GeoRelayDirectory: relay CSV not found; falling back to default relay set", category: SecureLogger.session, level: .warning)
|
||||
SecureLogger.log("GeoRelayDirectory: no local CSV found; entries empty", category: SecureLogger.session, level: .warning)
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -62,12 +119,26 @@ struct GeoRelayDirectory {
|
||||
if idx == 0 && line.lowercased().contains("relay url") { continue }
|
||||
let parts = line.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) }
|
||||
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 }
|
||||
result.insert(Entry(host: host, lat: lat, lon: lon))
|
||||
}
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user