From 40912c511225ebb7ef7309384146d956a01207c3 Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Sun, 15 Sep 2024 08:31:46 -0500 Subject: [PATCH] added support for NIP-62 Request to Vanish events #80 --- CHANGELOG.md | 1 + Nos.xcodeproj/project.pbxproj | 12 ++ Nos/Models/EventKind.swift | 3 + Nos/Models/JSONEvent+Kinds.swift | 27 +++ Nos/Service/CurrentUser+PublishEvents.swift | 225 ++++++++++++++++++++ Nos/Service/CurrentUser.swift | 202 +----------------- NosTests/Models/JSONEventTests.swift | 37 ++++ README.md | 1 + 8 files changed, 307 insertions(+), 201 deletions(-) create mode 100644 Nos/Models/JSONEvent+Kinds.swift create mode 100644 Nos/Service/CurrentUser+PublishEvents.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 631f878af..b5b80c613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where toggles in the settings screen were white instead of green when toggled on. [#1251](https://github.com/planetary-social/nos/issues/1251) - Added routing to profile when tapping on follow notification. [#1447](https://github.com/planetary-social/nos/issues/1447) - Localized follows notifications. [#1446](https://github.com/planetary-social/nos/issues/1446) +- Added support for NIP-62 Request to Vanish events. [#80](https://github.com/planetary-social/nos/issues/80) ### Internal Changes - Use NIP-92 media metadata to display media in the proper orientation. Currently behind the “Enable new media display” feature flag. [#1172](https://github.com/planetary-social/nos/issues/1172) diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 73caa9fbc..33fd503fe 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -125,6 +125,10 @@ 3FFB1D9729A6BBEC002A755D /* Collection+SafeSubscript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFB1D9529A6BBEC002A755D /* Collection+SafeSubscript.swift */; }; 3FFB1D9C29A7DF9D002A755D /* StackedAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFB1D9B29A7DF9D002A755D /* StackedAvatarsView.swift */; }; 3FFF3BD029A9645F00DD0B72 /* AuthorReference+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F43C47529A9625700E896A0 /* AuthorReference+CoreDataClass.swift */; }; + 50089A012C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */; }; + 50089A022C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */; }; + 50089A0C2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */; }; + 50089A0D2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */; }; 5044546E2C90726A00251A7E /* Event+Fetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5044546D2C90726A00251A7E /* Event+Fetching.swift */; }; 504454702C90728500251A7E /* Event+Hydration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5044546F2C90728500251A7E /* Event+Hydration.swift */; }; 504454712C90728E00251A7E /* Event+Fetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5044546D2C90726A00251A7E /* Event+Fetching.swift */; }; @@ -641,6 +645,8 @@ 3FFB1D9229A6BBCE002A755D /* EventReference+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EventReference+CoreDataClass.swift"; sourceTree = ""; }; 3FFB1D9529A6BBEC002A755D /* Collection+SafeSubscript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+SafeSubscript.swift"; sourceTree = ""; }; 3FFB1D9B29A7DF9D002A755D /* StackedAvatarsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackedAvatarsView.swift; sourceTree = ""; }; + 50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEvent+Kinds.swift"; sourceTree = ""; }; + 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrentUser+PublishEvents.swift"; sourceTree = ""; }; 5044546D2C90726A00251A7E /* Event+Fetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+Fetching.swift"; sourceTree = ""; }; 5044546F2C90728500251A7E /* Event+Hydration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+Hydration.swift"; sourceTree = ""; }; 5045540C2C81E10C0044ECAE /* EditableAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableAvatarView.swift; sourceTree = ""; }; @@ -1518,6 +1524,7 @@ C9ADB13C29929B540075E7F8 /* Bech32.swift */, C9B71DC12A9003670031ED9F /* CrashReporting.swift */, A34E439829A522F20057AFCB /* CurrentUser.swift */, + 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */, 034EBDB92C24895E006BA35A /* CurrentUserError.swift */, C9C097242C13537900F78EC3 /* DatabaseCleaner.swift */, C98298322ADD7F9A0096C5B5 /* DeepLinkService.swift */, @@ -1735,6 +1742,7 @@ 0365CD862C4016A200622A1A /* EventKind.swift */, C9EE3E622A053910008A7491 /* ExpirationTimeOption.swift */, C93CA0C229AE3A1E00921183 /* JSONEvent.swift */, + 50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */, 5B503F612A291A1A0098805A /* JSONRelayMetadata.swift */, C9F84C26298DC98800C6714D /* KeyPair.swift */, C930055E2A6AF8320098CA9E /* LoadingContent.swift */, @@ -2139,6 +2147,7 @@ C993148D2C5BD8FC00224BA6 /* NoteEditorController.swift in Sources */, 0350F12D2C0A7EF20024CC15 /* FeatureFlags.swift in Sources */, 3FFB1D9C29A7DF9D002A755D /* StackedAvatarsView.swift in Sources */, + 50089A0C2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */, C97A1C8E29E58EC7009D9E8D /* NSManagedObjectContext+Nos.swift in Sources */, 5BBA5E9C2BAE052F00D57D76 /* NiceWorkSheet.swift in Sources */, C9B678DE29EEC35B00303F33 /* Foundation+Sendable.swift in Sources */, @@ -2194,6 +2203,7 @@ C9F84C23298DC7B900C6714D /* SettingsView.swift in Sources */, 03C8B4962C6D065900A07CCD /* ImageViewer.swift in Sources */, 5B79F6092B98AC33002DA9BE /* ClaimYourUniqueIdentitySheet.swift in Sources */, + 50089A012C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */, C973AB652A323167002AED16 /* EventReference+CoreDataProperties.swift in Sources */, C973AB632A323167002AED16 /* Relay+CoreDataProperties.swift in Sources */, C94FE9F729DB259300019CD3 /* Text+Gradient.swift in Sources */, @@ -2443,6 +2453,7 @@ 035729CA2BE4173E005FEE85 /* PreviewData.swift in Sources */, 037975D12C0E341500ADDF37 /* MockFeatureFlags.swift in Sources */, C92E7F682C4EFF3D00B80638 /* WebSocketErrorEvent.swift in Sources */, + 50089A0D2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */, 504454722C90729100251A7E /* Event+Hydration.swift in Sources */, 5BD08BB22A38E96F00BB926C /* JSONRelayMetadata.swift in Sources */, C936B45A2A4C7B7C00DF1EB9 /* Nos.xcdatamodeld in Sources */, @@ -2508,6 +2519,7 @@ C9B678DC29EEBF3B00303F33 /* DependencyInjection.swift in Sources */, C9F0BB6D29A503D9000547FC /* Int+Bool.swift in Sources */, 0314D5AD2C7D31060002E7F4 /* MediaService.swift in Sources */, + 50089A022C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */, C9C097232C13534800F78EC3 /* DatabaseCleanerTests.swift in Sources */, 03618C962C826D5E00BCBC55 /* CompactNoteView.swift in Sources */, DC08FF812A7969C5009F87D1 /* UIDevice+Simulator.swift in Sources */, diff --git a/Nos/Models/EventKind.swift b/Nos/Models/EventKind.swift index d2a55ebcd..b5e21eeae 100644 --- a/Nos/Models/EventKind.swift +++ b/Nos/Models/EventKind.swift @@ -33,6 +33,9 @@ public enum EventKind: Int64, CaseIterable, Hashable { /// Channel Message case channelMessage = 42 + /// Request to Vanish + case requestToVanish = 62 + /// Gift Wrap case giftWrap = 1059 diff --git a/Nos/Models/JSONEvent+Kinds.swift b/Nos/Models/JSONEvent+Kinds.swift new file mode 100644 index 000000000..e11ebc3dc --- /dev/null +++ b/Nos/Models/JSONEvent+Kinds.swift @@ -0,0 +1,27 @@ +import Foundation + +extension JSONEvent { + + /// An event that represents the user's request for all of their published notes to be removed from relays. + /// - Parameters: + /// - pubKey: The public key of the user making the request. + /// - relays: The relays to request removal from. Note: A nil or empty relay array will be interpreted to mean + /// that the user seeks removal from all relays. + /// - reason: The reason the user wishes to have their content removed. Optional. + /// - Returns: The ``JSONEvent`` representing the request. + static func requestToVanish(pubKey: String, relays: [URL]? = nil, reason: String? = nil) -> JSONEvent { + let tags: [[String]] + if let relays, !relays.isEmpty { + tags = relays.map { ["relay", $0.absoluteString] } + } else { + tags = [["relay", "ALL_RELAYS"]] + } + + return JSONEvent( + pubKey: pubKey, + kind: .requestToVanish, + tags: tags, + content: reason ?? "" + ) + } +} diff --git a/Nos/Service/CurrentUser+PublishEvents.swift b/Nos/Service/CurrentUser+PublishEvents.swift new file mode 100644 index 000000000..9bde83db6 --- /dev/null +++ b/Nos/Service/CurrentUser+PublishEvents.swift @@ -0,0 +1,225 @@ +import Foundation +import Logger + +extension CurrentUser { + + /// Builds a dictionary to be used as content when publishing a kind 0 + /// event. + private func buildMetadataJSONObject(author: Author) -> [String: String] { + var metaEvent = MetadataEventJSON( + displayName: author.displayName, + name: author.name, + nip05: author.nip05, + uns: author.uns, + about: author.about, + website: author.website, + picture: author.profilePhotoURL?.absoluteString + ).dictionary + if let rawData = author.rawMetadata { + // Tack on any unsupported fields back onto the dictionary before + // publish. + do { + let rawJson = try JSONSerialization.jsonObject(with: rawData) + if let rawDictionary = rawJson as? [String: AnyObject] { + for key in rawDictionary.keys { + guard metaEvent[key] == nil else { + continue + } + if let rawValue = rawDictionary[key] as? String { + metaEvent[key] = rawValue + Log.debug("Added \(key) : \(rawValue)") + } + } + } + } catch { + Log.debug("Couldn't parse a JSON from the user raw metadata") + // Continue with the metaEvent object we built previously + } + } + return metaEvent + } + + @MainActor func publishMetadata() async throws { + guard let pubKey = publicKeyHex else { + Log.debug("Error: no publicKeyHex") + throw CurrentUserError.authorNotFound + } + guard let pair = keyPair else { + Log.debug("Error: no keyPair") + throw CurrentUserError.authorNotFound + } + guard let context = viewContext else { + Log.debug("Error: no context") + throw CurrentUserError.authorNotFound + } + guard let author = try Author.find(by: pubKey, context: context) else { + Log.debug("Error: no author in DB") + throw CurrentUserError.authorNotFound + } + + self.author = author + + let jsonObject = buildMetadataJSONObject(author: author) + let data = try JSONSerialization.data(withJSONObject: jsonObject) + let content = String(decoding: data, as: UTF8.self) + + let jsonEvent = JSONEvent( + pubKey: pubKey, + kind: .metaData, + tags: [], + content: content + ) + + do { + try await relayService.publishToAll( + event: jsonEvent, + signingKey: pair, + context: viewContext + ) + } catch { + Log.error(error.localizedDescription) + throw CurrentUserError.errorWhilePublishingToRelays + } + } + + @MainActor func publishMuteList(keys: [String]) async { + guard let pubKey = publicKeyHex else { + Log.debug("Error: no pubKey") + return + } + + let jsonEvent = JSONEvent(pubKey: pubKey, kind: .mute, tags: keys.pTags, content: "") + + if let pair = keyPair { + do { + try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext) + } catch { + Log.debug("Failed to update mute list \(error.localizedDescription)") + } + } + } + + @MainActor func publishDelete(for identifiers: [String], reason: String = "") async { + guard let pubKey = publicKeyHex else { + Log.debug("Error: no pubKey") + return + } + + let tags = identifiers.eTags + let jsonEvent = JSONEvent(pubKey: pubKey, kind: .delete, tags: tags, content: reason) + + if let pair = keyPair { + do { + try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext) + } catch { + Log.debug("Failed to delete events \(error.localizedDescription)") + } + } + } + + @MainActor func publishContactList(tags: [[String]]) async { + guard let pubKey = publicKeyHex else { + Log.debug("Error: no pubKey") + return + } + + guard let relays = author?.relays else { + Log.debug("Error: No relay service") + return + } + + var relayString = "{" + for relay in relays { + if let address = relay.address { + relayString += "\"\(address)\":{\"write\":true,\"read\":true}," + } + } + relayString.removeLast() + relayString += "}" + + let jsonEvent = JSONEvent(pubKey: pubKey, kind: .contactList, tags: tags, content: relayString) + + if let pair = keyPair { + do { + try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext) + } catch { + Log.debug("failed to update Follows \(error.localizedDescription)") + } + } + } + + /// Follow by public hex key + @MainActor func follow(author toFollow: Author) async throws { + guard let followKey = toFollow.hexadecimalPublicKey else { + Log.debug("Error: followKey is nil") + return + } + + Log.debug("Following \(followKey)") + + var followKeys = await Array(socialGraph.followedKeys) + followKeys.append(followKey) + + // Update author to add the new follow + if let followedAuthor = try? Author.find(by: followKey, context: viewContext), let currentUser = author { + let follow = try Follow.findOrCreate( + source: currentUser, + destination: followedAuthor, + context: viewContext + ) + + // Add to the current user's follows + currentUser.follows.insert(follow) + } + + try viewContext.save() + await publishContactList(tags: followKeys.pTags) + } + + /// Unfollow by public hex key + @MainActor func unfollow(author toUnfollow: Author) async throws { + guard let unfollowedKey = toUnfollow.hexadecimalPublicKey else { + Log.debug("Error: unfollowedKey is nil") + return + } + + Log.debug("Unfollowing \(unfollowedKey)") + + let stillFollowingKeys = await Array(socialGraph.followedKeys) + .filter { $0 != unfollowedKey } + + // Update author to only follow those still following + if let unfollowedAuthor = try? Author.find(by: unfollowedKey, context: viewContext), let currentUser = author { + // Remove from the current user's follows + let unfollows = Follow.follows(source: currentUser, destination: unfollowedAuthor, context: viewContext) + + for unfollow in unfollows { + // Remove current user's follows + currentUser.follows.remove(unfollow) + } + } + + try viewContext.save() + await publishContactList(tags: stillFollowingKeys.pTags) + } + + @MainActor func publishRequestToVanish(to relays: [URL]? = nil, reason: String? = nil) async throws { + guard let keyPair else { + Log.debug("Error: no key pair") + return + } + + let pubKey = keyPair.publicKey.hex + let jsonEvent = JSONEvent.requestToVanish(pubKey: pubKey, relays: relays, reason: reason) + + do { + if let relays, !relays.isEmpty { + try await relayService.publish(event: jsonEvent, to: relays, signingKey: keyPair, context: viewContext) + } else { + try await relayService.publishToAll(event: jsonEvent, signingKey: keyPair, context: viewContext) + } + } catch { + Log.debug("Failed to publish request to vanish \(error.localizedDescription)") + } + } +} diff --git a/Nos/Service/CurrentUser.swift b/Nos/Service/CurrentUser.swift index ecdfb008a..92f6d0f94 100644 --- a/Nos/Service/CurrentUser.swift +++ b/Nos/Service/CurrentUser.swift @@ -10,7 +10,7 @@ import Dependencies @ObservationIgnored @Dependency(\.crashReporting) private var crashReporting @ObservationIgnored @Dependency(\.persistenceController) private var persistenceController @ObservationIgnored @Dependency(\.pushNotificationService) private var pushNotificationService - @ObservationIgnored @Dependency(\.relayService) private var relayService + @ObservationIgnored @Dependency(\.relayService) var relayService @ObservationIgnored @Dependency(\.keychain) private var keychain // TODO: it's time to cache this @@ -284,206 +284,6 @@ import Dependencies return followKeys.contains(key) } - /// Builds a dictionary to be used as content when publishing a kind 0 - /// event. - private func buildMetadataJSONObject(author: Author) -> [String: String] { - var metaEvent = MetadataEventJSON( - displayName: author.displayName, - name: author.name, - nip05: author.nip05, - uns: author.uns, - about: author.about, - website: author.website, - picture: author.profilePhotoURL?.absoluteString - ).dictionary - if let rawData = author.rawMetadata { - // Tack on any unsupported fields back onto the dictionary before - // publish. - do { - let rawJson = try JSONSerialization.jsonObject(with: rawData) - if let rawDictionary = rawJson as? [String: AnyObject] { - for key in rawDictionary.keys { - guard metaEvent[key] == nil else { - continue - } - if let rawValue = rawDictionary[key] as? String { - metaEvent[key] = rawValue - Log.debug("Added \(key) : \(rawValue)") - } - } - } - } catch { - Log.debug("Couldn't parse a JSON from the user raw metadata") - // Continue with the metaEvent object we built previously - } - } - return metaEvent - } - - @MainActor func publishMetadata() async throws { - guard let pubKey = publicKeyHex else { - Log.debug("Error: no publicKeyHex") - throw CurrentUserError.authorNotFound - } - guard let pair = keyPair else { - Log.debug("Error: no keyPair") - throw CurrentUserError.authorNotFound - } - guard let context = viewContext else { - Log.debug("Error: no context") - throw CurrentUserError.authorNotFound - } - guard let author = try Author.find(by: pubKey, context: context) else { - Log.debug("Error: no author in DB") - throw CurrentUserError.authorNotFound - } - - self.author = author - - let jsonObject = buildMetadataJSONObject(author: author) - let data = try JSONSerialization.data(withJSONObject: jsonObject) - let content = String(decoding: data, as: UTF8.self) - - let jsonEvent = JSONEvent( - pubKey: pubKey, - kind: .metaData, - tags: [], - content: content - ) - - do { - try await relayService.publishToAll( - event: jsonEvent, - signingKey: pair, - context: viewContext - ) - } catch { - Log.error(error.localizedDescription) - throw CurrentUserError.errorWhilePublishingToRelays - } - } - - @MainActor func publishMuteList(keys: [String]) async { - guard let pubKey = publicKeyHex else { - Log.debug("Error: no pubKey") - return - } - - let jsonEvent = JSONEvent(pubKey: pubKey, kind: .mute, tags: keys.pTags, content: "") - - if let pair = keyPair { - do { - try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext) - } catch { - Log.debug("Failed to update mute list \(error.localizedDescription)") - } - } - } - - @MainActor func publishDelete(for identifiers: [String], reason: String = "") async { - guard let pubKey = publicKeyHex else { - Log.debug("Error: no pubKey") - return - } - - let tags = identifiers.eTags - let jsonEvent = JSONEvent(pubKey: pubKey, kind: .delete, tags: tags, content: reason) - - if let pair = keyPair { - do { - try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext) - } catch { - Log.debug("Failed to delete events \(error.localizedDescription)") - } - } - } - - @MainActor func publishContactList(tags: [[String]]) async { - guard let pubKey = publicKeyHex else { - Log.debug("Error: no pubKey") - return - } - - guard let relays = author?.relays else { - Log.debug("Error: No relay service") - return - } - - var relayString = "{" - for relay in relays { - if let address = relay.address { - relayString += "\"\(address)\":{\"write\":true,\"read\":true}," - } - } - relayString.removeLast() - relayString += "}" - - let jsonEvent = JSONEvent(pubKey: pubKey, kind: .contactList, tags: tags, content: relayString) - - if let pair = keyPair { - do { - try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext) - } catch { - Log.debug("failed to update Follows \(error.localizedDescription)") - } - } - } - - /// Follow by public hex key - @MainActor func follow(author toFollow: Author) async throws { - guard let followKey = toFollow.hexadecimalPublicKey else { - Log.debug("Error: followKey is nil") - return - } - - Log.debug("Following \(followKey)") - - var followKeys = await Array(socialGraph.followedKeys) - followKeys.append(followKey) - - // Update author to add the new follow - if let followedAuthor = try? Author.find(by: followKey, context: viewContext), let currentUser = author { - let follow = try Follow.findOrCreate( - source: currentUser, - destination: followedAuthor, - context: viewContext - ) - - // Add to the current user's follows - currentUser.follows.insert(follow) - } - - try viewContext.save() - await publishContactList(tags: followKeys.pTags) - } - - /// Unfollow by public hex key - @MainActor func unfollow(author toUnfollow: Author) async throws { - guard let unfollowedKey = toUnfollow.hexadecimalPublicKey else { - Log.debug("Error: unfollowedKey is nil") - return - } - - Log.debug("Unfollowing \(unfollowedKey)") - - let stillFollowingKeys = await Array(socialGraph.followedKeys) - .filter { $0 != unfollowedKey } - - // Update author to only follow those still following - if let unfollowedAuthor = try? Author.find(by: unfollowedKey, context: viewContext), let currentUser = author { - // Remove from the current user's follows - let unfollows = Follow.follows(source: currentUser, destination: unfollowedAuthor, context: viewContext) - - for unfollow in unfollows { - // Remove current user's follows - currentUser.follows.remove(unfollow) - } - } - - try viewContext.save() - await publishContactList(tags: stillFollowingKeys.pTags) - } - // MARK: - NSFetchedResultsControllerDelegate @MainActor func controllerDidChangeContent(_ controller: NSFetchedResultsController) { diff --git a/NosTests/Models/JSONEventTests.swift b/NosTests/Models/JSONEventTests.swift index fb61d9293..b47d019b1 100644 --- a/NosTests/Models/JSONEventTests.swift +++ b/NosTests/Models/JSONEventTests.swift @@ -14,4 +14,41 @@ class JSONEventTests: XCTestCase { // Act & Assert XCTAssertEqual(subject.replaceableID, replaceableID) } + + func test_requestToVanish_fromSpecificRelays() { + let event = JSONEvent.requestToVanish( + pubKey: "", + relays: [ + URL(string: "wss://relay1.lol")!, + URL(string: "wss://relay2.lol")!, + URL(string: "wss://relay3.lol")! + ], + reason: "I'm done with this." + ) + + let expectedTags = [ + ["relay", "wss://relay1.lol"], + ["relay", "wss://relay2.lol"], + ["relay", "wss://relay3.lol"] + ] + + XCTAssertEqual(event.kind, 62) + XCTAssertEqual(event.tags, expectedTags) + XCTAssertEqual(event.content, "I'm done with this.") + } + + func test_requestToVanish_fromAllRelays() { + let event = JSONEvent.requestToVanish( + pubKey: "", + reason: "I'm done with this." + ) + + let expectedTags = [ + ["relay", "ALL_RELAYS"] + ] + + XCTAssertEqual(event.kind, 62) + XCTAssertEqual(event.tags, expectedTags) + XCTAssertEqual(event.content, "I'm done with this.") + } } diff --git a/README.md b/README.md index 176f0de47..3615d3d6b 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ NIPs, or Nostr Implementation Possibilities, are specifications that Nostr apps - [NIP-50: Search Capability](https://github.com/nostr-protocol/nips/blob/master/50.md) - we only support searching for profiles at the moment, not text. - [NIP-51: Lists](https://github.com/nostr-protocol/nips/blob/master/51.md) - only supporting the mute list right now. - [NIP-56: Reporting](https://github.com/nostr-protocol/nips/blob/master/56.md) +- [NIP-62: Request to Vanish](https://github.com/nostr-protocol/nips/blob/master/62.md) - [NIP-65: Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md) - [NIP-96: HTTP File Storage Integration](https://github.com/nostr-protocol/nips/blob/master/96.md) - [NIP-98: HTTP Auth](https://github.com/nostr-protocol/nips/blob/master/98.md)