nostr 0.4.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.
- checksums.yaml +4 -4
- data/.editorconfig +1 -1
- data/.rubocop.yml +23 -0
- data/.tool-versions +2 -1
- data/CHANGELOG.md +36 -1
- data/README.md +92 -228
- data/docs/.gitignore +4 -0
- data/docs/.vitepress/config.mjs +112 -0
- data/docs/README.md +44 -0
- data/docs/api-examples.md +49 -0
- data/docs/bun.lockb +0 -0
- data/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md +190 -0
- data/docs/core/client.md +108 -0
- data/docs/core/keys.md +136 -0
- data/docs/core/user.md +43 -0
- data/docs/events/contact-list.md +29 -0
- data/docs/events/encrypted-direct-message.md +28 -0
- data/docs/events/recommend-server.md +32 -0
- data/docs/events/set-metadata.md +20 -0
- data/docs/events/text-note.md +15 -0
- data/docs/events.md +11 -0
- data/docs/getting-started/installation.md +21 -0
- data/docs/getting-started/overview.md +170 -0
- data/docs/implemented-nips.md +9 -0
- data/docs/index.md +44 -0
- data/docs/markdown-examples.md +85 -0
- data/docs/package.json +12 -0
- data/docs/relays/connecting-to-a-relay.md +21 -0
- data/docs/relays/publishing-events.md +29 -0
- data/docs/relays/receiving-events.md +6 -0
- data/docs/subscriptions/creating-a-subscription.md +49 -0
- data/docs/subscriptions/deleting-a-subscription.md +10 -0
- data/docs/subscriptions/filtering-subscription-events.md +115 -0
- data/docs/subscriptions/updating-a-subscription.md +4 -0
- data/lib/nostr/bech32.rb +203 -0
- data/lib/nostr/client.rb +2 -1
- data/lib/nostr/crypto.rb +11 -7
- data/lib/nostr/errors/error.rb +7 -0
- data/lib/nostr/errors/invalid_hrp_error.rb +21 -0
- data/lib/nostr/errors/invalid_key_format_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_length_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_type_error.rb +18 -0
- data/lib/nostr/errors/key_validation_error.rb +6 -0
- data/lib/nostr/errors.rb +8 -0
- data/lib/nostr/event.rb +3 -4
- data/lib/nostr/events/encrypted_direct_message.rb +4 -3
- data/lib/nostr/filter.rb +4 -4
- data/lib/nostr/key.rb +100 -0
- data/lib/nostr/key_pair.rb +30 -6
- data/lib/nostr/keygen.rb +43 -4
- data/lib/nostr/private_key.rb +36 -0
- data/lib/nostr/public_key.rb +36 -0
- data/lib/nostr/relay_message_type.rb +18 -0
- data/lib/nostr/subscription.rb +2 -2
- data/lib/nostr/user.rb +17 -8
- data/lib/nostr/version.rb +1 -1
- data/lib/nostr.rb +6 -0
- data/nostr.gemspec +9 -9
- data/sig/nostr/bech32.rbs +14 -0
- data/sig/nostr/client.rbs +5 -5
- data/sig/nostr/crypto.rbs +5 -5
- data/sig/nostr/errors/error.rbs +4 -0
- data/sig/nostr/errors/invalid_hrb_error.rbs +6 -0
- data/sig/nostr/errors/invalid_key_format_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_length_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_type_error.rbs +5 -0
- data/sig/nostr/errors/key_validation_error.rbs +4 -0
- data/sig/nostr/event.rbs +4 -4
- data/sig/nostr/events/encrypted_direct_message.rbs +2 -2
- data/sig/nostr/filter.rbs +3 -12
- data/sig/nostr/key.rbs +16 -0
- data/sig/nostr/key_pair.rbs +7 -3
- data/sig/nostr/keygen.rbs +5 -2
- data/sig/nostr/private_key.rbs +4 -0
- data/sig/nostr/public_key.rbs +4 -0
- data/sig/nostr/relay_message_type.rbs +8 -0
- data/sig/nostr/user.rbs +4 -8
- data/sig/vendor/bech32/nostr/entity.rbs +41 -0
- data/sig/vendor/bech32/nostr/nip19.rbs +20 -0
- data/sig/vendor/bech32/segwit_addr.rbs +21 -0
- data/sig/vendor/bech32.rbs +25 -0
- data/sig/vendor/event_emitter.rbs +10 -3
- data/sig/vendor/event_machine/channel.rbs +1 -1
- data/sig/vendor/faye/websocket/api.rbs +45 -0
- data/sig/vendor/faye/websocket/client.rbs +43 -0
- data/sig/vendor/faye/websocket.rbs +30 -0
- metadata +79 -21
data/lib/nostr/bech32.rb
ADDED
@@ -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.
|
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
|
data/lib/nostr/crypto.rb
CHANGED
@@ -30,8 +30,8 @@ module Nostr
|
|
30
30
|
# encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8')
|
31
31
|
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
32
32
|
#
|
33
|
-
# @param sender_private_key [
|
34
|
-
# @param recipient_public_key [
|
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
35
|
# @param plain_text [String] The text to be encrypted
|
36
36
|
#
|
37
37
|
# @return [String] Encrypted text.
|
@@ -54,14 +54,18 @@ module Nostr
|
|
54
54
|
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
55
55
|
# decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted)
|
56
56
|
#
|
57
|
-
# @param sender_public_key [
|
58
|
-
# @param recipient_private_key [
|
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
59
|
# @param encrypted_text [String] The text to be decrypted
|
60
60
|
#
|
61
61
|
# @return [String] Decrypted text.
|
62
62
|
#
|
63
63
|
def decrypt_text(recipient_private_key, sender_public_key, encrypted_text)
|
64
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
|
+
|
65
69
|
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
|
66
70
|
cipher.iv = Base64.decode64(iv)
|
67
71
|
cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
|
@@ -80,7 +84,7 @@ module Nostr
|
|
80
84
|
# event.sig # => a signature
|
81
85
|
#
|
82
86
|
# @param event [Event] The event to be signed
|
83
|
-
# @param private_key [
|
87
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
84
88
|
#
|
85
89
|
# @return [Event] An unsigned event.
|
86
90
|
#
|
@@ -103,8 +107,8 @@ module Nostr
|
|
103
107
|
#
|
104
108
|
# @api private
|
105
109
|
#
|
106
|
-
# @param private_key [
|
107
|
-
# @param public_key [
|
110
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
111
|
+
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
108
112
|
#
|
109
113
|
# @return [String] A shared key used in the event's content encryption and decryption.
|
110
114
|
#
|
@@ -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
|
data/lib/nostr/errors.rb
ADDED
@@ -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
@@ -128,7 +128,6 @@ module Nostr
|
|
128
128
|
id: nil,
|
129
129
|
sig: nil
|
130
130
|
)
|
131
|
-
|
132
131
|
@id = id
|
133
132
|
@sig = sig
|
134
133
|
@pubkey = pubkey
|
@@ -160,11 +159,11 @@ module Nostr
|
|
160
159
|
# pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
|
161
160
|
# event.add_pubkey_reference(pubkey)
|
162
161
|
#
|
163
|
-
# @param pubkey [
|
162
|
+
# @param pubkey [PublicKey] 32-bytes hex-encoded public key.
|
164
163
|
#
|
165
164
|
# @return [Array<String>] The event's updated list of tags
|
166
165
|
#
|
167
|
-
def add_pubkey_reference(pubkey) = tags.push(['p', pubkey])
|
166
|
+
def add_pubkey_reference(pubkey) = tags.push(['p', pubkey.to_s])
|
168
167
|
|
169
168
|
# Signs an event with the user's private key
|
170
169
|
#
|
@@ -173,7 +172,7 @@ module Nostr
|
|
173
172
|
# @example Signing an event
|
174
173
|
# event.sign(private_key)
|
175
174
|
#
|
176
|
-
# @param private_key [
|
175
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
177
176
|
#
|
178
177
|
# @return [Event] A signed event.
|
179
178
|
#
|
@@ -26,8 +26,9 @@ module Nostr
|
|
26
26
|
# )
|
27
27
|
#
|
28
28
|
# @param plain_text [String] The +content+ of the encrypted message.
|
29
|
-
# @param sender_private_key [
|
30
|
-
# @param recipient_public_key [
|
29
|
+
# @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the message's author.
|
30
|
+
# @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient of the encrypted
|
31
|
+
# message.
|
31
32
|
# @param previous_direct_message [String] 32-bytes hex-encoded id identifying the previous message in a
|
32
33
|
# conversation or a message we are explicitly replying to (such that contextual, more organized conversations
|
33
34
|
# may happen
|
@@ -43,7 +44,7 @@ module Nostr
|
|
43
44
|
pubkey: sender_public_key,
|
44
45
|
kind: Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE,
|
45
46
|
content: encrypted_content,
|
46
|
-
|
47
|
+
)
|
47
48
|
|
48
49
|
add_pubkey_reference(recipient_public_key)
|
49
50
|
add_event_reference(previous_direct_message) if previous_direct_message
|
data/lib/nostr/filter.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Nostr
|
4
4
|
# A filter determines what events will be sent in a subscription.
|
5
5
|
class Filter
|
6
|
-
# A list of event ids
|
6
|
+
# A list of event ids
|
7
7
|
#
|
8
8
|
# @api public
|
9
9
|
#
|
@@ -14,7 +14,7 @@ module Nostr
|
|
14
14
|
#
|
15
15
|
attr_reader :ids
|
16
16
|
|
17
|
-
# A list of pubkeys
|
17
|
+
# A list of pubkeys, the pubkey of an event must be one of these
|
18
18
|
#
|
19
19
|
# @api public
|
20
20
|
#
|
@@ -107,8 +107,8 @@ module Nostr
|
|
107
107
|
# )
|
108
108
|
#
|
109
109
|
# @param kwargs [Hash]
|
110
|
-
# @option kwargs [Array<String>, nil] ids A list of event ids
|
111
|
-
# @option kwargs [Array<String>, nil] authors A list of pubkeys
|
110
|
+
# @option kwargs [Array<String>, nil] ids A list of event ids
|
111
|
+
# @option kwargs [Array<String>, nil] authors A list of pubkeys, the pubkey of an event must be one
|
112
112
|
# of these
|
113
113
|
# @option kwargs [Array<Integer>, nil] kinds A list of a kind numbers
|
114
114
|
# @option kwargs [Array<String>, nil] e A list of event ids that are referenced in an "e" tag
|
data/lib/nostr/key.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Abstract class for all keys
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
class Key < String
|
9
|
+
# The regular expression for hexadecimal lowercase characters
|
10
|
+
#
|
11
|
+
# @return [Regexp] The regular expression for hexadecimal lowercase characters
|
12
|
+
#
|
13
|
+
FORMAT = /^[a-f0-9]+$/
|
14
|
+
|
15
|
+
# The length of the key in hex format
|
16
|
+
#
|
17
|
+
# @return [Integer] The length of the key in hex format
|
18
|
+
#
|
19
|
+
LENGTH = 64
|
20
|
+
|
21
|
+
# Instantiates a new key. Can't be used directly because this is an abstract class. Raises a +ValidationError+
|
22
|
+
#
|
23
|
+
# @see Nostr::PrivateKey
|
24
|
+
# @see Nostr::PublicKey
|
25
|
+
#
|
26
|
+
# @param [String] hex_value Hex-encoded value of the key
|
27
|
+
#
|
28
|
+
# @raise [ValidationError]
|
29
|
+
#
|
30
|
+
def initialize(hex_value)
|
31
|
+
validate_hex_value(hex_value)
|
32
|
+
|
33
|
+
super(hex_value)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Instantiates a key from a bech32 string
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# bech32_key = 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
|
42
|
+
# bech32_key.to_key # => #<Nostr::PublicKey:0x000000010601e3c8 @hex_value="...">
|
43
|
+
#
|
44
|
+
# @raise [Nostr::InvalidHRPError] if the bech32 string is invalid.
|
45
|
+
#
|
46
|
+
# @param [String] bech32_value The bech32 string representation of the key.
|
47
|
+
#
|
48
|
+
# @return [Key] the key.
|
49
|
+
#
|
50
|
+
def self.from_bech32(bech32_value)
|
51
|
+
type, data = Bech32.decode(bech32_value)
|
52
|
+
|
53
|
+
raise InvalidHRPError.new(type, hrp) unless type == hrp
|
54
|
+
|
55
|
+
new(data)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Abstract method to be implemented by subclasses to provide the HRP (npub, nsec)
|
59
|
+
#
|
60
|
+
# @api private
|
61
|
+
#
|
62
|
+
# @return [String] The HRP
|
63
|
+
#
|
64
|
+
def self.hrp
|
65
|
+
raise 'Subclasses must implement this method'
|
66
|
+
end
|
67
|
+
|
68
|
+
# Converts the key to a bech32 string representation
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
#
|
72
|
+
# @example Converting a private key to a bech32 string
|
73
|
+
# public_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
|
74
|
+
# public_key.to_bech32 # => 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
|
75
|
+
#
|
76
|
+
# @example Converting a public key to a bech32 string
|
77
|
+
# public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
78
|
+
# public_key.to_bech32 # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
79
|
+
#
|
80
|
+
# @return [String] The bech32 string representation of the key
|
81
|
+
#
|
82
|
+
def to_bech32 = Bech32.encode(hrp: self.class.hrp, data: self)
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
# Validates the hex value during initialization
|
87
|
+
#
|
88
|
+
# @api private
|
89
|
+
#
|
90
|
+
# @param [String] _hex_value The hex value of the key
|
91
|
+
#
|
92
|
+
# @raise [KeyValidationError] When the hex value is invalid
|
93
|
+
#
|
94
|
+
# @return [void]
|
95
|
+
#
|
96
|
+
def validate_hex_value(_hex_value)
|
97
|
+
raise 'Subclasses must implement this method'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/nostr/key_pair.rb
CHANGED
@@ -10,7 +10,7 @@ module Nostr
|
|
10
10
|
# @example
|
11
11
|
# keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
|
12
12
|
#
|
13
|
-
# @return [
|
13
|
+
# @return [PrivateKey]
|
14
14
|
#
|
15
15
|
attr_reader :private_key
|
16
16
|
|
@@ -21,7 +21,7 @@ module Nostr
|
|
21
21
|
# @example
|
22
22
|
# keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
|
23
23
|
#
|
24
|
-
# @return [
|
24
|
+
# @return [PublicKey]
|
25
25
|
#
|
26
26
|
attr_reader :public_key
|
27
27
|
|
@@ -31,16 +31,40 @@ module Nostr
|
|
31
31
|
#
|
32
32
|
# @example
|
33
33
|
# keypair = Nostr::KeyPair.new(
|
34
|
-
# private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
|
35
|
-
# public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
|
34
|
+
# private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
|
35
|
+
# public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
|
36
36
|
# )
|
37
37
|
#
|
38
|
-
# @param private_key [
|
39
|
-
# @param public_key [
|
38
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
39
|
+
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
40
|
+
#
|
41
|
+
# @raise ArgumentError when the private key is not a +PrivateKey+
|
42
|
+
# @raise ArgumentError when the public key is not a +PublicKey+
|
40
43
|
#
|
41
44
|
def initialize(private_key:, public_key:)
|
45
|
+
validate_keys(private_key, public_key)
|
46
|
+
|
42
47
|
@private_key = private_key
|
43
48
|
@public_key = public_key
|
44
49
|
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Validates the keys
|
54
|
+
#
|
55
|
+
# @api private
|
56
|
+
#
|
57
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
58
|
+
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
59
|
+
#
|
60
|
+
# @raise ArgumentError when the private key is not a +PrivateKey+
|
61
|
+
# @raise ArgumentError when the public key is not a +PublicKey+
|
62
|
+
#
|
63
|
+
# @return [void]
|
64
|
+
#
|
65
|
+
def validate_keys(private_key, public_key)
|
66
|
+
raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey)
|
67
|
+
raise ArgumentError, 'public_key is not an instance of PublicKey' unless public_key.is_a?(Nostr::PublicKey)
|
68
|
+
end
|
45
69
|
end
|
46
70
|
end
|