From ef4aeb40e07e70e935ffb46a1582df3c74764e24 Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Sun, 9 Jul 2023 08:45:33 -0500 Subject: [PATCH] add RelayLog class Signed-off-by: Bryan Montz Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 4 + damus/Nostr/RelayLog.swift | 143 ++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 damus/Nostr/RelayLog.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index afb06c81..f0cd7ce7 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -295,6 +295,7 @@ 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; }; 5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */; }; 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; }; + 50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A60D132A28BEEE00186190 /* RelayLog.swift */; }; 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; }; 50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; }; 5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; }; @@ -765,6 +766,7 @@ 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = ""; }; 5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = ""; }; 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; }; + 50A60D132A28BEEE00186190 /* RelayLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayLog.swift; sourceTree = ""; }; 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = ""; }; 50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = ""; }; 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = ""; }; @@ -1132,6 +1134,7 @@ children = ( 501F8C5329FF5EE2001AFC1D /* CoreData */, 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */, + 50A60D132A28BEEE00186190 /* RelayLog.swift */, 4C75EFA527FF87A20006080F /* Nostr.swift */, 4C75EFAE28049D340006080F /* NostrFilter.swift */, 4C75EFB028049D510006080F /* NostrResponse.swift */, @@ -1980,6 +1983,7 @@ 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */, 4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */, 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */, + 50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */, 4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */, 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */, 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, diff --git a/damus/Nostr/RelayLog.swift b/damus/Nostr/RelayLog.swift new file mode 100644 index 00000000..035af723 --- /dev/null +++ b/damus/Nostr/RelayLog.swift @@ -0,0 +1,143 @@ +// +// RelayLog.swift +// damus +// +// Created by Bryan Montz on 6/1/23. +// + +import Combine +import Foundation +import UIKit + +/// Stores a running list of events and state changes related to a relay, so that users +/// will have information to help developers debug issues. +final class RelayLog: ObservableObject { + private static let line_limit = 250 + private let relay_url: URL? + private lazy var formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .medium + return formatter + }() + + private(set) var lines = [String]() + + private var notification_token: AnyCancellable? + + /// Creates a RelayLog + /// - 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) { + self.relay_url = relay_url + + setUp() + } + + private var log_files_directory: URL { + FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("RelayLogs", isDirectory: true) + } + + private var log_file_url: URL? { + guard let file_name = relay_url?.absoluteString.data(using: .utf8) else { + return nil + } + return log_files_directory.appendingPathComponent(file_name.base64EncodedString()) + } + + /// Sets up the log file and prepares to listen to app state changes + private func setUp() { + guard let log_file_url else { + return + } + + try? FileManager.default.createDirectory(at: log_files_directory, withIntermediateDirectories: false) + + if !FileManager.default.fileExists(atPath: log_file_url.path) { + // create the log file if it doesn't exist yet + FileManager.default.createFile(atPath: log_file_url.path, contents: nil) + } else { + // otherwise load it into memory + readFromDisk() + } + + let willResignPublisher = NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification) + let willTerminatePublisher = NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification) + notification_token = Publishers.Merge(willResignPublisher, willTerminatePublisher) + .sink { [weak self] _ in + self?.writeToDisk() + } + } + + /// The current contents of the log + var contents: String? { + guard !lines.isEmpty else { + return nil + } + return lines.joined(separator: "\n") + } + + /// Adds content to the log + /// - Parameter content: what to add to the log. The date and time are prepended to the content. + func add(_ content: String) { + let line = "\(formatter.string(from: .now)) - \(content)" + lines.insert(line, at: 0) + truncateLines() + + Task { + await publishChanges() + } + } + + /// Tells views that our log has been updated + @MainActor private func publishChanges() { + objectWillChange.send() + } + + private func truncateLines() { + lines = Array(lines.prefix(RelayLog.line_limit)) + } + + /// Reads the contents of the log file from disk into memory + private func readFromDisk() { + guard let log_file_url else { + return + } + + do { + let handle = try FileHandle(forReadingFrom: log_file_url) + let data = try handle.readToEnd() + try handle.close() + + guard let data, let content = String(data: data, encoding: .utf8) else { + return + } + + lines = content.components(separatedBy: "\n") + + truncateLines() + } catch { + print("⚠️ Warning: RelayLog failed to read from \(log_file_url)") + } + } + + /// Writes the contents of the lines in memory to disk + private func writeToDisk() { + guard let log_file_url, let relay_url, + !lines.isEmpty, + let content = lines.joined(separator: "\n").data(using: .utf8) else { + return + } + + do { + let handle = try FileHandle(forWritingTo: log_file_url) + + try handle.truncate(atOffset: 0) + try handle.write(contentsOf: content) + try handle.close() + } catch { + print("⚠️ Warning: RelayLog(\(relay_url)) failed to write to file: \(error)") + } + } +}