commit 2a8b9f75c13d4b70cf77adc5d45e124b2749ad96
parent 7d323b65e47867e4d8209490b1b54ae79c3ac6f7
Author: William Casarin <jb55@jb55.com>
Date: Wed, 25 Jan 2023 09:53:41 -0800
Initial NIP-51 Mute List Implementation
Diffstat:
4 files changed, 183 insertions(+), 0 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -157,6 +157,8 @@
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */; };
4CF0ABD42980996B00D66079 /* Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD32980996B00D66079 /* Report.swift */; };
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD529817F5B00D66079 /* ReportView.swift */; };
+ 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD72981980C00D66079 /* Lists.swift */; };
+ 4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABDB2981A19E00D66079 /* ListTests.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
@@ -391,6 +393,8 @@
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventActionBar.swift; sourceTree = "<group>"; };
4CF0ABD32980996B00D66079 /* Report.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Report.swift; sourceTree = "<group>"; };
4CF0ABD529817F5B00D66079 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
+ 4CF0ABD72981980C00D66079 /* Lists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lists.swift; sourceTree = "<group>"; };
+ 4CF0ABDB2981A19E00D66079 /* ListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTests.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
@@ -633,6 +637,7 @@
4C3A1D3629637E0500558C0F /* PreviewCache.swift */,
64FBD06E296255C400D9D3B2 /* Theme.swift */,
4CB8838529656C8B00DC99E7 /* NIP05.swift */,
+ 4CF0ABD72981980C00D66079 /* Lists.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -748,6 +753,7 @@
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */,
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
4CB88399297322D200DC99E7 /* DMTests.swift */,
+ 4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -1031,6 +1037,7 @@
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
+ 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
@@ -1091,6 +1098,7 @@
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
+ 4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/damus/Util/Keys.swift b/damus/Util/Keys.swift
@@ -12,12 +12,25 @@ import Vault
let PUBKEY_HRP = "npub"
let PRIVKEY_HRP = "nsec"
+struct FullKeypair {
+ let pubkey: String
+ let privkey: String
+}
+
struct Keypair {
let pubkey: String
let privkey: String?
let pubkey_bech32: String
let privkey_bech32: String?
+ func to_full() -> FullKeypair? {
+ guard let privkey = self.privkey else {
+ return nil
+ }
+
+ return FullKeypair(pubkey: pubkey, privkey: privkey)
+ }
+
init(pubkey: String, privkey: String?) {
self.pubkey = pubkey
self.privkey = privkey
diff --git a/damus/Util/Lists.swift b/damus/Util/Lists.swift
@@ -0,0 +1,91 @@
+//
+// Mute.swift
+// damus
+//
+// Created by William Casarin on 2023-01-25.
+//
+
+import Foundation
+
+func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: String) -> NostrEvent? {
+ return create_or_update_list_event(keypair: keypair, mprev: mprev, to_add: to_add, list_name: "mute", list_type: "p")
+}
+
+func remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent, to_remove: String) -> NostrEvent? {
+ return remove_from_list_event(keypair: keypair, prev: prev, to_remove: to_remove, tag_type: "p")
+}
+
+func create_or_update_list_event(keypair: FullKeypair, mprev: NostrEvent?, to_add: String, list_name: String, list_type: String) -> NostrEvent? {
+ let pubkey = keypair.pubkey
+
+ if let prev = mprev {
+ if let okprev = ensure_list_name(list: prev, name: list_name), prev.pubkey == keypair.pubkey {
+ return add_to_list_event(keypair: keypair, prev: okprev, to_add: to_add, tag_type: list_type)
+ }
+ }
+
+ var tags = [["d", list_name], [list_type, to_add]]
+ let ev = NostrEvent(content: "", pubkey: pubkey, kind: 30000, tags: tags)
+
+ ev.tags = tags
+ ev.id = calculate_event_id(ev: ev)
+ ev.sig = sign_event(privkey: keypair.privkey, ev: ev)
+
+ return ev
+}
+
+func remove_from_list_event(keypair: FullKeypair, prev: NostrEvent, to_remove: String, tag_type: String) -> NostrEvent? {
+ var exists = false
+ for tag in prev.tags {
+ if tag.count >= 2 && tag[0] == tag_type && tag[1] == to_remove {
+ exists = true
+ }
+ }
+
+ // make sure we actually have the pubkey to remove
+ guard exists else {
+ return nil
+ }
+
+ let new_tags = prev.tags.filter { tag in
+ !(tag.count >= 2 && tag[0] == tag_type && tag[1] == to_remove)
+ }
+
+ let ev = NostrEvent(content: prev.content, pubkey: keypair.pubkey, kind: 30000, tags: new_tags)
+ ev.id = calculate_event_id(ev: ev)
+ ev.sig = sign_event(privkey: keypair.privkey, ev: ev)
+
+ return ev
+}
+
+func add_to_list_event(keypair: FullKeypair, prev: NostrEvent, to_add: String, tag_type: String) -> NostrEvent? {
+ for tag in prev.tags {
+ // we are already muting this user
+ if tag.count >= 2 && tag[0] == tag_type && tag[1] == to_add {
+ return nil
+ }
+ }
+
+ let new = NostrEvent(content: prev.content, pubkey: keypair.pubkey, kind: 30000, tags: prev.tags)
+ new.tags.append([tag_type, to_add])
+ new.id = calculate_event_id(ev: new)
+ new.sig = sign_event(privkey: keypair.privkey, ev: new)
+
+ return new
+}
+
+func ensure_list_name(list: NostrEvent, name: String) -> NostrEvent? {
+ for tag in list.tags {
+ if tag.count >= 2 && tag[0] == "d" {
+ if tag[1] != name {
+ return nil
+ } else {
+ return list
+ }
+ }
+ }
+
+ list.tags.insert(["d", name], at: 0)
+
+ return list
+}
diff --git a/damusTests/ListTests.swift b/damusTests/ListTests.swift
@@ -0,0 +1,71 @@
+//
+// ListTests.swift
+// damusTests
+//
+// Created by William Casarin on 2023-01-25.
+//
+
+import XCTest
+@testable import damus
+
+final class ListTests: XCTestCase {
+
+ override func setUpWithError() throws {
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+ }
+
+ override func tearDownWithError() throws {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ func testCreateMuteList() throws {
+ let privkey = "87f313b03f2548e6eaf1c188db47078e08e894252949779b639b28db0891937a"
+ let pubkey = "4b0c29bf96496130c1253102f6870c0eee05db38a257315858272aa43fd19685"
+ let to_mute = "2fa2630fea3d2c188c49f2799fcd92f0e9879ea6a36ae60770a5428ed6c19edd"
+ let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
+ let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: to_mute)!
+
+ XCTAssertEqual(mutelist.pubkey, pubkey)
+ XCTAssertEqual(mutelist.content, "")
+ XCTAssertEqual(mutelist.tags.count, 2)
+ XCTAssertEqual(mutelist.tags[0][0], "d")
+ XCTAssertEqual(mutelist.tags[0][1], "mute")
+ XCTAssertEqual(mutelist.tags[1][0], "p")
+ XCTAssertEqual(mutelist.tags[1][1], to_mute)
+ }
+
+ func testCreateAndRemoveMuteList() throws {
+ let privkey = "87f313b03f2548e6eaf1c188db47078e08e894252949779b639b28db0891937a"
+ let pubkey = "4b0c29bf96496130c1253102f6870c0eee05db38a257315858272aa43fd19685"
+ let to_mute = "2fa2630fea3d2c188c49f2799fcd92f0e9879ea6a36ae60770a5428ed6c19edd"
+ let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
+ let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: to_mute)!
+ let new = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: to_mute)!
+
+ XCTAssertEqual(new.pubkey, pubkey)
+ XCTAssertEqual(new.content, "")
+ XCTAssertEqual(new.tags.count, 1)
+ XCTAssertEqual(new.tags[0][0], "d")
+ XCTAssertEqual(new.tags[0][1], "mute")
+ }
+
+ func testAddToExistingMutelist() throws {
+ let privkey = "87f313b03f2548e6eaf1c188db47078e08e894252949779b639b28db0891937a"
+ let pubkey = "4b0c29bf96496130c1253102f6870c0eee05db38a257315858272aa43fd19685"
+ let to_mute = "2fa2630fea3d2c188c49f2799fcd92f0e9879ea6a36ae60770a5428ed6c19edd"
+ let to_mute_2 = "976b4ab41f8634119b4f21f57ef5836a4bef65d0bf72c7ced67b8b170ba4a38d"
+ let keypair = FullKeypair(pubkey: pubkey, privkey: privkey)
+ let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: to_mute)!
+ let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: to_mute_2)!
+
+ XCTAssertEqual(new.pubkey, pubkey)
+ XCTAssertEqual(new.content, "")
+ XCTAssertEqual(new.tags.count, 3)
+ XCTAssertEqual(new.tags[0][0], "d")
+ XCTAssertEqual(new.tags[0][1], "mute")
+ XCTAssertEqual(new.tags[1][0], "p")
+ XCTAssertEqual(new.tags[1][1], to_mute)
+ XCTAssertEqual(new.tags[2][0], "p")
+ XCTAssertEqual(new.tags[2][1], to_mute_2)
+ }
+}