Skip to content

Commit

Permalink
Add support for arbitrary single-letter tags in Filter
Browse files Browse the repository at this point in the history
  • Loading branch information
tyiu committed May 5, 2024
1 parent 429fcb9 commit ad9eae2
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 39 deletions.
85 changes: 65 additions & 20 deletions Sources/NostrSDK/Filter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
/// A structure that describes a filter to subscribe to relays with.
///
/// > Note: [NIP-01 Specification](https://github.com/nostr-protocol/nips/blob/master/01.md#communication-between-clients-and-relays)
public struct Filter: Codable, Hashable, Equatable {
public struct Filter: Encodable, Hashable, Equatable {
/// a list of event ids or prefixes
public let ids: [String]?

Expand All @@ -19,13 +19,10 @@ public struct Filter: Codable, Hashable, Equatable {

/// a list of a kind numbers
public let kinds: [Int]?

/// a list of event ids that are referenced in an "e" tag
public let events: [String]?

/// a list of pubkeys that are referenced in a "p" tag
public let pubkeys: [String]?


/// a list of tag values
public let tags: [Character: [String]]?

/// an integer unix timestamp, events must be newer than this to pass
public let since: Int?

Expand All @@ -36,16 +33,28 @@ public struct Filter: Codable, Hashable, Equatable {
public let limit: Int?

private enum CodingKeys: String, CodingKey {
case ids = "ids"
case authors = "authors"
case kinds = "kinds"
case events = "#e"
case pubkeys = "#p"
case since = "since"
case until = "until"
case limit = "limit"
case ids
case authors
case kinds
case since
case until
case limit
}


private struct TagFilterName: CodingKey {
var stringValue: String

init(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int? { nil }

init?(intValue: Int) {
return nil
}
}

/// Creates and returns a filter with the specified parameters.
///
/// - Parameters:
Expand All @@ -54,19 +63,55 @@ public struct Filter: Codable, Hashable, Equatable {
/// - kinds: a list of a kind numbers
/// - events: a list of event ids that are referenced in an "e" tag
/// - pubkeys: a list of pubkeys that are referenced in a "p" tag
/// - tags: a list of tag values that are referenced by single English-alphabet letter tag names
/// - since: an integer unix timestamp, events must be newer than this to pass
/// - until: an integer unix timestamp, events must be older than this to pass
/// - limit: maximum number of events to be returned in the initial query
///
/// If `tags` contains an `e` tag and `events` is also provided, `events` takes precedence.
/// If `tags` contains a `p` tag and `pubkeys` is also provided, `pubkeys` takes precedence.
///
/// Returns `nil` if `tags` contains tag names that are not in the English-alphabet of A-Z or a-z.
///
/// > Important: Event ids and pubkeys should be in the 32-byte hexadecimal format, not the `note...` and `npub...` formats.
public init(ids: [String]? = nil, authors: [String]? = nil, kinds: [Int]? = nil, events: [String]? = nil, pubkeys: [String]? = nil, since: Int? = nil, until: Int? = nil, limit: Int? = nil) {
public init?(ids: [String]? = nil, authors: [String]? = nil, kinds: [Int]? = nil, events: [String]? = nil, pubkeys: [String]? = nil, tags: [Character: [String]]? = nil, since: Int? = nil, until: Int? = nil, limit: Int? = nil) {
self.ids = ids
self.authors = authors
self.kinds = kinds
self.events = events
self.pubkeys = pubkeys
self.since = since
self.until = until
self.limit = limit

if let tags {
guard tags.keys.allSatisfy({
($0 >= "A" && $0 <= "Z") || ($0 >= "a" && $0 <= "z")
}) else {
return nil
}
}

var tagsBuilder: [Character: [String]] = tags ?? [:]
if let events {
tagsBuilder["e"] = events
}
if let pubkeys {
tagsBuilder["p"] = pubkeys
}
self.tags = tagsBuilder
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(self.ids, forKey: .ids)
try container.encodeIfPresent(self.authors, forKey: .authors)
try container.encodeIfPresent(self.kinds, forKey: .kinds)
try container.encodeIfPresent(self.since, forKey: .since)
try container.encodeIfPresent(self.until, forKey: .until)
try container.encodeIfPresent(self.limit, forKey: .limit)

var tagsContainer = encoder.container(keyedBy: TagFilterName.self)
try self.tags?.forEach {
try tagsContainer.encode($0.value, forKey: TagFilterName(stringValue: "#\($0.key)"))
}
}
}
5 changes: 5 additions & 0 deletions Tests/NostrSDKTests/FilterEncodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ final class FilterEncodingTests: XCTestCase, FixtureLoading, JSONTesting {
kinds: [1, 2, 3],
events: ["event1", "event2"],
pubkeys: ["referencedPubkey1"],
tags: ["t": ["hashtag"], "e": ["thisEventFilterIsDiscarded"], "p": ["thisPubkeyFilterIsDiscarded"]],
since: 1234,
until: 12345,
limit: 5)
Expand All @@ -42,4 +43,8 @@ final class FilterEncodingTests: XCTestCase, FixtureLoading, JSONTesting {

XCTAssertTrue(areEquivalentJSONObjectStrings(expected, resultString))
}

func testFilterWithInvalidTags() throws {
XCTAssertNil(Filter(tags: ["*": []]))
}
}
1 change: 1 addition & 0 deletions Tests/NostrSDKTests/Fixtures/filter_all_fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"kinds": [1, 2, 3],
"#e": ["event1", "event2"],
"#p": ["referencedPubkey1"],
"#t": ["hashtag"],
"since": 1234,
"until": 12345,
"limit": 5
Expand Down
38 changes: 22 additions & 16 deletions Tests/NostrSDKTests/RelayRequestEncodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ final class RelayRequestEncodingTests: XCTestCase, FixtureLoading, JSONTesting {
}

func testEncodeCount() throws {
let filter = Filter(ids: nil,
authors: ["some-pubkey"],
kinds: [1, 7],
events: nil,
pubkeys: nil,
since: nil,
until: nil,
limit: nil)
let filter = try XCTUnwrap(
Filter(ids: nil,
authors: ["some-pubkey"],
kinds: [1, 7],
events: nil,
pubkeys: nil,
since: nil,
until: nil,
limit: nil
)
)

let request = try XCTUnwrap(RelayRequest.count(subscriptionId: "some-subscription-id", filter: filter), "failed to encode request")
let expected = try loadFixtureString("count_request")
Expand All @@ -51,14 +54,17 @@ final class RelayRequestEncodingTests: XCTestCase, FixtureLoading, JSONTesting {
}

func testEncodeReq() throws {
let filter = Filter(ids: nil,
authors: ["some-pubkey"],
kinds: [1, 7],
events: nil,
pubkeys: nil,
since: nil,
until: nil,
limit: nil)
let filter = try XCTUnwrap(
Filter(ids: nil,
authors: ["some-pubkey"],
kinds: [1, 7],
events: nil,
pubkeys: nil,
since: nil,
until: nil,
limit: nil
)
)

let request = try XCTUnwrap(RelayRequest.request(subscriptionId: "some-subscription-id", filter: filter), "failed to encode request")
let expected = try loadFixtureString("req")
Expand Down
9 changes: 6 additions & 3 deletions Tests/NostrSDKTests/RelayTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ final class RelayTests: XCTestCase {

wait(for: [connectExpectation!], timeout: 10)

let subscriptionId = try relay.subscribe(with: Filter(kinds: [1], limit: 1))
let filter = try XCTUnwrap(Filter(kinds: [1], limit: 1))
let subscriptionId = try relay.subscribe(with: filter)

relay.events
.sink { [unowned relay] _ in
Expand All @@ -65,7 +66,8 @@ final class RelayTests: XCTestCase {

func testSubscribeWithoutConnection() throws {
let relay = try Relay(url: RelayTests.RelayURL)
XCTAssertThrowsError(try relay.subscribe(with: Filter(kinds: [1], limit: 1))) {
let filter = try XCTUnwrap(Filter(kinds: [1], limit: 1))
XCTAssertThrowsError(try relay.subscribe(with: filter)) {
XCTAssertEqual($0 as? RelayRequestError, RelayRequestError.notConnected)
}
}
Expand All @@ -88,7 +90,8 @@ final class RelayTests: XCTestCase {

wait(for: [connectExpectation!], timeout: 10)

let subscriptionId = try relay.subscribe(with: Filter(kinds: [1], limit: 1))
let filter = try XCTUnwrap(Filter(kinds: [1], limit: 1))
let subscriptionId = try relay.subscribe(with: filter)

wait(for: [receiveExpectation!], timeout: 10)

Expand Down

0 comments on commit ad9eae2

Please sign in to comment.