Skip to content

Commit

Permalink
Merge pull request #1507 from planetary-social/bdm/80
Browse files Browse the repository at this point in the history
added support for NIP-62 Request to Vanish events #80
  • Loading branch information
bryanmontz authored Sep 18, 2024
2 parents 48cd898 + c3118df commit 3bd4f82
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 201 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Localized follows notifications. [#1446](https://github.com/planetary-social/nos/issues/1446)
- Fixed alert when uploading big files suggesting users pay for nostr.build. [#1321](https://github.com/planetary-social/nos/issues/1321)
- Fixed issue where push notifications were not re-registered after account change. [#1501](https://github.com/planetary-social/nos/issues/1501)
- 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)
Expand Down
12 changes: 12 additions & 0 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,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 */; };
500899F32C95C1F900834588 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; };
502B6C3D2C9462A400446316 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; };
50089A172C98678600834588 /* View+ListRowGradientBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A162C98678600834588 /* View+ListRowGradientBackground.swift */; };
Expand Down Expand Up @@ -663,6 +667,8 @@
3FFB1D9229A6BBCE002A755D /* EventReference+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EventReference+CoreDataClass.swift"; sourceTree = "<group>"; };
3FFB1D9529A6BBEC002A755D /* Collection+SafeSubscript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+SafeSubscript.swift"; sourceTree = "<group>"; };
3FFB1D9B29A7DF9D002A755D /* StackedAvatarsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackedAvatarsView.swift; sourceTree = "<group>"; };
50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEvent+Kinds.swift"; sourceTree = "<group>"; };
50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrentUser+PublishEvents.swift"; sourceTree = "<group>"; };
502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationRegistrar.swift; sourceTree = "<group>"; };
50089A162C98678600834588 /* View+ListRowGradientBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ListRowGradientBackground.swift"; sourceTree = "<group>"; };
5044546D2C90726A00251A7E /* Event+Fetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+Fetching.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1582,6 +1588,7 @@
C9ADB13C29929B540075E7F8 /* Bech32.swift */,
C9B71DC12A9003670031ED9F /* CrashReporting.swift */,
A34E439829A522F20057AFCB /* CurrentUser.swift */,
50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */,
034EBDB92C24895E006BA35A /* CurrentUserError.swift */,
C9C097242C13537900F78EC3 /* DatabaseCleaner.swift */,
C98298322ADD7F9A0096C5B5 /* DeepLinkService.swift */,
Expand Down Expand Up @@ -1801,6 +1808,7 @@
0365CD862C4016A200622A1A /* EventKind.swift */,
C9EE3E622A053910008A7491 /* ExpirationTimeOption.swift */,
C93CA0C229AE3A1E00921183 /* JSONEvent.swift */,
50089A002C9712EF00834588 /* JSONEvent+Kinds.swift */,
5B503F612A291A1A0098805A /* JSONRelayMetadata.swift */,
C9F84C26298DC98800C6714D /* KeyPair.swift */,
C930055E2A6AF8320098CA9E /* LoadingContent.swift */,
Expand Down Expand Up @@ -2210,6 +2218,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 */,
Expand Down Expand Up @@ -2267,6 +2276,7 @@
03E711812C936DD1000B6F96 /* OpenGraphParser.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 */,
Expand Down Expand Up @@ -2520,6 +2530,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 */,
Expand Down Expand Up @@ -2589,6 +2600,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 */,
Expand Down
3 changes: 3 additions & 0 deletions Nos/Models/EventKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions Nos/Models/JSONEvent+Kinds.swift
Original file line number Diff line number Diff line change
@@ -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 ?? ""
)
}
}
225 changes: 225 additions & 0 deletions Nos/Service/CurrentUser+PublishEvents.swift
Original file line number Diff line number Diff line change
@@ -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)")
}
}
}
Loading

0 comments on commit 3bd4f82

Please sign in to comment.