Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for NIP-62 Request to Vanish events #80 #1507

Merged
merged 2 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading