nostr 0.3.0 → 0.5.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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +1 -1
  3. data/.rubocop.yml +26 -0
  4. data/.tool-versions +2 -1
  5. data/CHANGELOG.md +65 -1
  6. data/README.md +96 -183
  7. data/Steepfile +2 -0
  8. data/docs/.gitignore +4 -0
  9. data/docs/.vitepress/config.mjs +112 -0
  10. data/docs/README.md +44 -0
  11. data/docs/api-examples.md +49 -0
  12. data/docs/bun.lockb +0 -0
  13. data/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md +190 -0
  14. data/docs/core/client.md +108 -0
  15. data/docs/core/keys.md +136 -0
  16. data/docs/core/user.md +43 -0
  17. data/docs/events/contact-list.md +29 -0
  18. data/docs/events/encrypted-direct-message.md +28 -0
  19. data/docs/events/recommend-server.md +32 -0
  20. data/docs/events/set-metadata.md +20 -0
  21. data/docs/events/text-note.md +15 -0
  22. data/docs/events.md +11 -0
  23. data/docs/getting-started/installation.md +21 -0
  24. data/docs/getting-started/overview.md +170 -0
  25. data/docs/implemented-nips.md +9 -0
  26. data/docs/index.md +44 -0
  27. data/docs/markdown-examples.md +85 -0
  28. data/docs/package.json +12 -0
  29. data/docs/relays/connecting-to-a-relay.md +21 -0
  30. data/docs/relays/publishing-events.md +29 -0
  31. data/docs/relays/receiving-events.md +6 -0
  32. data/docs/subscriptions/creating-a-subscription.md +49 -0
  33. data/docs/subscriptions/deleting-a-subscription.md +10 -0
  34. data/docs/subscriptions/filtering-subscription-events.md +115 -0
  35. data/docs/subscriptions/updating-a-subscription.md +4 -0
  36. data/lib/nostr/bech32.rb +203 -0
  37. data/lib/nostr/client.rb +2 -1
  38. data/lib/nostr/crypto.rb +147 -0
  39. data/lib/nostr/errors/error.rb +7 -0
  40. data/lib/nostr/errors/invalid_hrp_error.rb +21 -0
  41. data/lib/nostr/errors/invalid_key_format_error.rb +20 -0
  42. data/lib/nostr/errors/invalid_key_length_error.rb +20 -0
  43. data/lib/nostr/errors/invalid_key_type_error.rb +18 -0
  44. data/lib/nostr/errors/key_validation_error.rb +6 -0
  45. data/lib/nostr/errors.rb +8 -0
  46. data/lib/nostr/event.rb +157 -12
  47. data/lib/nostr/event_kind.rb +8 -0
  48. data/lib/nostr/events/encrypted_direct_message.rb +54 -0
  49. data/lib/nostr/filter.rb +4 -4
  50. data/lib/nostr/key.rb +100 -0
  51. data/lib/nostr/key_pair.rb +30 -6
  52. data/lib/nostr/keygen.rb +43 -4
  53. data/lib/nostr/private_key.rb +36 -0
  54. data/lib/nostr/public_key.rb +36 -0
  55. data/lib/nostr/relay_message_type.rb +18 -0
  56. data/lib/nostr/subscription.rb +2 -2
  57. data/lib/nostr/user.rb +17 -36
  58. data/lib/nostr/version.rb +1 -1
  59. data/lib/nostr.rb +8 -1
  60. data/nostr.gemspec +9 -9
  61. data/sig/nostr/bech32.rbs +14 -0
  62. data/sig/nostr/client.rbs +5 -5
  63. data/sig/nostr/crypto.rbs +16 -0
  64. data/sig/nostr/errors/error.rbs +4 -0
  65. data/sig/nostr/errors/invalid_hrb_error.rbs +6 -0
  66. data/sig/nostr/errors/invalid_key_format_error.rbs +5 -0
  67. data/sig/nostr/errors/invalid_key_length_error.rbs +5 -0
  68. data/sig/nostr/errors/invalid_key_type_error.rbs +5 -0
  69. data/sig/nostr/errors/key_validation_error.rbs +4 -0
  70. data/sig/nostr/event.rbs +24 -9
  71. data/sig/nostr/event_kind.rbs +1 -0
  72. data/sig/nostr/events/encrypted_direct_message.rbs +12 -0
  73. data/sig/nostr/filter.rbs +3 -12
  74. data/sig/nostr/key.rbs +16 -0
  75. data/sig/nostr/key_pair.rbs +7 -3
  76. data/sig/nostr/keygen.rbs +5 -2
  77. data/sig/nostr/private_key.rbs +4 -0
  78. data/sig/nostr/public_key.rbs +4 -0
  79. data/sig/nostr/relay_message_type.rbs +8 -0
  80. data/sig/nostr/user.rbs +4 -10
  81. data/sig/vendor/bech32/nostr/entity.rbs +41 -0
  82. data/sig/vendor/bech32/nostr/nip19.rbs +20 -0
  83. data/sig/vendor/bech32/segwit_addr.rbs +21 -0
  84. data/sig/vendor/bech32.rbs +25 -0
  85. data/sig/vendor/event_emitter.rbs +10 -3
  86. data/sig/vendor/event_machine/channel.rbs +1 -1
  87. data/sig/vendor/faye/websocket/api.rbs +45 -0
  88. data/sig/vendor/faye/websocket/client.rbs +43 -0
  89. data/sig/vendor/faye/websocket.rbs +30 -0
  90. metadata +83 -23
  91. data/lib/nostr/event_fragment.rb +0 -111
  92. data/sig/nostr/event_fragment.rbs +0 -12
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bech32'
4
+ require 'bech32/nostr'
5
+ require 'bech32/nostr/entity'
6
+
7
+ module Nostr
8
+ # Bech32 encoding and decoding
9
+ #
10
+ # @api public
11
+ #
12
+ module Bech32
13
+ # Decodes a bech32-encoded string
14
+ #
15
+ # @api public
16
+ #
17
+ # @example
18
+ # bech32_value = 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
19
+ # Nostr::Bech32.decode(bech32_value) # => ['npub', '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d8...']
20
+ #
21
+ # @param [String] bech32_value The bech32-encoded string to decode
22
+ #
23
+ # @return [Array<String, String>] The human readable part and the data
24
+ #
25
+ def self.decode(bech32_value)
26
+ entity = ::Bech32::Nostr::NIP19.decode(bech32_value)
27
+
28
+ case entity
29
+ in ::Bech32::Nostr::BareEntity
30
+ [entity.hrp, entity.data]
31
+ in ::Bech32::Nostr::TLVEntity
32
+ [entity.hrp, entity.entries]
33
+ end
34
+ end
35
+
36
+ # Encodes data into a bech32 string
37
+ #
38
+ # @api public
39
+ #
40
+ # @example
41
+ # Nostr::Bech32.encode(hrp: 'npub', data: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
42
+ # # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
43
+ #
44
+ # @param [String] hrp The human readable part (npub, nsec, nprofile, nrelay, nevent, naddr, etc)
45
+ # @param [String] data The data to encode
46
+ #
47
+ # @return [String] The bech32-encoded string
48
+ #
49
+ def self.encode(hrp:, data:)
50
+ ::Bech32::Nostr::BareEntity.new(hrp, data).encode
51
+ end
52
+
53
+ # Encodes a hex-encoded public key into a bech32 string
54
+ #
55
+ # @api public
56
+ #
57
+ # @example
58
+ # Nostr::Bech32.npub_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
59
+ # # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
60
+ #
61
+ # @param [String] npub The public key to encode
62
+ #
63
+ # @see Nostr::Bech32#encode
64
+ # @see Nostr::PublicKey#to_bech32
65
+ # @see Nostr::PrivateKey#to_bech32
66
+ #
67
+ # @return [String] The bech32-encoded string
68
+ #
69
+ def self.npub_encode(npub)
70
+ encode(hrp: 'npub', data: npub)
71
+ end
72
+
73
+ # Encodes a hex-encoded private key into a bech32 string
74
+ #
75
+ # @api public
76
+ #
77
+ # @example
78
+ # Nostr::Bech32.nsec_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
79
+ # # => 'nsec10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
80
+ #
81
+ # @param [String] nsec The private key to encode
82
+ #
83
+ # @see Nostr::Bech32#encode
84
+ # @see Nostr::PrivateKey#to_bech32
85
+ # @see Nostr::PublicKey#to_bech32
86
+ #
87
+ # @return [String] The bech32-encoded string
88
+ #
89
+ def self.nsec_encode(nsec)
90
+ encode(hrp: 'nsec', data: nsec)
91
+ end
92
+
93
+ # Encodes an address into a bech32 string
94
+ #
95
+ # @api public
96
+ #
97
+ # @example
98
+ # naddr = Nostr::Bech32.naddr_encode(
99
+ # pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
100
+ # relays: ['wss://relay.damus.io', 'wss://nos.lol'],
101
+ # kind: Nostr::EventKind::TEXT_NOTE,
102
+ # identifier: 'damus'
103
+ # )
104
+ # naddr # => 'naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7ns...'
105
+ #
106
+ # @param [PublicKey] pubkey The public key to encode
107
+ # @param [Array<String>] relays The relays to encode
108
+ # @param [String] kind The kind of address to encode
109
+ # @param [String] identifier The identifier of the address to encode
110
+ #
111
+ # @return [String] The bech32-encoded string
112
+ #
113
+ def self.naddr_encode(pubkey:, relays: [], kind: nil, identifier: nil)
114
+ entry_relays = relays.map do |relay_url|
115
+ ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
116
+ end
117
+
118
+ pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, pubkey)
119
+ kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
120
+ identifier_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, identifier)
121
+
122
+ entries = [pubkey_entry, *entry_relays, kind_entry, identifier_entry].compact
123
+ entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT_COORDINATE, entries)
124
+ entity.encode
125
+ end
126
+
127
+ # Encodes an event into a bech32 string
128
+ #
129
+ # @api public
130
+ #
131
+ # @example
132
+ # nevent = Nostr::Bech32.nevent_encode(
133
+ # id: '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3',
134
+ # relays: ['wss://relay.damus.io', 'wss://nos.lol'],
135
+ # kind: Nostr::EventKind::TEXT_NOTE,
136
+ # )
137
+ # nevent # => 'nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0pra...'
138
+ #
139
+ # @param [PublicKey] id The id the event to encode
140
+ # @param [Array<String>] relays The relays to encode
141
+ # @param [String] kind The kind of event to encode
142
+ #
143
+ # @return [String] The bech32-encoded string
144
+ #
145
+ def self.nevent_encode(id:, relays: [], kind: nil)
146
+ entry_relays = relays.map do |relay_url|
147
+ ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
148
+ end
149
+
150
+ id_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, id)
151
+ kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
152
+
153
+ entries = [id_entry, *entry_relays, kind_entry].compact
154
+ entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT, entries)
155
+ entity.encode
156
+ end
157
+
158
+ # Encodes a profile into a bech32 string
159
+ #
160
+ # @api public
161
+ #
162
+ # @example
163
+ # nprofile = Nostr::Bech32.nprofile_encode(
164
+ # pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
165
+ # relays: ['wss://relay.damus.io', 'wss://nos.lol']
166
+ # )
167
+ #
168
+ # @param [PublicKey] pubkey The public key to encode
169
+ # @param [Array<String>] relays The relays to encode
170
+ #
171
+ # @return [String] The bech32-encoded string
172
+ #
173
+ def self.nprofile_encode(pubkey:, relays: [])
174
+ entry_relays = relays.map do |relay_url|
175
+ ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
176
+ end
177
+
178
+ pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, pubkey)
179
+ entries = [pubkey_entry, *entry_relays].compact
180
+ entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_PROFILE, entries)
181
+ entity.encode
182
+ end
183
+
184
+ # Encodes a relay URL into a bech32 string
185
+ #
186
+ # @api public
187
+ #
188
+ # @example
189
+ # nrelay = Nostr::Bech32.nrelay_encode('wss://relay.damus.io')
190
+ # nrelay # => 'nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x'
191
+ #
192
+ # @param [String] relay_url The relay url to encode
193
+ #
194
+ # @return [String] The bech32-encoded string
195
+ #
196
+ def self.nrelay_encode(relay_url)
197
+ relay_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, relay_url)
198
+
199
+ entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_RELAY, [relay_entry])
200
+ entity.encode
201
+ end
202
+ end
203
+ end
data/lib/nostr/client.rb CHANGED
@@ -74,7 +74,8 @@ module Nostr
74
74
  # @example Subscribing to all events created after a certain time
75
75
  # subscription = client.subscribe(filter: Nostr::Filter.new(since: 1230981305))
76
76
  #
77
- # @param subscription_id [String] The subscription id. A random string.
77
+ # @param subscription_id [String] The subscription id. An arbitrary, non-empty string of max length 64
78
+ # chars used to represent a subscription.
78
79
  # @param filter [Filter] A set of attributes that represent the events that the client is interested in.
79
80
  #
80
81
  # @return [Subscription] The subscription object
@@ -0,0 +1,147 @@
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 [PrivateKey] 32-bytes hex-encoded private key of the creator.
34
+ # @param recipient_public_key [PublicKey] 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 [PublicKey] 32-bytes hex-encoded public key of the message creator.
58
+ # @param recipient_private_key [PrivateKey] 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
+
66
+ # Ensure iv and base64_encoded_text are not nil
67
+ return '' unless iv && base64_encoded_text
68
+
69
+ cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
70
+ cipher.iv = Base64.decode64(iv)
71
+ cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
72
+ plain_text = cipher.update(Base64.decode64(base64_encoded_text)) + cipher.final
73
+ plain_text.force_encoding('UTF-8')
74
+ end
75
+
76
+ # Uses the private key to generate an event id and sign the event
77
+ #
78
+ # @api public
79
+ #
80
+ # @example Signing an event
81
+ # crypto = Nostr::Crypto.new
82
+ # crypto.sign(event, private_key)
83
+ # event.id # => an id
84
+ # event.sig # => a signature
85
+ #
86
+ # @param event [Event] The event to be signed
87
+ # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
88
+ #
89
+ # @return [Event] An unsigned event.
90
+ #
91
+ def sign_event(event, private_key)
92
+ event_digest = hash_event(event)
93
+
94
+ hex_private_key = Array(private_key).pack('H*')
95
+ hex_message = Array(event_digest).pack('H*')
96
+ event_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
97
+
98
+ event.id = event_digest
99
+ event.sig = event_signature
100
+
101
+ event
102
+ end
103
+
104
+ private
105
+
106
+ # Finds a shared key between two keys
107
+ #
108
+ # @api private
109
+ #
110
+ # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
111
+ # @param public_key [PublicKey] 32-bytes hex-encoded public key.
112
+ #
113
+ # @return [String] A shared key used in the event's content encryption and decryption.
114
+ #
115
+ def compute_shared_key(private_key, public_key)
116
+ group = OpenSSL::PKey::EC::Group.new(CIPHER_CURVE)
117
+
118
+ private_key_bn = OpenSSL::BN.new(private_key, BN_BASE)
119
+ public_key_bn = OpenSSL::BN.new("02#{public_key}", BN_BASE)
120
+ public_key_point = OpenSSL::PKey::EC::Point.new(group, public_key_bn)
121
+
122
+ asn1 = OpenSSL::ASN1::Sequence(
123
+ [
124
+ OpenSSL::ASN1::Integer.new(1),
125
+ OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)),
126
+ OpenSSL::ASN1::ObjectId(CIPHER_CURVE, 0, :EXPLICIT),
127
+ OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT)
128
+ ]
129
+ )
130
+
131
+ pkey = OpenSSL::PKey::EC.new(asn1.to_der)
132
+ pkey.dh_compute_key(public_key_point)
133
+ end
134
+
135
+ # Generates a SHA256 hash of a +Nostr::Event+
136
+ #
137
+ # @api private
138
+ #
139
+ # @param event [Event] The event to be hashed
140
+ #
141
+ # @return [String] A SHA256 digest of the event
142
+ #
143
+ def hash_event(event)
144
+ Digest::SHA256.hexdigest(JSON.dump(event.serialize))
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Base error class
5
+ class Error < StandardError
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Raised when the human readable part of a Bech32 string is invalid
5
+ #
6
+ # @api public
7
+ #
8
+ class InvalidHRPError < KeyValidationError
9
+ # Initializes the error
10
+ #
11
+ # @example
12
+ # InvalidHRPError.new('example wrong hrp', 'nsec')
13
+ #
14
+ # @param given_hrp [String] The given human readable part of the Bech32 string
15
+ # @param allowed_hrp [String] The allowed human readable part of the Bech32 string
16
+ #
17
+ def initialize(given_hrp, allowed_hrp)
18
+ super("Invalid hrp: #{given_hrp}. The allowed hrp value for this kind of entity is '#{allowed_hrp}'.")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Raised when the private key is in an invalid format
5
+ #
6
+ # @api public
7
+ #
8
+ class InvalidKeyFormatError < KeyValidationError
9
+ # Initializes the error
10
+ #
11
+ # @example
12
+ # InvalidKeyFormatError.new('private'')
13
+ #
14
+ # @param [String] key_kind The kind of key that is invalid (public or private)
15
+ #
16
+ def initialize(key_kind)
17
+ super("Only lowercase hexadecimal characters are allowed in #{key_kind} keys.")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Raised when the private key's length is not 64 characters
5
+ #
6
+ # @api public
7
+ #
8
+ class InvalidKeyLengthError < KeyValidationError
9
+ # Initializes the error
10
+ #
11
+ # @example
12
+ # InvalidKeyLengthError.new('private'')
13
+ #
14
+ # @param [String] key_kind The kind of key that is invalid (public or private)
15
+ #
16
+ def initialize(key_kind)
17
+ super("Invalid #{key_kind} key length. It should have 64 characters.")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Raised when the private key is not a string
5
+ #
6
+ # @api public
7
+ #
8
+ class InvalidKeyTypeError < KeyValidationError
9
+ # Initializes the error
10
+ #
11
+ # @example
12
+ # InvalidKeyTypeError.new('private'')
13
+ #
14
+ # @param [String] key_kind The kind of key that is invalid (public or private)
15
+ #
16
+ def initialize(key_kind) = super("Invalid #{key_kind} key type")
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Base class for all key validation errors
5
+ class KeyValidationError < Error; end
6
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors/error'
4
+ require_relative 'errors/key_validation_error'
5
+ require_relative 'errors/invalid_hrp_error'
6
+ require_relative 'errors/invalid_key_type_error'
7
+ require_relative 'errors/invalid_key_length_error'
8
+ require_relative 'errors/invalid_key_format_error'
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,95 @@ 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)
54
-
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
+ )
55
131
  @id = id
56
132
  @sig = sig
133
+ @pubkey = pubkey
134
+ @created_at = created_at
135
+ @kind = kind
136
+ @tags = tags
137
+ @content = content
138
+ end
139
+
140
+ # Adds a reference to an event id as an 'e' tag
141
+ #
142
+ # @api public
143
+ #
144
+ # @example Adding a reference to a pubkey
145
+ # event_id = '189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408'
146
+ # event.add_event_reference(event_id)
147
+ #
148
+ # @param event_id [String] 32-bytes hex-encoded event id.
149
+ #
150
+ # @return [Array<String>] The event's updated list of tags
151
+ #
152
+ def add_event_reference(event_id) = tags.push(['e', event_id])
153
+
154
+ # Adds a reference to a pubkey as a 'p' tag
155
+ #
156
+ # @api public
157
+ #
158
+ # @example Adding a reference to a pubkey
159
+ # pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
160
+ # event.add_pubkey_reference(pubkey)
161
+ #
162
+ # @param pubkey [PublicKey] 32-bytes hex-encoded public key.
163
+ #
164
+ # @return [Array<String>] The event's updated list of tags
165
+ #
166
+ def add_pubkey_reference(pubkey) = tags.push(['p', pubkey.to_s])
167
+
168
+ # Signs an event with the user's private key
169
+ #
170
+ # @api public
171
+ #
172
+ # @example Signing an event
173
+ # event.sign(private_key)
174
+ #
175
+ # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
176
+ #
177
+ # @return [Event] A signed event.
178
+ #
179
+ def sign(private_key)
180
+ crypto = Crypto.new
181
+ crypto.sign_event(self, private_key)
182
+ end
183
+
184
+ # Serializes the event, to obtain a SHA256 digest of it
185
+ #
186
+ # @api public
187
+ #
188
+ # @example Converting the event to a digest
189
+ # event.serialize
190
+ #
191
+ # @return [Array] The event as an array.
192
+ #
193
+ def serialize
194
+ [
195
+ 0,
196
+ pubkey,
197
+ created_at,
198
+ kind,
199
+ tags,
200
+ content
201
+ ]
57
202
  end
58
203
 
59
204
  # Converts the event to a hash
@@ -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