Fix relay URL trailing slash issues
This commit tries to replace all usage of `String` to represent relay URLs and use `RelayURL` which automatically converts strings to a canonical relay URL format that is more reliable and avoids issues related to trailing slashes. Test 1: Main issue fix ----------------------- PASS Device: iPhone 15 Simulator iOS: 17.4 Damus: This commit Steps: 1. Delete all connected relays 2. Add `wss://relay.damus.io/` (with the trailing slash) to the relay list 3. Try to post. Post should succeed. PASS 4. Try removing this newly added relay. Relay should be removed successfully. PASS Test 2: Persistent relay list after upgrade -------------------------------------------- PASS Device: iPhone 15 Simulator iOS: 17.4 Damus: 1.8 (1) `247f313b` + This commit Steps: 1. Downgrade to old version 2. Add some relays to the list, some without a trailing slash, some with 3. Upgrade to this commit 4. All relays added in step 2 should still be there, and ones with a trailing slash should have been corrected to remove the trailing slash Test 3: Miscellaneous regression tests -------------------------------------- Device: iPhone 15 Simulator iOS: 17.4 Damus: This commit Coverage: 1. Posting works 2. Search works 3. Relay connection status works 4. Adding relays work 5. Removing relays work 6. Adding relay with trailing slashes works (it fixes itself to remove the trailing slash) 7. Adding relays with different paths works (e.g. wss://yabu.me/v1 and wss://yabu.me/v2) 8. Adding duplicate relay (but with trailing slash) gets rejected as expected 9. Relay details page works. All items on that view loads correctly 10. Relay logs work 11. Getting follower counts and seeing follow lists on profiles still work 12. Relay list changes persist after app restart 13. Notifications view still work 14. Copying the user's pubkey and profile link works 15. Share note + copy link button still works 16. Connecting NWC wallet works 17. One-tap zaps work 18. Onboarding works 19. Unit tests all passing Closes: https://github.com/damus-io/damus/issues/2072 Changelog-Fixed: Fix bug that would cause connection issues with relays defined with a trailing slash URL, and an inability to delete them. Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
committed by
William Casarin
parent
b18941383f
commit
e951370a76
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: Relay) -> NostrEvent? {
|
||||
let tags: [[String]] = [["relay", relay.descriptor.url.id],["challenge", challenge_string]]
|
||||
let tags: [[String]] = [["relay", relay.descriptor.url.absoluteString],["challenge", challenge_string]]
|
||||
let event = NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 22242, tags: tags)
|
||||
return event
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import Foundation
|
||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
|
||||
var tags = zap_target_to_tags(target)
|
||||
var relay_tag = ["relays"]
|
||||
relay_tag.append(contentsOf: relays.map { $0.url.id })
|
||||
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
||||
tags.append(relay_tag)
|
||||
|
||||
|
||||
var kp = keypair
|
||||
|
||||
let now = UInt32(Date().timeIntervalSince1970)
|
||||
@@ -79,8 +79,8 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
|
||||
func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
|
||||
let rw_relay_info = RelayInfo(read: true, write: true)
|
||||
var relays: [String: RelayInfo] = [:]
|
||||
|
||||
var relays: [RelayURL: RelayInfo] = [:]
|
||||
|
||||
for relay in bootstrap_relays {
|
||||
relays[relay] = rw_relay_info
|
||||
}
|
||||
|
||||
@@ -118,9 +118,9 @@ class Relay: Identifiable {
|
||||
var is_broken: Bool {
|
||||
return (flags & RelayFlags.broken.rawValue) == RelayFlags.broken.rawValue
|
||||
}
|
||||
|
||||
var id: String {
|
||||
return get_relay_id(descriptor.url)
|
||||
|
||||
var id: RelayURL {
|
||||
return descriptor.url
|
||||
}
|
||||
|
||||
}
|
||||
@@ -128,15 +128,3 @@ class Relay: Identifiable {
|
||||
enum RelayError: Error {
|
||||
case RelayAlreadyExists
|
||||
}
|
||||
|
||||
func get_relay_id(_ url: RelayURL) -> String {
|
||||
let trimTrailingSlashes: (String) -> String = { url in
|
||||
var trimmedUrl = url
|
||||
while trimmedUrl.hasSuffix("/") {
|
||||
trimmedUrl.removeLast()
|
||||
}
|
||||
return trimmedUrl
|
||||
}
|
||||
|
||||
return trimTrailingSlashes(url.url.absoluteString)
|
||||
}
|
||||
|
||||
@@ -21,19 +21,19 @@ final class RelayConnection: ObservableObject {
|
||||
private(set) var last_connection_attempt: TimeInterval = 0
|
||||
private(set) var last_pong: Date? = nil
|
||||
private(set) var backoff: TimeInterval = 1.0
|
||||
private lazy var socket = WebSocket(url.url)
|
||||
private lazy var socket = WebSocket(relay_url.url)
|
||||
private var subscriptionToken: AnyCancellable?
|
||||
|
||||
|
||||
private var handleEvent: (NostrConnectionEvent) -> ()
|
||||
private var processEvent: (WebSocketEvent) -> ()
|
||||
private let url: RelayURL
|
||||
private let relay_url: RelayURL
|
||||
var log: RelayLog?
|
||||
|
||||
init(url: RelayURL,
|
||||
handleEvent: @escaping (NostrConnectionEvent) -> (),
|
||||
processEvent: @escaping (WebSocketEvent) -> ())
|
||||
{
|
||||
self.url = url
|
||||
self.relay_url = url
|
||||
self.handleEvent = handleEvent
|
||||
self.processEvent = processEvent
|
||||
}
|
||||
@@ -48,7 +48,7 @@ final class RelayConnection: ObservableObject {
|
||||
self.last_pong = .now
|
||||
self.log?.add("Successful ping")
|
||||
} else {
|
||||
print("pong failed, reconnecting \(self.url.id)")
|
||||
print("pong failed, reconnecting \(self.relay_url.id)")
|
||||
self.isConnected = false
|
||||
self.isConnecting = false
|
||||
self.reconnect_with_backoff()
|
||||
@@ -126,7 +126,7 @@ final class RelayConnection: ObservableObject {
|
||||
self.receive(message: message)
|
||||
case .disconnected(let closeCode, let reason):
|
||||
if closeCode != .normalClosure {
|
||||
print("⚠️ Warning: RelayConnection (\(self.url)) closed with code \(closeCode), reason: \(String(describing: reason))")
|
||||
print("⚠️ Warning: RelayConnection (\(self.relay_url)) closed with code \(closeCode), reason: \(String(describing: reason))")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.isConnected = false
|
||||
@@ -134,7 +134,7 @@ final class RelayConnection: ObservableObject {
|
||||
self.reconnect()
|
||||
}
|
||||
case .error(let error):
|
||||
print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)")
|
||||
print("⚠️ Warning: RelayConnection (\(self.relay_url)) error: \(error)")
|
||||
let nserr = error as NSError
|
||||
if nserr.domain == NSPOSIXErrorDomain && nserr.code == 57 {
|
||||
// ignore socket not connected?
|
||||
|
||||
@@ -13,7 +13,7 @@ import UIKit
|
||||
/// will have information to help developers debug issues.
|
||||
final class RelayLog: ObservableObject {
|
||||
private static let line_limit = 250
|
||||
private let relay_url: URL?
|
||||
private let relay_url: RelayURL?
|
||||
private lazy var formatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .short
|
||||
@@ -29,9 +29,9 @@ final class RelayLog: ObservableObject {
|
||||
/// - Parameter relay_url: the relay url the log represents. Pass nil for the url to create
|
||||
/// a RelayLog that does nothing. This is required to allow RelayLog to be used as a StateObject,
|
||||
/// because they cannot be Optional.
|
||||
init(_ relay_url: URL? = nil) {
|
||||
init(_ relay_url: RelayURL? = nil) {
|
||||
self.relay_url = relay_url
|
||||
|
||||
|
||||
setUp()
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,17 @@ import Network
|
||||
|
||||
struct RelayHandler {
|
||||
let sub_id: String
|
||||
let callback: (String, NostrConnectionEvent) -> ()
|
||||
let callback: (RelayURL, NostrConnectionEvent) -> ()
|
||||
}
|
||||
|
||||
struct QueuedRequest {
|
||||
let req: NostrRequestType
|
||||
let relay: String
|
||||
let relay: RelayURL
|
||||
let skip_ephemeral: Bool
|
||||
}
|
||||
|
||||
struct SeenEvent: Hashable {
|
||||
let relay_id: String
|
||||
let relay_id: RelayURL
|
||||
let evid: NoteId
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class RelayPool {
|
||||
var handlers: [RelayHandler] = []
|
||||
var request_queue: [QueuedRequest] = []
|
||||
var seen: Set<SeenEvent> = Set()
|
||||
var counts: [String: UInt64] = [:]
|
||||
var counts: [RelayURL: UInt64] = [:]
|
||||
var ndb: Ndb
|
||||
var keypair: Keypair?
|
||||
var message_received_function: (((String, RelayDescriptor)) -> Void)?
|
||||
@@ -93,8 +93,8 @@ class RelayPool {
|
||||
relay.connection.ping()
|
||||
}
|
||||
}
|
||||
|
||||
func register_handler(sub_id: String, handler: @escaping (String, NostrConnectionEvent) -> ()) {
|
||||
|
||||
func register_handler(sub_id: String, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) {
|
||||
for handler in handlers {
|
||||
// don't add duplicate handlers
|
||||
if handler.sub_id == sub_id {
|
||||
@@ -104,10 +104,10 @@ class RelayPool {
|
||||
self.handlers.append(RelayHandler(sub_id: sub_id, callback: handler))
|
||||
print("registering \(sub_id) handler, current: \(self.handlers.count)")
|
||||
}
|
||||
|
||||
func remove_relay(_ relay_id: String) {
|
||||
|
||||
func remove_relay(_ relay_id: RelayURL) {
|
||||
var i: Int = 0
|
||||
|
||||
|
||||
self.disconnect(to: [relay_id])
|
||||
|
||||
for relay in relays {
|
||||
@@ -120,14 +120,13 @@ class RelayPool {
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func add_relay(_ desc: RelayDescriptor) throws {
|
||||
let url = desc.url
|
||||
let relay_id = get_relay_id(url)
|
||||
let relay_id = desc.url
|
||||
if get_relay(relay_id) != nil {
|
||||
throw RelayError.RelayAlreadyExists
|
||||
}
|
||||
let conn = RelayConnection(url: url, handleEvent: { event in
|
||||
let conn = RelayConnection(url: desc.url, handleEvent: { event in
|
||||
self.handle_event(relay_id: relay_id, event: event)
|
||||
}, processEvent: { wsev in
|
||||
guard case .message(let msg) = wsev,
|
||||
@@ -140,11 +139,11 @@ class RelayPool {
|
||||
let relay = Relay(descriptor: desc, connection: conn)
|
||||
self.relays.append(relay)
|
||||
}
|
||||
|
||||
func setLog(_ log: RelayLog, for relay_id: String) {
|
||||
|
||||
func setLog(_ log: RelayLog, for relay_id: RelayURL) {
|
||||
// add the current network state to the log
|
||||
log.add("Network state: \(network_monitor.currentPath.status)")
|
||||
|
||||
|
||||
get_relay(relay_id)?.connection.log = log
|
||||
}
|
||||
|
||||
@@ -154,9 +153,9 @@ class RelayPool {
|
||||
let c = relay.connection
|
||||
|
||||
let is_connecting = c.isConnecting
|
||||
|
||||
|
||||
if is_connecting && (Date.now.timeIntervalSince1970 - c.last_connection_attempt) > 5 {
|
||||
print("stale connection detected (\(relay.descriptor.url.url.absoluteString)). retrying...")
|
||||
print("stale connection detected (\(relay.descriptor.url.absoluteString)). retrying...")
|
||||
relay.connection.reconnect()
|
||||
} else if relay.is_broken || is_connecting || c.isConnected {
|
||||
continue
|
||||
@@ -166,8 +165,8 @@ class RelayPool {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func reconnect(to: [String]? = nil) {
|
||||
|
||||
func reconnect(to: [RelayURL]? = nil) {
|
||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||
for relay in relays {
|
||||
// don't try to reconnect to broken relays
|
||||
@@ -175,38 +174,38 @@ class RelayPool {
|
||||
}
|
||||
}
|
||||
|
||||
func connect(to: [String]? = nil) {
|
||||
func connect(to: [RelayURL]? = nil) {
|
||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||
for relay in relays {
|
||||
relay.connection.connect()
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect(to: [String]? = nil) {
|
||||
func disconnect(to: [RelayURL]? = nil) {
|
||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||
for relay in relays {
|
||||
relay.connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
func unsubscribe(sub_id: String, to: [String]? = nil) {
|
||||
|
||||
func unsubscribe(sub_id: String, to: [RelayURL]? = nil) {
|
||||
if to == nil {
|
||||
self.remove_handler(sub_id: sub_id)
|
||||
}
|
||||
self.send(.unsubscribe(sub_id), to: to)
|
||||
}
|
||||
|
||||
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> (), to: [String]? = nil) {
|
||||
|
||||
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (RelayURL, NostrConnectionEvent) -> (), to: [RelayURL]? = nil) {
|
||||
register_handler(sub_id: sub_id, handler: handler)
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
||||
}
|
||||
|
||||
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [String]?, handler: @escaping (String, NostrConnectionEvent) -> ()) {
|
||||
|
||||
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) {
|
||||
register_handler(sub_id: sub_id, handler: handler)
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
||||
}
|
||||
|
||||
func count_queued(relay: String) -> Int {
|
||||
|
||||
func count_queued(relay: RelayURL) -> Int {
|
||||
var c = 0
|
||||
for request in request_queue {
|
||||
if request.relay == relay {
|
||||
@@ -216,8 +215,8 @@ class RelayPool {
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func queue_req(r: NostrRequestType, relay: String, skip_ephemeral: Bool) {
|
||||
|
||||
func queue_req(r: NostrRequestType, relay: RelayURL, skip_ephemeral: Bool) {
|
||||
let count = count_queued(relay: relay)
|
||||
guard count <= 10 else {
|
||||
print("can't queue, too many queued events for \(relay)")
|
||||
@@ -228,9 +227,9 @@ class RelayPool {
|
||||
request_queue.append(QueuedRequest(req: r, relay: relay, skip_ephemeral: skip_ephemeral))
|
||||
}
|
||||
|
||||
func send_raw(_ req: NostrRequestType, to: [String]? = nil, skip_ephemeral: Bool = true) {
|
||||
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
|
||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||
|
||||
|
||||
// send to local relay (nostrdb)
|
||||
switch req {
|
||||
case .typical(let r):
|
||||
@@ -264,21 +263,21 @@ class RelayPool {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func send(_ req: NostrRequest, to: [String]? = nil, skip_ephemeral: Bool = true) {
|
||||
|
||||
func send(_ req: NostrRequest, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
|
||||
send_raw(.typical(req), to: to, skip_ephemeral: skip_ephemeral)
|
||||
}
|
||||
|
||||
func get_relays(_ ids: [String]) -> [Relay] {
|
||||
|
||||
func get_relays(_ ids: [RelayURL]) -> [Relay] {
|
||||
// don't include ephemeral relays in the default list to query
|
||||
relays.filter { ids.contains($0.id) }
|
||||
}
|
||||
|
||||
func get_relay(_ id: String) -> Relay? {
|
||||
|
||||
func get_relay(_ id: RelayURL) -> Relay? {
|
||||
relays.first(where: { $0.id == id })
|
||||
}
|
||||
|
||||
func run_queue(_ relay_id: String) {
|
||||
|
||||
func run_queue(_ relay_id: RelayURL) {
|
||||
self.request_queue = request_queue.reduce(into: Array<QueuedRequest>()) { (q, req) in
|
||||
guard req.relay == relay_id else {
|
||||
q.append(req)
|
||||
@@ -289,8 +288,8 @@ class RelayPool {
|
||||
self.send_raw(req.req, to: [relay_id], skip_ephemeral: false)
|
||||
}
|
||||
}
|
||||
|
||||
func record_seen(relay_id: String, event: NostrConnectionEvent) {
|
||||
|
||||
func record_seen(relay_id: RelayURL, event: NostrConnectionEvent) {
|
||||
if case .nostr_event(let ev) = event {
|
||||
if case .event(_, let nev) = ev {
|
||||
let k = SeenEvent(relay_id: relay_id, evid: nev.id)
|
||||
@@ -305,10 +304,10 @@ class RelayPool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handle_event(relay_id: String, event: NostrConnectionEvent) {
|
||||
|
||||
func handle_event(relay_id: RelayURL, event: NostrConnectionEvent) {
|
||||
record_seen(relay_id: relay_id, event: event)
|
||||
|
||||
|
||||
// run req queue when we reconnect
|
||||
if case .ws_event(let ws) = event {
|
||||
if case .connected = ws {
|
||||
@@ -349,10 +348,7 @@ class RelayPool {
|
||||
}
|
||||
}
|
||||
|
||||
func add_rw_relay(_ pool: RelayPool, _ url: String) {
|
||||
guard let url = RelayURL(url) else {
|
||||
return
|
||||
}
|
||||
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) {
|
||||
try? pool.add_relay(RelayDescriptor(url: url, info: .rw))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,18 +7,31 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct RelayURL: Hashable, Equatable, Codable, CodingKeyRepresentable {
|
||||
public struct RelayURL: Hashable, Equatable, Codable, CodingKeyRepresentable, Identifiable, Comparable, CustomStringConvertible {
|
||||
private(set) var url: URL
|
||||
|
||||
var id: String {
|
||||
|
||||
public var id: URL {
|
||||
return url
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return self.absoluteString
|
||||
}
|
||||
|
||||
public var absoluteString: String {
|
||||
return url.absoluteString
|
||||
}
|
||||
|
||||
|
||||
init?(_ str: String) {
|
||||
guard let url = URL(string: str) else {
|
||||
var trimmed_url_str = str
|
||||
while trimmed_url_str.hasSuffix("/") {
|
||||
trimmed_url_str.removeLast()
|
||||
}
|
||||
|
||||
guard let url = URL(string: trimmed_url_str) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
guard let scheme = url.scheme else {
|
||||
return nil
|
||||
}
|
||||
@@ -67,7 +80,12 @@ public struct RelayURL: Hashable, Equatable, Codable, CodingKeyRepresentable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(self.url)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Comparable
|
||||
public static func < (lhs: RelayURL, rhs: RelayURL) -> Bool {
|
||||
return lhs.url.absoluteString < rhs.url.absoluteString
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct StringKey: CodingKey {
|
||||
|
||||
Reference in New Issue
Block a user