nostr 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90770e269f0c966f76013797314b9e3b4b0743fa57f5b84f2460371b413bc5f1
4
- data.tar.gz: 7a4a908a6c1b086781d26dc6f45c289a85859e12e1e854451a0e9f8e75f5fa06
3
+ metadata.gz: cd56d59d68235fe8fa4bceb67df4a7e83d4b6a8295ed31cc0152002800ad7d23
4
+ data.tar.gz: 56b70ada7f9fd6cd29be1c7bc23b7a23adc49bfe2f8d8de48300f70709626a3e
5
5
  SHA512:
6
- metadata.gz: 35a4401f078587bed79d6063f4051cca3f8dc170c074c552a202fee7872b75668eb8204a5f6db09f810ed0c5bb84026438d7e942f48a4825d63ba64552d032c7
7
- data.tar.gz: b9400efc3228dea17e23e40803e2e7b94ce9966e6a729fcf5ea0b79ffcea07b7e51f7fd9703d01af1d287ffdaf85b6b8d49d325813b8e32518a0be240537e86e
6
+ metadata.gz: be113a67cd651739cc3bac1deb4bba30e3598cfd787e8971a764e82d58c03ad1c918d3edd14630bbb1a5e2c6504c3c344859f0449a1774df7bb7bf0d3c38f537
7
+ data.tar.gz: c80bbf1f4d3caa25f48e1630b650587a2319197d7edba33e824b17e93f6355d8593c41364ad1fdb86a834d8cae63ad9f15e8feacae953b0ff8d7294f09c2c765
data/.rubocop.yml CHANGED
@@ -31,6 +31,9 @@ Metrics/BlockLength:
31
31
  - '**/*_spec.rb'
32
32
  - nostr.gemspec
33
33
 
34
+ Metrics/ParameterLists:
35
+ CountKeywordArgs: false
36
+
34
37
  # ----------------------- RSpec -----------------------
35
38
 
36
39
  RSpec/ExampleLength:
data/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ 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
+
30
+ ## [0.3.0] - 2023-02-15
31
+
32
+ ### Added
33
+
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
41
+
7
42
  ## [0.2.0] - 2023-01-12
8
43
 
9
44
  ### Added
@@ -14,5 +49,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
14
49
 
15
50
  - Initial release
16
51
 
52
+ [0.3.0]: https://github.com/wilsonsilva/nostr/compare/v0.2.0...v0.3.0
17
53
  [0.2.0]: https://github.com/wilsonsilva/nostr/compare/v0.1.0...v0.2.0
18
54
  [0.1.0]: https://github.com/wilsonsilva/nostr/compare/7fded5...v0.1.0
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:
@@ -27,12 +48,11 @@ All examples below assume that the gem has been required.
27
48
  require 'nostr'
28
49
  ```
29
50
 
30
-
31
51
  ### Generating a keypair
32
52
 
33
53
  ```ruby
34
54
  keygen = Nostr::Keygen.new
35
- keypair = keygen.generate_keypair
55
+ keypair = keygen.generate_key_pair
36
56
 
37
57
  keypair.private_key
38
58
  keypair.public_key
@@ -55,7 +75,7 @@ You may instantiate multiple Clients and multiple Relays.
55
75
 
56
76
  ```ruby
57
77
  client = Nostr::Client.new
58
- relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
78
+ relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
59
79
 
60
80
  client.connect(relay)
61
81
  ```
@@ -82,14 +102,28 @@ end
82
102
  # > Network error: wss://rsslay.fiatjaf.com: Unable to verify the server certificate for 'rsslay.fiatjaf.com'
83
103
  ```
84
104
 
85
- The `:message` event is fired whenwhen data is received through a WebSocket.
105
+ The `:message` event is fired when data is received through a WebSocket.
86
106
 
87
107
  ```ruby
88
108
  client.on :message do |message|
89
109
  puts message
90
110
  end
91
111
 
92
- # > Network error: wss://rsslay.fiatjaf.com: Unable to verify the server certificate for 'rsslay.fiatjaf.com'
112
+ # [
113
+ # "EVENT",
114
+ # "d34107357089bfc9882146d3bfab0386",
115
+ # {
116
+ # "content":"",
117
+ # "created_at":1676456512,
118
+ # "id":"18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
119
+ # "kind":3,
120
+ # "pubkey":"117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
121
+ # "sig":"d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
122
+ # "tags":[
123
+ # ["p","1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
124
+ # ]
125
+ # }
126
+ # ]
93
127
  ```
94
128
 
95
129
  The `:close` event is fired when a connection with a WebSocket is closed.
@@ -179,10 +213,66 @@ event = user.create_event(
179
213
  client.publish(event)
180
214
  ```
181
215
 
182
- ## NIPS
216
+ ### Creating/updating your contact list
217
+
218
+ Every new contact list that gets published overwrites the past ones, so it should contain all entries.
219
+
220
+ ```ruby
221
+ # Creating a contact list event with 2 contacts
222
+ update_contacts_event = user.create_event(
223
+ kind: Nostr::EventKind::CONTACT_LIST,
224
+ tags: [
225
+ [
226
+ "p", # mandatory
227
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
228
+ "wss://alicerelay.com/", # can be an empty string or can be omitted
229
+ "alice" # can be an empty string or can be omitted
230
+ ],
231
+ [
232
+ "p", # mandatory
233
+ "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
234
+ "wss://bobrelay.com/nostr", # can be an empty string or can be omitted
235
+ "bob" # can be an empty string or can be omitted
236
+ ],
237
+ ],
238
+ )
239
+
240
+ # Send it to the Relay
241
+ client.publish(update_contacts_event)
242
+ ```
243
+
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
183
272
 
184
273
  - [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md)
185
- - [ ] [NIP-01 - Relay](https://github.com/nostr-protocol/nips/blob/master/01.md)
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)
186
276
 
187
277
  ## Development
188
278
 
@@ -200,17 +290,34 @@ The health and maintainability of the codebase is ensured through a set of
200
290
  Rake tasks to test, lint and audit the gem for security vulnerabilities and documentation:
201
291
 
202
292
  ```
203
- rake bundle:audit # Checks for vulnerable versions of gems
204
- rake qa # Test, lint and perform security and documentation audits
205
- rake rubocop # Lint the codebase with RuboCop
206
- rake rubocop:auto_correct # Auto-correct RuboCop offenses
207
- rake spec # Run RSpec code examples
208
- rake verify_measurements # Verify that yardstick coverage is at least 100%
209
- rake yard # Generate YARD Documentation
210
- rake yard:junk # Check the junk in your YARD Documentation
211
- rake yardstick_measure # Measure docs in lib/**/*.rb with yardstick
293
+ rake build # Build nostr.gem into the pkg directory
294
+ rake build:checksum # Generate SHA512 checksum if nostr.gem into the checksums directory
295
+ rake bundle:audit:check # Checks the Gemfile.lock for insecure dependencies
296
+ rake bundle:audit:update # Updates the bundler-audit vulnerability database
297
+ rake clean # Remove any temporary products
298
+ rake clobber # Remove any generated files
299
+ rake coverage # Run spec with coverage
300
+ rake install # Build and install nostr.gem into system gems
301
+ rake install:local # Build and install nostr.gem into system gems without network access
302
+ rake qa # Test, lint and perform security and documentation audits
303
+ rake release[remote] # Create a tag, build and push nostr.gem to rubygems.org
304
+ rake rubocop # Run RuboCop
305
+ rake rubocop:autocorrect # Autocorrect RuboCop offenses (only when it's safe)
306
+ rake rubocop:autocorrect_all # Autocorrect RuboCop offenses (safe and unsafe)
307
+ rake spec # Run RSpec code examples
308
+ rake verify_measurements # Verify that yardstick coverage is at least 100%
309
+ rake yard # Generate YARD Documentation
310
+ rake yard:junk # Check the junk in your YARD Documentation
311
+ rake yardstick_measure # Measure docs in lib/**/*.rb with yardstick
212
312
  ```
213
313
 
314
+ ### Type checking
315
+
316
+ This gem leverages [RBS](https://github.com/ruby/rbs), a language to describe the structure of Ruby programs. It is
317
+ used to provide type checking and autocompletion in your editor. Run `bundle exec typeprof FILENAME` to generate
318
+ an RBS definition for the given Ruby file. And validate all definitions using [Steep](https://github.com/soutaro/steep)
319
+ with the command `bundle exec steep check`.
320
+
214
321
  ## Contributing
215
322
 
216
323
  Bug reports and pull requests are welcome on GitHub at https://github.com/wilsonsilva/nostr.
data/Steepfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ target :lib do
4
+ signature 'sig'
5
+
6
+ check 'lib'
7
+
8
+ # Core libraries
9
+ library 'base64'
10
+ library 'digest'
11
+ library 'openssl'
12
+ library 'securerandom'
13
+
14
+ # Gems
15
+ library 'json'
16
+ end
@@ -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 < EventFragment
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
- # @return [String]
73
+ # @example Setting the event id
74
+ # event.id = 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
75
+ # event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
76
+ #
77
+ # @return [String|nil]
16
78
  #
17
- attr_reader :id
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
- # @return [String]
89
+ # @example Setting the event signature
90
+ # event.sig = '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
91
+ # event.sig # => '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
92
+ #
93
+ # @return [String|nil]
28
94
  #
29
- attr_reader :sig
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 id [String] 32-bytes sha256 of the the serialized event data.
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
- def initialize(id:, sig:, **kwargs)
53
- super(**kwargs)
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
@@ -24,5 +24,20 @@ module Nostr
24
24
  # @return [Integer]
25
25
  #
26
26
  RECOMMEND_SERVER = 2
27
+
28
+ # A special event with kind 3, meaning "contact list" is defined as having a list of p tags, one for each of
29
+ # the followed/known profiles one is following.
30
+ #
31
+ # @return [Integer]
32
+ #
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
27
42
  end
28
43
  end