nostr 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +29 -0
- data/README.md +51 -2
- data/Steepfile +2 -0
- data/lib/nostr/crypto.rb +143 -0
- data/lib/nostr/event.rb +157 -11
- data/lib/nostr/event_kind.rb +8 -0
- data/lib/nostr/events/encrypted_direct_message.rb +53 -0
- data/lib/nostr/keygen.rb +1 -1
- data/lib/nostr/user.rb +2 -30
- data/lib/nostr/version.rb +1 -1
- data/lib/nostr.rb +2 -1
- data/sig/nostr/crypto.rbs +16 -0
- data/sig/nostr/event.rbs +24 -9
- data/sig/nostr/event_kind.rbs +1 -0
- data/sig/nostr/events/encrypted_direct_message.rbs +12 -0
- data/sig/nostr/user.rbs +0 -2
- metadata +7 -5
- data/lib/nostr/event_fragment.rb +0 -111
- data/sig/nostr/event_fragment.rbs +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd56d59d68235fe8fa4bceb67df4a7e83d4b6a8295ed31cc0152002800ad7d23
|
|
4
|
+
data.tar.gz: 56b70ada7f9fd6cd29be1c7bc23b7a23adc49bfe2f8d8de48300f70709626a3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be113a67cd651739cc3bac1deb4bba30e3598cfd787e8971a764e82d58c03ad1c918d3edd14630bbb1a5e2c6504c3c344859f0449a1774df7bb7bf0d3c38f537
|
|
7
|
+
data.tar.gz: c80bbf1f4d3caa25f48e1630b650587a2319197d7edba33e824b17e93f6355d8593c41364ad1fdb86a834d8cae63ad9f15e8feacae953b0ff8d7294f09c2c765
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,40 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] - 2023-02-25
|
|
8
|
+
|
|
9
|
+
### Removed
|
|
10
|
+
|
|
11
|
+
- Removed `EventFragment` class. The `Event` class is no longer a Value Object. In other words, it is no longer
|
|
12
|
+
immutable and it may be invalid by not having attributes `id` or `sig`. The `EventFragment` abstraction, along with the
|
|
13
|
+
principles of immutability and was a major source of internal complexity as I needed to scale the codebase.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Client compliance with [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) (encrypted direct messages)
|
|
18
|
+
- Extracted the cryptographic concerns into a `Crypto` class.
|
|
19
|
+
- Added the setters `Event#id=` and `Event#sig=`
|
|
20
|
+
- Added a method on the event class to sign events (`Event#sign`)
|
|
21
|
+
- Added a missing test for `EventKind::CONTACT_LIST`
|
|
22
|
+
- Added two convenience methods to append event and pubkey references to an event's tags `add_event_reference` and
|
|
23
|
+
`add_pubkey_reference`
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Fixed the generation of public keys
|
|
28
|
+
- Fixed the RBS signature of `User#create_event`
|
|
29
|
+
|
|
7
30
|
## [0.3.0] - 2023-02-15
|
|
8
31
|
|
|
9
32
|
### Added
|
|
10
33
|
|
|
11
34
|
- Client compliance wth [NIP-02](https://github.com/nostr-protocol/nips/blob/master/02.md) (manage contact lists)
|
|
35
|
+
- RBS type checking using Steep and TypeProf
|
|
36
|
+
|
|
37
|
+
## Fixed
|
|
38
|
+
|
|
39
|
+
- Fixed a documentation typo
|
|
40
|
+
- Fixed a documentation error regarding the receiving of messages via websockets
|
|
12
41
|
|
|
13
42
|
## [0.2.0] - 2023-01-12
|
|
14
43
|
|
data/README.md
CHANGED
|
@@ -7,6 +7,27 @@
|
|
|
7
7
|
Asynchronous Nostr client. Please note that the API is likely to change as the gem is still in development and
|
|
8
8
|
has not yet reached a stable release. Use with caution.
|
|
9
9
|
|
|
10
|
+
## Table of contents
|
|
11
|
+
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
- [Usage](#usage)
|
|
14
|
+
* [Requiring the gem](#requiring-the-gem)
|
|
15
|
+
* [Generating a keypair](#generating-a-keypair)
|
|
16
|
+
* [Generating a private key and a public key](#generating-a-private-key-and-a-public-key)
|
|
17
|
+
* [Connecting to a Relay](#connecting-to-a-relay)
|
|
18
|
+
* [WebSocket events](#websocket-events)
|
|
19
|
+
* [Requesting for events / creating a subscription](#requesting-for-events--creating-a-subscription)
|
|
20
|
+
* [Stop previous subscriptions](#stop-previous-subscriptions)
|
|
21
|
+
* [Publishing an event](#publishing-an-event)
|
|
22
|
+
* [Creating/updating your contact list](#creatingupdating-your-contact-list)
|
|
23
|
+
* [Sending an encrypted direct message](#sending-an-encrypted-direct-message)
|
|
24
|
+
- [Implemented NIPs](#implemented-nips)
|
|
25
|
+
- [Development](#development)
|
|
26
|
+
* [Type checking](#type-checking)
|
|
27
|
+
- [Contributing](#contributing)
|
|
28
|
+
- [License](#license)
|
|
29
|
+
- [Code of Conduct](#code-of-conduct)
|
|
30
|
+
|
|
10
31
|
## Installation
|
|
11
32
|
|
|
12
33
|
Install the gem and add to the application's Gemfile by executing:
|
|
@@ -31,7 +52,7 @@ require 'nostr'
|
|
|
31
52
|
|
|
32
53
|
```ruby
|
|
33
54
|
keygen = Nostr::Keygen.new
|
|
34
|
-
keypair = keygen.
|
|
55
|
+
keypair = keygen.generate_key_pair
|
|
35
56
|
|
|
36
57
|
keypair.private_key
|
|
37
58
|
keypair.public_key
|
|
@@ -220,10 +241,38 @@ update_contacts_event = user.create_event(
|
|
|
220
241
|
client.publish(update_contacts_event)
|
|
221
242
|
```
|
|
222
243
|
|
|
223
|
-
|
|
244
|
+
### Sending an encrypted direct message
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
sender_private_key = '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
|
|
248
|
+
|
|
249
|
+
encrypted_direct_message = Nostr::Events::EncryptedDirectMessage.new(
|
|
250
|
+
sender_private_key: sender_private_key,
|
|
251
|
+
recipient_public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0',
|
|
252
|
+
plain_text: 'Your feedback is appreciated, now pay $8',
|
|
253
|
+
previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460' # optional
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
encrypted_direct_message.sign(sender_private_key)
|
|
257
|
+
|
|
258
|
+
# #<Nostr::Events::EncryptedDirectMessage:0x0000000104c9fa68
|
|
259
|
+
# @content="mjIFNo1sSP3KROE6QqhWnPSGAZRCuK7Np9X+88HSVSwwtFyiZ35msmEVoFgRpKx4?iv=YckChfS2oWCGpMt1uQ4GbQ==",
|
|
260
|
+
# @created_at=1676456512,
|
|
261
|
+
# @id="daac98826d5eb29f7c013b6160986c4baf4fe6d4b995df67c1b480fab1839a9b",
|
|
262
|
+
# @kind=4,
|
|
263
|
+
# @pubkey="8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca",
|
|
264
|
+
# @sig="028bb5f5bab0396e2065000c84a4bcce99e68b1a79bb1b91a84311546f49c5b67570b48d4a328a1827e7a8419d74451347d4f55011a196e71edab31aa3d6bdac",
|
|
265
|
+
# @tags=[["p", "6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0"], ["e", "ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460"]]>
|
|
266
|
+
|
|
267
|
+
# Send it to the Relay
|
|
268
|
+
client.publish(encrypted_direct_message)
|
|
269
|
+
````
|
|
270
|
+
|
|
271
|
+
## Implemented NIPs
|
|
224
272
|
|
|
225
273
|
- [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
|
226
274
|
- [x] [NIP-02 - Client](https://github.com/nostr-protocol/nips/blob/master/02.md)
|
|
275
|
+
- [x] [NIP-04 - Client](https://github.com/nostr-protocol/nips/blob/master/04.md)
|
|
227
276
|
|
|
228
277
|
## Development
|
|
229
278
|
|
data/Steepfile
CHANGED
data/lib/nostr/crypto.rb
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nostr
|
|
4
|
+
# Performs cryptographic operations on a +Nostr::Event+.
|
|
5
|
+
class Crypto
|
|
6
|
+
# Numeric base of the OpenSSL big number used in an event content's encryption.
|
|
7
|
+
#
|
|
8
|
+
# @return [Integer]
|
|
9
|
+
#
|
|
10
|
+
BN_BASE = 16
|
|
11
|
+
|
|
12
|
+
# Name of the cipher curve used in an event content's encryption.
|
|
13
|
+
#
|
|
14
|
+
# @return [String]
|
|
15
|
+
#
|
|
16
|
+
CIPHER_CURVE = 'secp256k1'
|
|
17
|
+
|
|
18
|
+
# Name of the cipher algorithm used in an event content's encryption.
|
|
19
|
+
#
|
|
20
|
+
# @return [String]
|
|
21
|
+
#
|
|
22
|
+
CIPHER_ALGORITHM = 'aes-256-cbc'
|
|
23
|
+
|
|
24
|
+
# Encrypts a piece of text
|
|
25
|
+
#
|
|
26
|
+
# @api public
|
|
27
|
+
#
|
|
28
|
+
# @example Encrypting an event's content
|
|
29
|
+
# crypto = Nostr::Crypto.new
|
|
30
|
+
# encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8')
|
|
31
|
+
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
|
32
|
+
#
|
|
33
|
+
# @param sender_private_key [String] 32-bytes hex-encoded private key of the creator.
|
|
34
|
+
# @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient.
|
|
35
|
+
# @param plain_text [String] The text to be encrypted
|
|
36
|
+
#
|
|
37
|
+
# @return [String] Encrypted text.
|
|
38
|
+
#
|
|
39
|
+
def encrypt_text(sender_private_key, recipient_public_key, plain_text)
|
|
40
|
+
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).encrypt
|
|
41
|
+
cipher.iv = iv = cipher.random_iv
|
|
42
|
+
cipher.key = compute_shared_key(sender_private_key, recipient_public_key)
|
|
43
|
+
encrypted_text = cipher.update(plain_text) + cipher.final
|
|
44
|
+
encrypted_text = "#{Base64.encode64(encrypted_text)}?iv=#{Base64.encode64(iv)}"
|
|
45
|
+
encrypted_text.gsub("\n", '')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Decrypts a piece of text
|
|
49
|
+
#
|
|
50
|
+
# @api public
|
|
51
|
+
#
|
|
52
|
+
# @example Encrypting an event's content
|
|
53
|
+
# crypto = Nostr::Crypto.new
|
|
54
|
+
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
|
55
|
+
# decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted)
|
|
56
|
+
#
|
|
57
|
+
# @param sender_public_key [String] 32-bytes hex-encoded public key of the message creator.
|
|
58
|
+
# @param recipient_private_key [String] 32-bytes hex-encoded public key of the recipient.
|
|
59
|
+
# @param encrypted_text [String] The text to be decrypted
|
|
60
|
+
#
|
|
61
|
+
# @return [String] Decrypted text.
|
|
62
|
+
#
|
|
63
|
+
def decrypt_text(recipient_private_key, sender_public_key, encrypted_text)
|
|
64
|
+
base64_encoded_text, iv = encrypted_text.split('?iv=')
|
|
65
|
+
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
|
|
66
|
+
cipher.iv = Base64.decode64(iv)
|
|
67
|
+
cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
|
|
68
|
+
plain_text = cipher.update(Base64.decode64(base64_encoded_text)) + cipher.final
|
|
69
|
+
plain_text.force_encoding('UTF-8')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Uses the private key to generate an event id and sign the event
|
|
73
|
+
#
|
|
74
|
+
# @api public
|
|
75
|
+
#
|
|
76
|
+
# @example Signing an event
|
|
77
|
+
# crypto = Nostr::Crypto.new
|
|
78
|
+
# crypto.sign(event, private_key)
|
|
79
|
+
# event.id # => an id
|
|
80
|
+
# event.sig # => a signature
|
|
81
|
+
#
|
|
82
|
+
# @param event [Event] The event to be signed
|
|
83
|
+
# @param private_key [String] 32-bytes hex-encoded private key.
|
|
84
|
+
#
|
|
85
|
+
# @return [Event] An unsigned event.
|
|
86
|
+
#
|
|
87
|
+
def sign_event(event, private_key)
|
|
88
|
+
event_digest = hash_event(event)
|
|
89
|
+
|
|
90
|
+
hex_private_key = Array(private_key).pack('H*')
|
|
91
|
+
hex_message = Array(event_digest).pack('H*')
|
|
92
|
+
event_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
|
|
93
|
+
|
|
94
|
+
event.id = event_digest
|
|
95
|
+
event.sig = event_signature
|
|
96
|
+
|
|
97
|
+
event
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Finds a shared key between two keys
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
105
|
+
#
|
|
106
|
+
# @param private_key [String] 32-bytes hex-encoded private key.
|
|
107
|
+
# @param public_key [String] 32-bytes hex-encoded public key.
|
|
108
|
+
#
|
|
109
|
+
# @return [String] A shared key used in the event's content encryption and decryption.
|
|
110
|
+
#
|
|
111
|
+
def compute_shared_key(private_key, public_key)
|
|
112
|
+
group = OpenSSL::PKey::EC::Group.new(CIPHER_CURVE)
|
|
113
|
+
|
|
114
|
+
private_key_bn = OpenSSL::BN.new(private_key, BN_BASE)
|
|
115
|
+
public_key_bn = OpenSSL::BN.new("02#{public_key}", BN_BASE)
|
|
116
|
+
public_key_point = OpenSSL::PKey::EC::Point.new(group, public_key_bn)
|
|
117
|
+
|
|
118
|
+
asn1 = OpenSSL::ASN1::Sequence(
|
|
119
|
+
[
|
|
120
|
+
OpenSSL::ASN1::Integer.new(1),
|
|
121
|
+
OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)),
|
|
122
|
+
OpenSSL::ASN1::ObjectId(CIPHER_CURVE, 0, :EXPLICIT),
|
|
123
|
+
OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT)
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
pkey = OpenSSL::PKey::EC.new(asn1.to_der)
|
|
128
|
+
pkey.dh_compute_key(public_key_point)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Generates a SHA256 hash of a +Nostr::Event+
|
|
132
|
+
#
|
|
133
|
+
# @api private
|
|
134
|
+
#
|
|
135
|
+
# @param event [Event] The event to be hashed
|
|
136
|
+
#
|
|
137
|
+
# @return [String] A SHA256 digest of the event
|
|
138
|
+
#
|
|
139
|
+
def hash_event(event)
|
|
140
|
+
Digest::SHA256.hexdigest(JSON.dump(event.serialize))
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/nostr/event.rb
CHANGED
|
@@ -2,7 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
module Nostr
|
|
4
4
|
# The only object type that exists in Nostr is an event. Events are immutable.
|
|
5
|
-
class Event
|
|
5
|
+
class Event
|
|
6
|
+
# 32-bytes hex-encoded public key of the event creator
|
|
7
|
+
#
|
|
8
|
+
# @api public
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# event.pubkey # => '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
|
|
12
|
+
#
|
|
13
|
+
# @return [String]
|
|
14
|
+
#
|
|
15
|
+
attr_reader :pubkey
|
|
16
|
+
|
|
17
|
+
# Date of the creation of the vent. A UNIX timestamp, in seconds
|
|
18
|
+
#
|
|
19
|
+
# @api public
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# event.created_at # => 1230981305
|
|
23
|
+
#
|
|
24
|
+
# @return [Integer]
|
|
25
|
+
#
|
|
26
|
+
attr_reader :created_at
|
|
27
|
+
|
|
28
|
+
# The kind of the event. An integer from 0 to 3
|
|
29
|
+
#
|
|
30
|
+
# @api public
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# event.kind # => 1
|
|
34
|
+
#
|
|
35
|
+
# @return [Integer]
|
|
36
|
+
#
|
|
37
|
+
attr_reader :kind
|
|
38
|
+
|
|
39
|
+
# An array of tags. Each tag is an array of strings
|
|
40
|
+
#
|
|
41
|
+
# @api public
|
|
42
|
+
#
|
|
43
|
+
# @example Tags referencing an event
|
|
44
|
+
# event.tags #=> [["e", "event_id", "relay URL"]]
|
|
45
|
+
#
|
|
46
|
+
# @example Tags referencing a key
|
|
47
|
+
# event.tags #=> [["p", "event_id", "relay URL"]]
|
|
48
|
+
#
|
|
49
|
+
# @return [Array<Array>]
|
|
50
|
+
#
|
|
51
|
+
attr_reader :tags
|
|
52
|
+
|
|
53
|
+
# An arbitrary string
|
|
54
|
+
#
|
|
55
|
+
# @api public
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# event.content # => 'Your feedback is appreciated, now pay $8'
|
|
59
|
+
#
|
|
60
|
+
# @return [String]
|
|
61
|
+
#
|
|
62
|
+
attr_reader :content
|
|
63
|
+
|
|
6
64
|
# 32-bytes sha256 of the the serialized event data.
|
|
7
65
|
# To obtain the event.id, we sha256 the serialized event. The serialization is done over the UTF-8 JSON-serialized
|
|
8
66
|
# string (with no white space or line breaks)
|
|
@@ -12,9 +70,13 @@ module Nostr
|
|
|
12
70
|
# @example Getting the event id
|
|
13
71
|
# event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
|
14
72
|
#
|
|
15
|
-
# @
|
|
73
|
+
# @example Setting the event id
|
|
74
|
+
# event.id = 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
|
75
|
+
# event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
|
76
|
+
#
|
|
77
|
+
# @return [String|nil]
|
|
16
78
|
#
|
|
17
|
-
|
|
79
|
+
attr_accessor :id
|
|
18
80
|
|
|
19
81
|
# 64-bytes signature of the sha256 hash of the serialized event data, which is
|
|
20
82
|
# the same as the "id" field
|
|
@@ -24,9 +86,13 @@ module Nostr
|
|
|
24
86
|
# @example Getting the event signature
|
|
25
87
|
# event.sig # => ''
|
|
26
88
|
#
|
|
27
|
-
# @
|
|
89
|
+
# @example Setting the event signature
|
|
90
|
+
# event.sig = '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
|
|
91
|
+
# event.sig # => '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
|
|
92
|
+
#
|
|
93
|
+
# @return [String|nil]
|
|
28
94
|
#
|
|
29
|
-
|
|
95
|
+
attr_accessor :sig
|
|
30
96
|
|
|
31
97
|
# Instantiates a new Event
|
|
32
98
|
#
|
|
@@ -44,16 +110,96 @@ module Nostr
|
|
|
44
110
|
# 937c6d6e9855477638f5655c5d89c9aa5501ea9b578a66aced4f1cd7b3'
|
|
45
111
|
# )
|
|
46
112
|
#
|
|
47
|
-
#
|
|
48
|
-
# @param
|
|
49
|
-
# @param sig [String] 64-bytes signature of the sha256 hash of the serialized event data, which is
|
|
113
|
+
# @param id [String|nil] 32-bytes sha256 of the the serialized event data.
|
|
114
|
+
# @param sig [String|nil] 64-bytes signature of the sha256 hash of the serialized event data, which is
|
|
50
115
|
# the same as the "id" field
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
116
|
+
# @param pubkey [String] 32-bytes hex-encoded public key of the event creator.
|
|
117
|
+
# @param created_at [Integer] Date of the creation of the vent. A UNIX timestamp, in seconds.
|
|
118
|
+
# @param kind [Integer] The kind of the event. An integer from 0 to 3.
|
|
119
|
+
# @param tags [Array<Array>] An array of tags. Each tag is an array of strings.
|
|
120
|
+
# @param content [String] Arbitrary string.
|
|
121
|
+
#
|
|
122
|
+
def initialize(
|
|
123
|
+
pubkey:,
|
|
124
|
+
kind:,
|
|
125
|
+
content:,
|
|
126
|
+
created_at: Time.now.to_i,
|
|
127
|
+
tags: [],
|
|
128
|
+
id: nil,
|
|
129
|
+
sig: nil
|
|
130
|
+
)
|
|
54
131
|
|
|
55
132
|
@id = id
|
|
56
133
|
@sig = sig
|
|
134
|
+
@pubkey = pubkey
|
|
135
|
+
@created_at = created_at
|
|
136
|
+
@kind = kind
|
|
137
|
+
@tags = tags
|
|
138
|
+
@content = content
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Adds a reference to an event id as an 'e' tag
|
|
142
|
+
#
|
|
143
|
+
# @api public
|
|
144
|
+
#
|
|
145
|
+
# @example Adding a reference to a pubkey
|
|
146
|
+
# event_id = '189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408'
|
|
147
|
+
# event.add_event_reference(event_id)
|
|
148
|
+
#
|
|
149
|
+
# @param event_id [String] 32-bytes hex-encoded event id.
|
|
150
|
+
#
|
|
151
|
+
# @return [Array<String>] The event's updated list of tags
|
|
152
|
+
#
|
|
153
|
+
def add_event_reference(event_id) = tags.push(['e', event_id])
|
|
154
|
+
|
|
155
|
+
# Adds a reference to a pubkey as a 'p' tag
|
|
156
|
+
#
|
|
157
|
+
# @api public
|
|
158
|
+
#
|
|
159
|
+
# @example Adding a reference to a pubkey
|
|
160
|
+
# pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
|
|
161
|
+
# event.add_pubkey_reference(pubkey)
|
|
162
|
+
#
|
|
163
|
+
# @param pubkey [String] 32-bytes hex-encoded public key.
|
|
164
|
+
#
|
|
165
|
+
# @return [Array<String>] The event's updated list of tags
|
|
166
|
+
#
|
|
167
|
+
def add_pubkey_reference(pubkey) = tags.push(['p', pubkey])
|
|
168
|
+
|
|
169
|
+
# Signs an event with the user's private key
|
|
170
|
+
#
|
|
171
|
+
# @api public
|
|
172
|
+
#
|
|
173
|
+
# @example Signing an event
|
|
174
|
+
# event.sign(private_key)
|
|
175
|
+
#
|
|
176
|
+
# @param private_key [String] 32-bytes hex-encoded private key.
|
|
177
|
+
#
|
|
178
|
+
# @return [Event] A signed event.
|
|
179
|
+
#
|
|
180
|
+
def sign(private_key)
|
|
181
|
+
crypto = Crypto.new
|
|
182
|
+
crypto.sign_event(self, private_key)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Serializes the event, to obtain a SHA256 digest of it
|
|
186
|
+
#
|
|
187
|
+
# @api public
|
|
188
|
+
#
|
|
189
|
+
# @example Converting the event to a digest
|
|
190
|
+
# event.serialize
|
|
191
|
+
#
|
|
192
|
+
# @return [Array] The event as an array.
|
|
193
|
+
#
|
|
194
|
+
def serialize
|
|
195
|
+
[
|
|
196
|
+
0,
|
|
197
|
+
pubkey,
|
|
198
|
+
created_at,
|
|
199
|
+
kind,
|
|
200
|
+
tags,
|
|
201
|
+
content
|
|
202
|
+
]
|
|
57
203
|
end
|
|
58
204
|
|
|
59
205
|
# Converts the event to a hash
|
data/lib/nostr/event_kind.rb
CHANGED
|
@@ -31,5 +31,13 @@ module Nostr
|
|
|
31
31
|
# @return [Integer]
|
|
32
32
|
#
|
|
33
33
|
CONTACT_LIST = 3
|
|
34
|
+
|
|
35
|
+
# A special event with kind 4, meaning "encrypted direct message". An event of this kind has its +content+
|
|
36
|
+
# equal to the base64-encoded, aes-256-cbc encrypted string of anything a user wants to write, encrypted using a
|
|
37
|
+
# shared cipher generated by combining the recipient's public-key with the sender's private-key.
|
|
38
|
+
#
|
|
39
|
+
# @return [Integer]
|
|
40
|
+
#
|
|
41
|
+
ENCRYPTED_DIRECT_MESSAGE = 4
|
|
34
42
|
end
|
|
35
43
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nostr
|
|
4
|
+
# Classes of event kinds.
|
|
5
|
+
module Events
|
|
6
|
+
# An event whose +content+ is encrypted. It can only be decrypted by the owner of the private key that pairs
|
|
7
|
+
# the event's +pubkey+.
|
|
8
|
+
class EncryptedDirectMessage < Event
|
|
9
|
+
# Instantiates a new encrypted direct message
|
|
10
|
+
#
|
|
11
|
+
# @api public
|
|
12
|
+
#
|
|
13
|
+
# @example Instantiating a new encrypted direct message
|
|
14
|
+
# Nostr::Events::EncryptedDirectMessage.new(
|
|
15
|
+
# sender_private_key: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
|
16
|
+
# recipient_public_key: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
|
|
17
|
+
# plain_text: 'Your feedback is appreciated, now pay $8',
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @example Instantiating a new encrypted direct message that references a previous direct message
|
|
21
|
+
# Nostr::Events::EncryptedDirectMessage.new(
|
|
22
|
+
# sender_private_key: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
|
23
|
+
# recipient_public_key: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
|
|
24
|
+
# plain_text: 'Your feedback is appreciated, now pay $8',
|
|
25
|
+
# previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# @param plain_text [String] The +content+ of the encrypted message.
|
|
29
|
+
# @param sender_private_key [String] 32-bytes hex-encoded private key of the message's author.
|
|
30
|
+
# @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient of the encrypted message.
|
|
31
|
+
# @param previous_direct_message [String] 32-bytes hex-encoded id identifying the previous message in a
|
|
32
|
+
# conversation or a message we are explicitly replying to (such that contextual, more organized conversations
|
|
33
|
+
# may happen
|
|
34
|
+
#
|
|
35
|
+
def initialize(plain_text:, sender_private_key:, recipient_public_key:, previous_direct_message: nil)
|
|
36
|
+
crypto = Crypto.new
|
|
37
|
+
keygen = Keygen.new
|
|
38
|
+
|
|
39
|
+
encrypted_content = crypto.encrypt_text(sender_private_key, recipient_public_key, plain_text)
|
|
40
|
+
sender_public_key = keygen.extract_public_key(sender_private_key)
|
|
41
|
+
|
|
42
|
+
super(
|
|
43
|
+
pubkey: sender_public_key,
|
|
44
|
+
kind: Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE,
|
|
45
|
+
content: encrypted_content,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
add_pubkey_reference(recipient_public_key)
|
|
49
|
+
add_event_reference(previous_direct_message) if previous_direct_message
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/nostr/keygen.rb
CHANGED
|
@@ -62,7 +62,7 @@ module Nostr
|
|
|
62
62
|
# @return [String] A 32-bytes hex-encoded public key.
|
|
63
63
|
#
|
|
64
64
|
def extract_public_key(private_key)
|
|
65
|
-
group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16)
|
|
65
|
+
group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
private
|
data/lib/nostr/user.rb
CHANGED
|
@@ -57,36 +57,8 @@ module Nostr
|
|
|
57
57
|
# @return [Event]
|
|
58
58
|
#
|
|
59
59
|
def create_event(event_attributes)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
signature = sign(event_sha256)
|
|
64
|
-
|
|
65
|
-
Event.new(
|
|
66
|
-
id: event_sha256,
|
|
67
|
-
pubkey: event_fragment.pubkey,
|
|
68
|
-
created_at: event_fragment.created_at,
|
|
69
|
-
kind: event_fragment.kind,
|
|
70
|
-
tags: event_fragment.tags,
|
|
71
|
-
content: event_fragment.content,
|
|
72
|
-
sig: signature
|
|
73
|
-
)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private
|
|
77
|
-
|
|
78
|
-
# Signs an event with the user's private key
|
|
79
|
-
#
|
|
80
|
-
# @api private
|
|
81
|
-
#
|
|
82
|
-
# @param event_sha256 [String] The SHA256 hash of the event.
|
|
83
|
-
#
|
|
84
|
-
# @return [String] The signature of the event.
|
|
85
|
-
#
|
|
86
|
-
def sign(event_sha256)
|
|
87
|
-
hex_private_key = Array(keypair.private_key).pack('H*')
|
|
88
|
-
hex_message = Array(event_sha256).pack('H*')
|
|
89
|
-
Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
|
|
60
|
+
event = Event.new(**event_attributes.merge(pubkey: keypair.public_key))
|
|
61
|
+
event.sign(keypair.private_key)
|
|
90
62
|
end
|
|
91
63
|
end
|
|
92
64
|
end
|
data/lib/nostr/version.rb
CHANGED
data/lib/nostr.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'nostr/crypto'
|
|
3
4
|
require_relative 'nostr/version'
|
|
4
5
|
require_relative 'nostr/keygen'
|
|
5
6
|
require_relative 'nostr/client_message_type'
|
|
@@ -8,8 +9,8 @@ require_relative 'nostr/subscription'
|
|
|
8
9
|
require_relative 'nostr/relay'
|
|
9
10
|
require_relative 'nostr/key_pair'
|
|
10
11
|
require_relative 'nostr/event_kind'
|
|
11
|
-
require_relative 'nostr/event_fragment'
|
|
12
12
|
require_relative 'nostr/event'
|
|
13
|
+
require_relative 'nostr/events/encrypted_direct_message'
|
|
13
14
|
require_relative 'nostr/client'
|
|
14
15
|
require_relative 'nostr/user'
|
|
15
16
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Nostr
|
|
2
|
+
class Crypto
|
|
3
|
+
BN_BASE: Integer
|
|
4
|
+
CIPHER_CURVE: String
|
|
5
|
+
CIPHER_ALGORITHM: String
|
|
6
|
+
|
|
7
|
+
def encrypt_text: (String, String, String) -> String
|
|
8
|
+
def decrypt_text: (String, String, String) -> String
|
|
9
|
+
def sign_event: (Event, String) -> Event
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def compute_shared_key: (String, String) -> String
|
|
14
|
+
def hash_event:(Event) -> String
|
|
15
|
+
end
|
|
16
|
+
end
|
data/sig/nostr/event.rbs
CHANGED
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
module Nostr
|
|
2
|
-
class Event
|
|
3
|
-
attr_reader
|
|
4
|
-
attr_reader
|
|
2
|
+
class Event
|
|
3
|
+
attr_reader pubkey: String
|
|
4
|
+
attr_reader created_at: Integer
|
|
5
|
+
attr_reader kind: Integer
|
|
6
|
+
attr_reader tags: Array[Array[String]]
|
|
7
|
+
attr_reader content: String
|
|
8
|
+
attr_accessor id: String?|nil
|
|
9
|
+
attr_accessor sig: String?|nil
|
|
5
10
|
|
|
6
|
-
def initialize: (
|
|
7
|
-
|
|
11
|
+
def initialize: (
|
|
12
|
+
pubkey: String,
|
|
8
13
|
kind: Integer,
|
|
9
|
-
tags: Array[String],
|
|
10
14
|
content: String,
|
|
15
|
+
?created_at: Integer,
|
|
16
|
+
?tags: Array[Array[String]],
|
|
17
|
+
?id: String|nil,
|
|
18
|
+
?sig: String|nil
|
|
11
19
|
) -> void
|
|
12
20
|
|
|
21
|
+
def serialize: -> [Integer, String, Integer, Integer, Array[Array[String]], String]
|
|
22
|
+
|
|
13
23
|
def to_h: -> {
|
|
14
|
-
id: String,
|
|
24
|
+
id: String?|nil,
|
|
15
25
|
pubkey: String,
|
|
16
26
|
created_at: Integer,
|
|
17
27
|
kind: Integer,
|
|
18
|
-
tags: Array[String],
|
|
28
|
+
tags: Array[Array[String]],
|
|
19
29
|
content: String,
|
|
20
|
-
sig: String
|
|
30
|
+
sig: String?|nil
|
|
21
31
|
}
|
|
22
32
|
def ==: (Event other) -> bool
|
|
33
|
+
|
|
34
|
+
def sign:(String) -> Event
|
|
35
|
+
|
|
36
|
+
def add_event_reference: (String) -> Array[Array[String]]
|
|
37
|
+
def add_pubkey_reference: (String) -> Array[Array[String]]
|
|
23
38
|
end
|
|
24
39
|
end
|
data/sig/nostr/event_kind.rbs
CHANGED
data/sig/nostr/user.rbs
CHANGED
|
@@ -6,14 +6,12 @@ module Nostr
|
|
|
6
6
|
def initialize: (?keypair: KeyPair | nil, ?keygen: Keygen) -> void
|
|
7
7
|
def create_event: (
|
|
8
8
|
{
|
|
9
|
-
id: String,
|
|
10
9
|
pubkey: String,
|
|
11
10
|
created_at: Integer,
|
|
12
11
|
kind: Integer,
|
|
13
12
|
tags: Array[String],
|
|
14
13
|
content: String,
|
|
15
14
|
created_at: Integer,
|
|
16
|
-
sig: String
|
|
17
15
|
}
|
|
18
16
|
) -> Event
|
|
19
17
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nostr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wilson Silva
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-02-
|
|
11
|
+
date: 2023-02-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bech32
|
|
@@ -455,9 +455,10 @@ files:
|
|
|
455
455
|
- lib/nostr.rb
|
|
456
456
|
- lib/nostr/client.rb
|
|
457
457
|
- lib/nostr/client_message_type.rb
|
|
458
|
+
- lib/nostr/crypto.rb
|
|
458
459
|
- lib/nostr/event.rb
|
|
459
|
-
- lib/nostr/event_fragment.rb
|
|
460
460
|
- lib/nostr/event_kind.rb
|
|
461
|
+
- lib/nostr/events/encrypted_direct_message.rb
|
|
461
462
|
- lib/nostr/filter.rb
|
|
462
463
|
- lib/nostr/key_pair.rb
|
|
463
464
|
- lib/nostr/keygen.rb
|
|
@@ -469,9 +470,10 @@ files:
|
|
|
469
470
|
- sig/nostr.rbs
|
|
470
471
|
- sig/nostr/client.rbs
|
|
471
472
|
- sig/nostr/client_message_type.rbs
|
|
473
|
+
- sig/nostr/crypto.rbs
|
|
472
474
|
- sig/nostr/event.rbs
|
|
473
|
-
- sig/nostr/event_fragment.rbs
|
|
474
475
|
- sig/nostr/event_kind.rbs
|
|
476
|
+
- sig/nostr/events/encrypted_direct_message.rbs
|
|
475
477
|
- sig/nostr/filter.rbs
|
|
476
478
|
- sig/nostr/key_pair.rbs
|
|
477
479
|
- sig/nostr/keygen.rbs
|
|
@@ -506,7 +508,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
506
508
|
- !ruby/object:Gem::Version
|
|
507
509
|
version: '0'
|
|
508
510
|
requirements: []
|
|
509
|
-
rubygems_version: 3.4.
|
|
511
|
+
rubygems_version: 3.4.7
|
|
510
512
|
signing_key:
|
|
511
513
|
specification_version: 4
|
|
512
514
|
summary: Client and relay implementation of the Nostr protocol.
|
data/lib/nostr/event_fragment.rb
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nostr
|
|
4
|
-
# Part of an +Event+. A complete +Event+ must have an +id+ and a +sig+.
|
|
5
|
-
class EventFragment
|
|
6
|
-
# 32-bytes hex-encoded public key of the event creator
|
|
7
|
-
#
|
|
8
|
-
# @api public
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# event.pubkey # => '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
|
|
12
|
-
#
|
|
13
|
-
# @return [String]
|
|
14
|
-
#
|
|
15
|
-
attr_reader :pubkey
|
|
16
|
-
|
|
17
|
-
# Date of the creation of the vent. A UNIX timestamp, in seconds
|
|
18
|
-
#
|
|
19
|
-
# @api public
|
|
20
|
-
#
|
|
21
|
-
# @example
|
|
22
|
-
# event.created_at # => 1230981305
|
|
23
|
-
#
|
|
24
|
-
# @return [Integer]
|
|
25
|
-
#
|
|
26
|
-
attr_reader :created_at
|
|
27
|
-
|
|
28
|
-
# The kind of the event. An integer from 0 to 3
|
|
29
|
-
#
|
|
30
|
-
# @api public
|
|
31
|
-
#
|
|
32
|
-
# @example
|
|
33
|
-
# event.kind # => 1
|
|
34
|
-
#
|
|
35
|
-
# @return [Integer]
|
|
36
|
-
#
|
|
37
|
-
attr_reader :kind
|
|
38
|
-
|
|
39
|
-
# An array of tags. Each tag is an array of strings
|
|
40
|
-
#
|
|
41
|
-
# @api public
|
|
42
|
-
#
|
|
43
|
-
# @example Tags referencing an event
|
|
44
|
-
# event.tags #=> [["e", "event_id", "relay URL"]]
|
|
45
|
-
#
|
|
46
|
-
# @example Tags referencing a key
|
|
47
|
-
# event.tags #=> [["p", "event_id", "relay URL"]]
|
|
48
|
-
#
|
|
49
|
-
# @return [Array<Array>]
|
|
50
|
-
#
|
|
51
|
-
attr_reader :tags
|
|
52
|
-
|
|
53
|
-
# An arbitrary string
|
|
54
|
-
#
|
|
55
|
-
# @api public
|
|
56
|
-
#
|
|
57
|
-
# @example
|
|
58
|
-
# event.content # => 'Your feedback is appreciated, now pay $8'
|
|
59
|
-
#
|
|
60
|
-
# @return [String]
|
|
61
|
-
#
|
|
62
|
-
attr_reader :content
|
|
63
|
-
|
|
64
|
-
# Instantiates a new EventFragment
|
|
65
|
-
#
|
|
66
|
-
# @api public
|
|
67
|
-
#
|
|
68
|
-
# @example
|
|
69
|
-
# Nostr::EventFragment.new(
|
|
70
|
-
# pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
|
71
|
-
# created_at: 1230981305,
|
|
72
|
-
# kind: 1,
|
|
73
|
-
# tags: [['e', '189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408']],
|
|
74
|
-
# content: 'Your feedback is appreciated, now pay $8'
|
|
75
|
-
# )
|
|
76
|
-
#
|
|
77
|
-
# @param pubkey [String] 32-bytes hex-encoded public key of the event creator.
|
|
78
|
-
# @param created_at [Integer] Date of the creation of the vent. A UNIX timestamp, in seconds.
|
|
79
|
-
# @param kind [Integer] The kind of the event. An integer from 0 to 3.
|
|
80
|
-
# @param tags [Array<Array>] An array of tags. Each tag is an array of strings.
|
|
81
|
-
# @param content [String] Arbitrary string.
|
|
82
|
-
#
|
|
83
|
-
def initialize(pubkey:, kind:, content:, created_at: Time.now.to_i, tags: [])
|
|
84
|
-
@pubkey = pubkey
|
|
85
|
-
@created_at = created_at
|
|
86
|
-
@kind = kind
|
|
87
|
-
@tags = tags
|
|
88
|
-
@content = content
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Serializes the event fragment, to obtain a SHA256 hash of it
|
|
92
|
-
#
|
|
93
|
-
# @api public
|
|
94
|
-
#
|
|
95
|
-
# @example Converting the event to a hash
|
|
96
|
-
# event_fragment.serialize
|
|
97
|
-
#
|
|
98
|
-
# @return [Array] The event fragment as an array.
|
|
99
|
-
#
|
|
100
|
-
def serialize
|
|
101
|
-
[
|
|
102
|
-
0,
|
|
103
|
-
pubkey,
|
|
104
|
-
created_at,
|
|
105
|
-
kind,
|
|
106
|
-
tags,
|
|
107
|
-
content
|
|
108
|
-
]
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
module Nostr
|
|
2
|
-
class EventFragment
|
|
3
|
-
attr_reader pubkey: String
|
|
4
|
-
attr_reader created_at: Integer
|
|
5
|
-
attr_reader kind: Integer
|
|
6
|
-
attr_reader tags: Array[String]
|
|
7
|
-
attr_reader content: String
|
|
8
|
-
|
|
9
|
-
def initialize: (pubkey: String, kind: Integer, content: String, ?created_at: Integer, ?tags: Array[String]) -> void
|
|
10
|
-
def serialize: -> [Integer, String, Integer, Integer, Array[String], String]
|
|
11
|
-
end
|
|
12
|
-
end
|