-
Notifications
You must be signed in to change notification settings - Fork 14
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
Adds support for NIP-28 #178
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// | ||
// ChannelMetadata.swift | ||
// | ||
// | ||
// Created by Konstantin Yurchenko, Jr on 9/20/24. | ||
// | ||
|
||
import Foundation | ||
|
||
/// A structure that describes channel. | ||
/// | ||
/// See [NIP-28 Specification](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-40-create-channel). | ||
public struct ChannelMetadata: Codable { | ||
/// Channel name | ||
public let name: String? | ||
/// Channel desctription | ||
public let about: String? | ||
/// URL of channel picture | ||
public let picture: String? | ||
/// List of relays to download and broadcast events to | ||
public let relays: [String]? | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case name | ||
case about | ||
case picture | ||
case relays | ||
} | ||
|
||
public init(name: String? = nil, about: String? = nil, picture: String? = nil, relays: [String] = []) { | ||
self.name = name | ||
self.about = about | ||
self.picture = picture | ||
self.relays = relays | ||
} | ||
|
||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
|
||
name = try container.decodeIfPresent(String.self, forKey: .name) | ||
about = try container.decodeIfPresent(String.self, forKey: .about) | ||
picture = try container.decodeIfPresent(String.self, forKey: .picture) | ||
relays = try container.decodeIfPresent([String].self, forKey: .relays) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,76 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// CreateChannelEvent.swift | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Created by Konstantin Yurchenko, Jr on 9/11/24. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import Foundation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// Create a public chat channel. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-40-create-channel). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
public class CreateChannelEvent: NostrEvent { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
public required init(from decoder: Decoder) throws { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try super.init(from: decoder) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@available(*, unavailable, message: "This initializer is unavailable for this class.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@available(*, unavailable, message: "This initializer is unavailable for this class.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@available(*, unavailable, message: "This initializer is unavailable for this class.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
init(content: String, tags: [Tag], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try super.init(kind: Self.kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class var kind: EventKind { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.channelCreation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
public extension EventCreating { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func createChannelEvent(withContent content: String, signedBy keypair: Keypair) throws -> CreateChannelEvent { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm in the process of deprecating the See https://github.com/nostr-sdk/nostr-sdk-ios/pull/175/files#diff-c87e105b65b1d7c9512acf2ee8c7e3fc7582648811025c959a37b36d97403b67R170 as an example. Also, it would be better to have an API where the developer can specify the basic channel metadata (name, about, picture and relays) rather than needing to enter in raw JSON in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, @tyiu, thanks for your time and looking over my commit! Yes, I am on board with the later. The interface in this PR needs further definition and polish. I don't quite understand what you mean by ‘deprecating EventCreating protocol’. I do use it a lot but I did find it somewhat rigid. Regards! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I say deprecating the try CreateChannelEvent.Builder()
.channelMetadata(channelMetadata, merging: rawChannelMetadata)
.build(signedBy: keypair) Take a look at what we do here for kind 0, as an example: nostr-sdk-ios/Sources/NostrSDK/Events/MetadataEvent.swift Lines 161 to 192 in 8b40eb9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @masterial Turns out the SwiftLint errors were on the main branch, due to how CI pulls in the latest SwiftLint version and it had some breaking changes in what it validates. I just fixed the issue on the main branch so the lint errors should go away after you pull it into your branch. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merged, thanks for the heads up, @tyiu! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return try CreateChannelEvent(content: content, tags: [], signedBy: keypair) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
public extension CreateChannelEvent { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// Builder of ``CreateChannelEvent``. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
final class Builder: NostrEvent.Builder<CreateChannelEvent> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
public init() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
super.init(kind: .channelCreation) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
public final func channelMetadata(_ channelMetadata: ChannelMetadata, merging rawChannelMetadata: [String: Any] = [:]) throws -> Self { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let channelMetadataAsData = try JSONEncoder().encode(channelMetadata) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let allChannelMetadataAsData: Data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if rawChannelMetadata.isEmpty { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
allChannelMetadataAsData = channelMetadataAsData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
var channelMetadataAsDictionary = try JSONSerialization.jsonObject(with: channelMetadataAsData, options: []) as? [String: Any] ?? [:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
channelMetadataAsDictionary.merge(rawChannelMetadata) { (current, _) in current } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
allChannelMetadataAsData = try JSONSerialization.data(withJSONObject: channelMetadataAsDictionary, options: .sortedKeys) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
guard let allChannelMetadataAsString = String(data: allChannelMetadataAsData, encoding: .utf8) else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw EventCreatingError.invalidInput | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
content(allChannelMetadataAsString) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return self | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// | ||
// CreateChannelMessageEvent.swift | ||
// | ||
// | ||
// Created by Konstantin Yurchenko, Jr on 9/11/24. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Send a text message to a channel. | ||
/// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-42-create-channel-message). | ||
public class CreateChannelMessageEvent: NostrEvent { | ||
|
||
public required init(from decoder: Decoder) throws { | ||
try super.init(from: decoder) | ||
} | ||
|
||
@available(*, unavailable, message: "This initializer is unavailable for this class.") | ||
required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { | ||
try super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) | ||
} | ||
|
||
@available(*, unavailable, message: "This initializer is unavailable for this class.") | ||
required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { | ||
super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) | ||
} | ||
|
||
@available(*, unavailable, message: "This initializer is unavailable for this class.") | ||
override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { | ||
super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) | ||
} | ||
|
||
init(content: String, tags: [Tag], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { | ||
try super.init(kind: Self.kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) | ||
} | ||
|
||
class var kind: EventKind { | ||
.channelMessage | ||
} | ||
} | ||
|
||
public extension EventCreating { | ||
func createChannelMessageEvent( | ||
withContent content: String, | ||
eventId: String, | ||
relayUrl: String, | ||
hashtag: String? = nil, | ||
signedBy keypair: Keypair | ||
) throws -> CreateChannelMessageEvent { | ||
|
||
var tags: [Tag] = [ | ||
Tag.pubkey(keypair.publicKey.hex), | ||
Tag.event(eventId, otherParameters: [relayUrl, "root"]), | ||
] | ||
|
||
if let hashtag = hashtag { | ||
tags.append(Tag.hashtag(hashtag)) | ||
} | ||
|
||
return try CreateChannelMessageEvent(content: content, tags: tags, signedBy: keypair) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// | ||
// HideChannelMessageEvent.swift | ||
// | ||
// | ||
// Created by Konstantin Yurchenko, Jr on 9/11/24. | ||
// | ||
|
||
import Foundation | ||
|
||
/// User no longer wants to see a certain message. | ||
/// See [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md#kind-43-hide-message). | ||
public class HideChannelMessageEvent: NostrEvent { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here for the |
||
|
||
public required init(from decoder: Decoder) throws { | ||
try super.init(from: decoder) | ||
} | ||
|
||
@available(*, unavailable, message: "This initializer is unavailable for this class.") | ||
required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { | ||
try super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) | ||
} | ||
|
||
@available(*, unavailable, message: "This initializer is unavailable for this class.") | ||
required init(kind: EventKind, content: String, tags: [Tag] = [], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), pubkey: String) { | ||
super.init(kind: kind, content: content, tags: tags, createdAt: createdAt, pubkey: pubkey) | ||
} | ||
|
||
@available(*, unavailable, message: "This initializer is unavailable for this class.") | ||
override init(id: String, pubkey: String, createdAt: Int64, kind: EventKind, tags: [Tag], content: String, signature: String?) { | ||
super.init(id: id, pubkey: pubkey, createdAt: createdAt, kind: kind, tags: tags, content: content, signature: signature) | ||
} | ||
|
||
init(content: String, tags: [Tag], createdAt: Int64 = Int64(Date.now.timeIntervalSince1970), signedBy keypair: Keypair) throws { | ||
try super.init(kind: Self.kind, content: content, tags: tags, createdAt: createdAt, signedBy: keypair) | ||
} | ||
|
||
class var kind: EventKind { | ||
.channelHideMessage | ||
} | ||
} | ||
|
||
public extension EventCreating { | ||
func hideChannelMessageEvent(withContent content: String, signedBy keypair: Keypair) throws -> HideChannelMessageEvent { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||
return try HideChannelMessageEvent(content: content, tags: [], signedBy: keypair) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to expose the basic channel metadata (name, about, picture and relays) properties.
Take a look at https://github.com/nostr-sdk/nostr-sdk-ios/blob/main/Sources/NostrSDK/Events/MetadataEvent.swift as it also does JSON encoding and decoding for the kind 0 event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I make a MetadataChannel class here or what?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a separate file, you could create a
ChannelMetadata
struct with the properties. Then, you could create aChannelMetadataInterpreting
protocol that decodes the JSON.Kinds 40
CreateChannelEvent
and kind 41SetChannelMetadataEvent
could extend fromChannelMetadataInterpreting
.Something like this, it'll be pretty similar to what we do with kind 0: