Skip to content

Commit 3bd4f82

Browse files
authored
Merge pull request #1507 from planetary-social/bdm/80
added support for NIP-62 Request to Vanish events #80
2 parents 48cd898 + c3118df commit 3bd4f82

File tree

8 files changed

+307
-201
lines changed

8 files changed

+307
-201
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Localized follows notifications. [#1446](https://github.com/planetary-social/nos/issues/1446)
2424
- Fixed alert when uploading big files suggesting users pay for nostr.build. [#1321](https://github.com/planetary-social/nos/issues/1321)
2525
- Fixed issue where push notifications were not re-registered after account change. [#1501](https://github.com/planetary-social/nos/issues/1501)
26+
- Added support for NIP-62 Request to Vanish events. [#80](https://github.com/planetary-social/nos/issues/80)
2627

2728
### Internal Changes
2829
- 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)

Nos.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@
137137
3FFB1D9729A6BBEC002A755D /* Collection+SafeSubscript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFB1D9529A6BBEC002A755D /* Collection+SafeSubscript.swift */; };
138138
3FFB1D9C29A7DF9D002A755D /* StackedAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFB1D9B29A7DF9D002A755D /* StackedAvatarsView.swift */; };
139139
3FFF3BD029A9645F00DD0B72 /* AuthorReference+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F43C47529A9625700E896A0 /* AuthorReference+CoreDataClass.swift */; };
140+
50089A012C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */; };
141+
50089A022C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */; };
142+
50089A0C2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */; };
143+
50089A0D2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */; };
140144
500899F32C95C1F900834588 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; };
141145
502B6C3D2C9462A400446316 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; };
142146
50089A172C98678600834588 /* View+ListRowGradientBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A162C98678600834588 /* View+ListRowGradientBackground.swift */; };
@@ -663,6 +667,8 @@
663667
3FFB1D9229A6BBCE002A755D /* EventReference+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EventReference+CoreDataClass.swift"; sourceTree = "<group>"; };
664668
3FFB1D9529A6BBEC002A755D /* Collection+SafeSubscript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+SafeSubscript.swift"; sourceTree = "<group>"; };
665669
3FFB1D9B29A7DF9D002A755D /* StackedAvatarsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackedAvatarsView.swift; sourceTree = "<group>"; };
670+
50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEvent+Kinds.swift"; sourceTree = "<group>"; };
671+
50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrentUser+PublishEvents.swift"; sourceTree = "<group>"; };
666672
502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationRegistrar.swift; sourceTree = "<group>"; };
667673
50089A162C98678600834588 /* View+ListRowGradientBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ListRowGradientBackground.swift"; sourceTree = "<group>"; };
668674
5044546D2C90726A00251A7E /* Event+Fetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+Fetching.swift"; sourceTree = "<group>"; };
@@ -1582,6 +1588,7 @@
15821588
C9ADB13C29929B540075E7F8 /* Bech32.swift */,
15831589
C9B71DC12A9003670031ED9F /* CrashReporting.swift */,
15841590
A34E439829A522F20057AFCB /* CurrentUser.swift */,
1591+
50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */,
15851592
034EBDB92C24895E006BA35A /* CurrentUserError.swift */,
15861593
C9C097242C13537900F78EC3 /* DatabaseCleaner.swift */,
15871594
C98298322ADD7F9A0096C5B5 /* DeepLinkService.swift */,
@@ -1801,6 +1808,7 @@
18011808
0365CD862C4016A200622A1A /* EventKind.swift */,
18021809
C9EE3E622A053910008A7491 /* ExpirationTimeOption.swift */,
18031810
C93CA0C229AE3A1E00921183 /* JSONEvent.swift */,
1811+
50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */,
18041812
5B503F612A291A1A0098805A /* JSONRelayMetadata.swift */,
18051813
C9F84C26298DC98800C6714D /* KeyPair.swift */,
18061814
C930055E2A6AF8320098CA9E /* LoadingContent.swift */,
@@ -2210,6 +2218,7 @@
22102218
C993148D2C5BD8FC00224BA6 /* NoteEditorController.swift in Sources */,
22112219
0350F12D2C0A7EF20024CC15 /* FeatureFlags.swift in Sources */,
22122220
3FFB1D9C29A7DF9D002A755D /* StackedAvatarsView.swift in Sources */,
2221+
50089A0C2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */,
22132222
C97A1C8E29E58EC7009D9E8D /* NSManagedObjectContext+Nos.swift in Sources */,
22142223
5BBA5E9C2BAE052F00D57D76 /* NiceWorkSheet.swift in Sources */,
22152224
C9B678DE29EEC35B00303F33 /* Foundation+Sendable.swift in Sources */,
@@ -2267,6 +2276,7 @@
22672276
03E711812C936DD1000B6F96 /* OpenGraphParser.swift in Sources */,
22682277
03C8B4962C6D065900A07CCD /* ImageViewer.swift in Sources */,
22692278
5B79F6092B98AC33002DA9BE /* ClaimYourUniqueIdentitySheet.swift in Sources */,
2279+
50089A012C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */,
22702280
C973AB652A323167002AED16 /* EventReference+CoreDataProperties.swift in Sources */,
22712281
C973AB632A323167002AED16 /* Relay+CoreDataProperties.swift in Sources */,
22722282
C94FE9F729DB259300019CD3 /* Text+Gradient.swift in Sources */,
@@ -2520,6 +2530,7 @@
25202530
035729CA2BE4173E005FEE85 /* PreviewData.swift in Sources */,
25212531
037975D12C0E341500ADDF37 /* MockFeatureFlags.swift in Sources */,
25222532
C92E7F682C4EFF3D00B80638 /* WebSocketErrorEvent.swift in Sources */,
2533+
50089A0D2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */,
25232534
504454722C90729100251A7E /* Event+Hydration.swift in Sources */,
25242535
5BD08BB22A38E96F00BB926C /* JSONRelayMetadata.swift in Sources */,
25252536
C936B45A2A4C7B7C00DF1EB9 /* Nos.xcdatamodeld in Sources */,
@@ -2589,6 +2600,7 @@
25892600
C9B678DC29EEBF3B00303F33 /* DependencyInjection.swift in Sources */,
25902601
C9F0BB6D29A503D9000547FC /* Int+Bool.swift in Sources */,
25912602
0314D5AD2C7D31060002E7F4 /* MediaService.swift in Sources */,
2603+
50089A022C9712EF00834588 /* JSONEvent+Kinds.swift in Sources */,
25922604
C9C097232C13534800F78EC3 /* DatabaseCleanerTests.swift in Sources */,
25932605
03618C962C826D5E00BCBC55 /* CompactNoteView.swift in Sources */,
25942606
DC08FF812A7969C5009F87D1 /* UIDevice+Simulator.swift in Sources */,

Nos/Models/EventKind.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public enum EventKind: Int64, CaseIterable, Hashable {
3333
/// Channel Message
3434
case channelMessage = 42
3535

36+
/// Request to Vanish
37+
case requestToVanish = 62
38+
3639
/// Gift Wrap
3740
case giftWrap = 1059
3841

Nos/Models/JSONEvent+Kinds.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Foundation
2+
3+
extension JSONEvent {
4+
5+
/// An event that represents the user's request for all of their published notes to be removed from relays.
6+
/// - Parameters:
7+
/// - pubKey: The public key of the user making the request.
8+
/// - relays: The relays to request removal from. Note: A nil or empty relay array will be interpreted to mean
9+
/// that the user seeks removal from all relays.
10+
/// - reason: The reason the user wishes to have their content removed. Optional.
11+
/// - Returns: The ``JSONEvent`` representing the request.
12+
static func requestToVanish(pubKey: String, relays: [URL]? = nil, reason: String? = nil) -> JSONEvent {
13+
let tags: [[String]]
14+
if let relays, !relays.isEmpty {
15+
tags = relays.map { ["relay", $0.absoluteString] }
16+
} else {
17+
tags = [["relay", "ALL_RELAYS"]]
18+
}
19+
20+
return JSONEvent(
21+
pubKey: pubKey,
22+
kind: .requestToVanish,
23+
tags: tags,
24+
content: reason ?? ""
25+
)
26+
}
27+
}
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import Foundation
2+
import Logger
3+
4+
extension CurrentUser {
5+
6+
/// Builds a dictionary to be used as content when publishing a kind 0
7+
/// event.
8+
private func buildMetadataJSONObject(author: Author) -> [String: String] {
9+
var metaEvent = MetadataEventJSON(
10+
displayName: author.displayName,
11+
name: author.name,
12+
nip05: author.nip05,
13+
uns: author.uns,
14+
about: author.about,
15+
website: author.website,
16+
picture: author.profilePhotoURL?.absoluteString
17+
).dictionary
18+
if let rawData = author.rawMetadata {
19+
// Tack on any unsupported fields back onto the dictionary before
20+
// publish.
21+
do {
22+
let rawJson = try JSONSerialization.jsonObject(with: rawData)
23+
if let rawDictionary = rawJson as? [String: AnyObject] {
24+
for key in rawDictionary.keys {
25+
guard metaEvent[key] == nil else {
26+
continue
27+
}
28+
if let rawValue = rawDictionary[key] as? String {
29+
metaEvent[key] = rawValue
30+
Log.debug("Added \(key) : \(rawValue)")
31+
}
32+
}
33+
}
34+
} catch {
35+
Log.debug("Couldn't parse a JSON from the user raw metadata")
36+
// Continue with the metaEvent object we built previously
37+
}
38+
}
39+
return metaEvent
40+
}
41+
42+
@MainActor func publishMetadata() async throws {
43+
guard let pubKey = publicKeyHex else {
44+
Log.debug("Error: no publicKeyHex")
45+
throw CurrentUserError.authorNotFound
46+
}
47+
guard let pair = keyPair else {
48+
Log.debug("Error: no keyPair")
49+
throw CurrentUserError.authorNotFound
50+
}
51+
guard let context = viewContext else {
52+
Log.debug("Error: no context")
53+
throw CurrentUserError.authorNotFound
54+
}
55+
guard let author = try Author.find(by: pubKey, context: context) else {
56+
Log.debug("Error: no author in DB")
57+
throw CurrentUserError.authorNotFound
58+
}
59+
60+
self.author = author
61+
62+
let jsonObject = buildMetadataJSONObject(author: author)
63+
let data = try JSONSerialization.data(withJSONObject: jsonObject)
64+
let content = String(decoding: data, as: UTF8.self)
65+
66+
let jsonEvent = JSONEvent(
67+
pubKey: pubKey,
68+
kind: .metaData,
69+
tags: [],
70+
content: content
71+
)
72+
73+
do {
74+
try await relayService.publishToAll(
75+
event: jsonEvent,
76+
signingKey: pair,
77+
context: viewContext
78+
)
79+
} catch {
80+
Log.error(error.localizedDescription)
81+
throw CurrentUserError.errorWhilePublishingToRelays
82+
}
83+
}
84+
85+
@MainActor func publishMuteList(keys: [String]) async {
86+
guard let pubKey = publicKeyHex else {
87+
Log.debug("Error: no pubKey")
88+
return
89+
}
90+
91+
let jsonEvent = JSONEvent(pubKey: pubKey, kind: .mute, tags: keys.pTags, content: "")
92+
93+
if let pair = keyPair {
94+
do {
95+
try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext)
96+
} catch {
97+
Log.debug("Failed to update mute list \(error.localizedDescription)")
98+
}
99+
}
100+
}
101+
102+
@MainActor func publishDelete(for identifiers: [String], reason: String = "") async {
103+
guard let pubKey = publicKeyHex else {
104+
Log.debug("Error: no pubKey")
105+
return
106+
}
107+
108+
let tags = identifiers.eTags
109+
let jsonEvent = JSONEvent(pubKey: pubKey, kind: .delete, tags: tags, content: reason)
110+
111+
if let pair = keyPair {
112+
do {
113+
try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext)
114+
} catch {
115+
Log.debug("Failed to delete events \(error.localizedDescription)")
116+
}
117+
}
118+
}
119+
120+
@MainActor func publishContactList(tags: [[String]]) async {
121+
guard let pubKey = publicKeyHex else {
122+
Log.debug("Error: no pubKey")
123+
return
124+
}
125+
126+
guard let relays = author?.relays else {
127+
Log.debug("Error: No relay service")
128+
return
129+
}
130+
131+
var relayString = "{"
132+
for relay in relays {
133+
if let address = relay.address {
134+
relayString += "\"\(address)\":{\"write\":true,\"read\":true},"
135+
}
136+
}
137+
relayString.removeLast()
138+
relayString += "}"
139+
140+
let jsonEvent = JSONEvent(pubKey: pubKey, kind: .contactList, tags: tags, content: relayString)
141+
142+
if let pair = keyPair {
143+
do {
144+
try await relayService.publishToAll(event: jsonEvent, signingKey: pair, context: viewContext)
145+
} catch {
146+
Log.debug("failed to update Follows \(error.localizedDescription)")
147+
}
148+
}
149+
}
150+
151+
/// Follow by public hex key
152+
@MainActor func follow(author toFollow: Author) async throws {
153+
guard let followKey = toFollow.hexadecimalPublicKey else {
154+
Log.debug("Error: followKey is nil")
155+
return
156+
}
157+
158+
Log.debug("Following \(followKey)")
159+
160+
var followKeys = await Array(socialGraph.followedKeys)
161+
followKeys.append(followKey)
162+
163+
// Update author to add the new follow
164+
if let followedAuthor = try? Author.find(by: followKey, context: viewContext), let currentUser = author {
165+
let follow = try Follow.findOrCreate(
166+
source: currentUser,
167+
destination: followedAuthor,
168+
context: viewContext
169+
)
170+
171+
// Add to the current user's follows
172+
currentUser.follows.insert(follow)
173+
}
174+
175+
try viewContext.save()
176+
await publishContactList(tags: followKeys.pTags)
177+
}
178+
179+
/// Unfollow by public hex key
180+
@MainActor func unfollow(author toUnfollow: Author) async throws {
181+
guard let unfollowedKey = toUnfollow.hexadecimalPublicKey else {
182+
Log.debug("Error: unfollowedKey is nil")
183+
return
184+
}
185+
186+
Log.debug("Unfollowing \(unfollowedKey)")
187+
188+
let stillFollowingKeys = await Array(socialGraph.followedKeys)
189+
.filter { $0 != unfollowedKey }
190+
191+
// Update author to only follow those still following
192+
if let unfollowedAuthor = try? Author.find(by: unfollowedKey, context: viewContext), let currentUser = author {
193+
// Remove from the current user's follows
194+
let unfollows = Follow.follows(source: currentUser, destination: unfollowedAuthor, context: viewContext)
195+
196+
for unfollow in unfollows {
197+
// Remove current user's follows
198+
currentUser.follows.remove(unfollow)
199+
}
200+
}
201+
202+
try viewContext.save()
203+
await publishContactList(tags: stillFollowingKeys.pTags)
204+
}
205+
206+
@MainActor func publishRequestToVanish(to relays: [URL]? = nil, reason: String? = nil) async throws {
207+
guard let keyPair else {
208+
Log.debug("Error: no key pair")
209+
return
210+
}
211+
212+
let pubKey = keyPair.publicKey.hex
213+
let jsonEvent = JSONEvent.requestToVanish(pubKey: pubKey, relays: relays, reason: reason)
214+
215+
do {
216+
if let relays, !relays.isEmpty {
217+
try await relayService.publish(event: jsonEvent, to: relays, signingKey: keyPair, context: viewContext)
218+
} else {
219+
try await relayService.publishToAll(event: jsonEvent, signingKey: keyPair, context: viewContext)
220+
}
221+
} catch {
222+
Log.debug("Failed to publish request to vanish \(error.localizedDescription)")
223+
}
224+
}
225+
}

0 commit comments

Comments
 (0)