session-ios/Session/Utilities/IP2Country.swift
Morgan Pretty 5b5f4a4e88 Various tweaks and fixes
Fixed an issue where the GlobalSearch push animation could be jittery
Fixed a crash which could occur when returning from the background on certain screens
Removed the keyboard dismiss animation when pushing from global search to a conversation (apparently this is how iMessage avoids the animation bug...)
Updated to the latest version of GRDB
Updated the Atomic wrapper to use the ReadWrite lock for less blocking behaviours
Updated the audio attachment icon to be consistent with Android & Desktop
Updated the QuoteView to omit the "author" if we don't have their name and the quote can't be found
2023-05-16 09:38:14 +10:00

84 lines
3.5 KiB
Swift

import Foundation
import GRDB
import SessionSnodeKit
final class IP2Country {
var countryNamesCache: Atomic<[String: String]> = Atomic([:])
private static let workQueue = DispatchQueue(label: "IP2Country.workQueue", qos: .utility) // It's important that this is a serial queue
static var isInitialized = false
// MARK: Tables
/// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains
/// the **lower** bound of an IP range and the "registered_country_geoname_id" column contains the ID of the country corresponding
/// to that range. We look up an IP by finding the first index in the network column where the value is greater than the IP we're looking
/// up (converted to an integer). The IP we're looking up must then be in the range **before** that range.
private lazy var ipv4Table: [String:[Int]] = {
let url = Bundle.main.url(forResource: "GeoLite2-Country-Blocks-IPv4", withExtension: nil)!
let data = try! Data(contentsOf: url)
return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String:[Int]]
}()
private lazy var countryNamesTable: [String:[String]] = {
let url = Bundle.main.url(forResource: "GeoLite2-Country-Locations-English", withExtension: nil)!
let data = try! Data(contentsOf: url)
return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String:[String]]
}()
// MARK: Lifecycle
static let shared = IP2Country()
private init() {
NotificationCenter.default.addObserver(self, selector: #selector(populateCacheIfNeededAsync), name: .pathsBuilt, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Implementation
@discardableResult private func cacheCountry(for ip: String, inCache cache: inout [String: String]) -> String {
if let result: String = cache[ip] { return result }
let ipAsInt: Int = IPv4.toInt(ip)
guard
let ipv4TableIndex = ipv4Table["network"]?.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }),
let countryID: Int = ipv4Table["registered_country_geoname_id"]?[ipv4TableIndex],
let countryNamesTableIndex = countryNamesTable["geoname_id"]?.firstIndex(of: String(countryID)),
let result: String = countryNamesTable["country_name"]?[countryNamesTableIndex]
else {
return "Unknown Country" // Relies on the array being sorted
}
cache[ip] = result
return result
}
@objc func populateCacheIfNeededAsync() {
// This has to be sync since the `countryNamesCache` dict doesn't like async access
IP2Country.workQueue.sync { [weak self] in
_ = self?.populateCacheIfNeeded()
}
}
func populateCacheIfNeeded() -> Bool {
guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else { return false }
countryNamesCache.mutate { [weak self] cache in
pathToDisplay.forEach { snode in
self?.cacheCountry(for: snode.ip, inCache: &cache) // Preload if needed
}
}
DispatchQueue.main.async {
IP2Country.isInitialized = true
NotificationCenter.default.post(name: .onionRequestPathCountriesLoaded, object: nil)
}
SNLog("Finished preloading onion request path countries.")
return true
}
}