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

Add support for NIP-40 - Expiration Timestamp #193

Merged
merged 1 commit into from
Oct 20, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The following [NIPs](https://github.com/nostr-protocol/nips) are implemented:
- [ ] [NIP-36: Sensitive Content](https://github.com/nostr-protocol/nips/blob/master/36.md)
- [ ] [NIP-38: User Statuses](https://github.com/nostr-protocol/nips/blob/master/38.md)
- [ ] [NIP-39: External Identities in Profiles](https://github.com/nostr-protocol/nips/blob/master/39.md)
- [ ] [NIP-40: Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md)
- [x] [NIP-40: Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md)
- [ ] [NIP-42: Authentication of clients to relays](https://github.com/nostr-protocol/nips/blob/master/42.md)
- [x] [NIP-44: Versioned Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
- [ ] [NIP-45: Counting results](https://github.com/nostr-protocol/nips/blob/master/45.md)
Expand Down
28 changes: 28 additions & 0 deletions Sources/NostrSDK/Events/NostrEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,26 @@ public class NostrEvent: Codable, Equatable, Hashable {
tags.compactMap { EventCoordinates(eventCoordinatesTag: $0) }
}

/// Unix timestamp at which the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
/// See [NIP-40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md).
public var expiration: Int64? {
if let expiration = firstValueForTagName(.expiration) {
return Int64(expiration)
} else {
return nil
}
}

/// Whether the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
/// See [NIP-40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md).
public var isExpired: Bool {
if let expiration {
return Int64(Date.now.timeIntervalSince1970) >= expiration
} else {
return false
}
}

/// All tags with the provided name.
public func allTags(withTagName tagName: TagName) -> [Tag] {
tags.filter { $0.name == tagName.rawValue }
Expand Down Expand Up @@ -355,6 +375,14 @@ public extension NostrEvent {
return self
}

/// Specifies a unix timestamp at which the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
/// See [NIP-40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md).
@discardableResult
public final func expiration(_ expiration: Int64) -> Self {
tags.append(Tag(name: .expiration, value: String(expiration)))
return self
}

public func build(signedBy keypair: Keypair) throws -> T {
try T(
kind: kind,
Expand Down
4 changes: 4 additions & 0 deletions Sources/NostrSDK/Tag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public enum TagName: String {
/// coordinates to a replaceable event, which includes the kind number, pubkey that signed the event, and optionally the identifier (if the replaceable event is parameterized)
case eventCoordinates = "a"

/// Specifies a unix timestamp at which the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
/// See [NIP-40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md).
case expiration

/// a hashtag to categorize events for easy searching
case hashtag = "t"

Expand Down
20 changes: 20 additions & 0 deletions Tests/NostrSDKTests/Events/NostrEventTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,24 @@ final class NostrEventTests: XCTestCase, FixtureLoading, MetadataCoding {
XCTAssertEqual(metadata.relays?[1], relay2)
}

func testExpiration() throws {
let futureExpiration = Int64(Date.now.timeIntervalSince1970 + 10000)
let futureExpirationEvent = try NostrEvent.Builder(kind: .textNote)
.expiration(futureExpiration)
.build(signedBy: .test)
XCTAssertEqual(futureExpirationEvent.expiration, futureExpiration)
XCTAssertFalse(futureExpirationEvent.isExpired)

let pastExpiration = Int64(Date.now.timeIntervalSince1970 - 1)
let pastExpirationEvent = try NostrEvent.Builder(kind: .textNote)
.expiration(pastExpiration)
.build(signedBy: .test)
XCTAssertEqual(pastExpirationEvent.expiration, pastExpiration)
XCTAssertTrue(pastExpirationEvent.isExpired)

let decodedExpiredEvent: NostrEvent = try decodeFixture(filename: "test_event_expired")
XCTAssertEqual(decodedExpiredEvent.expiration, 1697090842)
XCTAssertTrue(decodedExpiredEvent.isExpired)
}

}
9 changes: 9 additions & 0 deletions Tests/NostrSDKTests/Fixtures/test_event_expired.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"content": "This is a test message.",
"created_at": 1687090842,
"id": "b7fbbb4d473db3b3a671ad995f1248d56e9a2ed7f145c1e5f74308e60db2eba7",
"kind": 1,
"pubkey": "9947f9659dd80c3682402b612f5447e28249997fb3709500c32a585eb0977340",
"sig": "09a1dcace3cec8cd9934b544cede6955c9876e385d73491f1ca5354a0baecda834c43ac9510ef779f8c8c7543b3faccd821e40e0a52dde8527d071cb0d53230c",
"tags": [["expiration","1697090842"]]
}