nostr 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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