nostr 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +23 -0
- data/CHANGELOG.md +16 -0
- data/README.md +232 -13
- data/Steepfile +14 -0
- data/lib/nostr/client.rb +176 -0
- data/lib/nostr/client_message_type.rb +15 -0
- data/lib/nostr/event.rb +93 -0
- data/lib/nostr/event_fragment.rb +111 -0
- data/lib/nostr/event_kind.rb +35 -0
- data/lib/nostr/filter.rb +172 -0
- data/lib/nostr/key_pair.rb +46 -0
- data/lib/nostr/keygen.rb +78 -0
- data/lib/nostr/relay.rb +43 -0
- data/lib/nostr/subscription.rb +65 -0
- data/lib/nostr/user.rb +92 -0
- data/lib/nostr/version.rb +2 -1
- data/lib/nostr.rb +11 -2
- data/nostr.gemspec +15 -2
- data/sig/nostr/client.rbs +20 -0
- data/sig/nostr/client_message_type.rbs +7 -0
- data/sig/nostr/event.rbs +24 -0
- data/sig/nostr/event_fragment.rbs +12 -0
- data/sig/nostr/event_kind.rbs +8 -0
- data/sig/nostr/filter.rbs +25 -0
- data/sig/nostr/key_pair.rbs +9 -0
- data/sig/nostr/keygen.rbs +13 -0
- data/sig/nostr/relay.rbs +9 -0
- data/sig/nostr/subscription.rbs +9 -0
- data/sig/nostr/user.rbs +24 -0
- data/sig/vendor/ecsda/group/secp256k1.rbs +6 -0
- data/sig/vendor/event_emitter.rbs +9 -0
- data/sig/vendor/event_machine/channel.rbs +18 -0
- data/sig/vendor/event_machine.rbs +69 -0
- data/sig/vendor/schnorr.rbs +4 -0
- metadata +205 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 371ed11c0474fd944cc55a054d553945623f7439f67c55f3eadc564805d2fb11
|
4
|
+
data.tar.gz: f7fbe86119bd7816e7066ea3603263dee7327b06daeb4b92f9e90977f52ef8ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa602354304ce9e77377b80cab60146f51ac8943b1399070a87ce81a5e44582f0a23b50f1736fafd9d7d0f13042b8b7292b9d29684077ad878e2439ddf7970ef
|
7
|
+
data.tar.gz: 83fc3d535a1e35438522779ef7dc3e101b4fea3a3f9874db9457b4f6c61d74469ded6ba973923d731789d3c50d7aa66b1c0770d62451794d45b52720c7928ebf
|
data/.rubocop.yml
CHANGED
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2023-01-12 10:00:10 UTC using RuboCop version 1.42.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 2
|
10
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes.
|
11
|
+
Metrics/AbcSize:
|
12
|
+
Max: 23
|
13
|
+
|
14
|
+
# Offense count: 2
|
15
|
+
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods.
|
16
|
+
Metrics/MethodLength:
|
17
|
+
Max: 16
|
18
|
+
|
19
|
+
# Offense count: 4
|
20
|
+
# Configuration parameters: AssignmentOnly.
|
21
|
+
RSpec/InstanceVariable:
|
22
|
+
Exclude:
|
23
|
+
- 'spec/nostr/client_spec.rb'
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,22 @@ 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.3.0] - 2023-02-15
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- Client compliance wth [NIP-02](https://github.com/nostr-protocol/nips/blob/master/02.md) (manage contact lists)
|
12
|
+
|
13
|
+
## [0.2.0] - 2023-01-12
|
14
|
+
|
15
|
+
### Added
|
16
|
+
|
17
|
+
- [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) compliant client
|
18
|
+
|
7
19
|
## [0.1.0] - 2023-01-06
|
8
20
|
|
9
21
|
- Initial release
|
22
|
+
|
23
|
+
[0.3.0]: https://github.com/wilsonsilva/nostr/compare/v0.2.0...v0.3.0
|
24
|
+
[0.2.0]: https://github.com/wilsonsilva/nostr/compare/v0.1.0...v0.2.0
|
25
|
+
[0.1.0]: https://github.com/wilsonsilva/nostr/compare/7fded5...v0.1.0
|
data/README.md
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# Nostr
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/nostr.svg)](https://badge.fury.io/rb/nostr)
|
4
|
-
[![
|
5
|
-
[![
|
4
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/maintainability)](https://codeclimate.com/github/wilsonsilva/nostr/maintainability)
|
5
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/test_coverage)](https://codeclimate.com/github/wilsonsilva/nostr/test_coverage)
|
6
6
|
|
7
|
-
Nostr client. Please note that the API is likely to change as the gem is still in development and
|
8
|
-
stable release. Use with caution.
|
7
|
+
Asynchronous Nostr client. Please note that the API is likely to change as the gem is still in development and
|
8
|
+
has not yet reached a stable release. Use with caution.
|
9
9
|
|
10
10
|
## Installation
|
11
11
|
|
@@ -19,10 +19,212 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
19
19
|
|
20
20
|
## Usage
|
21
21
|
|
22
|
+
### Requiring the gem
|
23
|
+
|
24
|
+
All examples below assume that the gem has been required.
|
25
|
+
|
22
26
|
```ruby
|
23
27
|
require 'nostr'
|
24
28
|
```
|
25
29
|
|
30
|
+
### Generating a keypair
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
keygen = Nostr::Keygen.new
|
34
|
+
keypair = keygen.generate_keypair
|
35
|
+
|
36
|
+
keypair.private_key
|
37
|
+
keypair.public_key
|
38
|
+
```
|
39
|
+
|
40
|
+
### Generating a private key and a public key
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
keygen = Nostr::Keygen.new
|
44
|
+
|
45
|
+
private_key = keygen.generate_private_key
|
46
|
+
public_key = keygen.extract_public_key(private_key)
|
47
|
+
```
|
48
|
+
|
49
|
+
### Connecting to a Relay
|
50
|
+
|
51
|
+
Clients can connect to multiple Relays. In this version, a Client can only connect to a single Relay at a time.
|
52
|
+
|
53
|
+
You may instantiate multiple Clients and multiple Relays.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
client = Nostr::Client.new
|
57
|
+
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
|
58
|
+
|
59
|
+
client.connect(relay)
|
60
|
+
```
|
61
|
+
|
62
|
+
### WebSocket events
|
63
|
+
|
64
|
+
All communication between clients and relays happen in WebSockets.
|
65
|
+
|
66
|
+
The `:connect` event is fired when a connection with a WebSocket is opened. You must call `Nostr::Client#connect` first.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
client.on :connect do
|
70
|
+
# all the code goes here
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
The `:close` event is fired when a connection with a WebSocket has been closed because of an error.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
client.on :error do |error_message|
|
78
|
+
puts error_message
|
79
|
+
end
|
80
|
+
|
81
|
+
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the server certificate for 'rsslay.fiatjaf.com'
|
82
|
+
```
|
83
|
+
|
84
|
+
The `:message` event is fired when data is received through a WebSocket.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
client.on :message do |message|
|
88
|
+
puts message
|
89
|
+
end
|
90
|
+
|
91
|
+
# [
|
92
|
+
# "EVENT",
|
93
|
+
# "d34107357089bfc9882146d3bfab0386",
|
94
|
+
# {
|
95
|
+
# "content":"",
|
96
|
+
# "created_at":1676456512,
|
97
|
+
# "id":"18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
|
98
|
+
# "kind":3,
|
99
|
+
# "pubkey":"117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
|
100
|
+
# "sig":"d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
|
101
|
+
# "tags":[
|
102
|
+
# ["p","1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
|
103
|
+
# ]
|
104
|
+
# }
|
105
|
+
# ]
|
106
|
+
```
|
107
|
+
|
108
|
+
The `:close` event is fired when a connection with a WebSocket is closed.
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
client.on :close do |code, reason|
|
112
|
+
# you may attempt to reconnect
|
113
|
+
|
114
|
+
client.connect(relay)
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
### Requesting for events / creating a subscription
|
119
|
+
|
120
|
+
A client can request events and subscribe to new updates after it has established a connection with the Relay.
|
121
|
+
|
122
|
+
You may use a `Nostr::Filter` instance with as many attributes as you wish:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
client.on :connect do
|
126
|
+
filter = Nostr::Filter.new(
|
127
|
+
ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
|
128
|
+
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
|
129
|
+
kinds: [Nostr::EventKind::TEXT_NOTE],
|
130
|
+
e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
|
131
|
+
p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
|
132
|
+
since: 1230981305,
|
133
|
+
until: 1292190341,
|
134
|
+
limit: 420,
|
135
|
+
)
|
136
|
+
|
137
|
+
subscription = client.subscribe('a_random_subscription_id', filter)
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
With just a few:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
client.on :connect do
|
145
|
+
filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
|
146
|
+
subscription = client.subscribe('a_random_subscription_id', filter)
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
Or omit the filter:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
client.on :connect do
|
154
|
+
subscription = client.subscribe('a_random_subscription_id')
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
Or even omit the subscription id:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
client.on :connect do
|
162
|
+
subscription = client.subscribe('a_random_subscription_id')
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
### Stop previous subscriptions
|
167
|
+
|
168
|
+
You can stop receiving messages from a subscription by calling `#unsubscribe`:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
client.unsubscribe('your_subscription_id')
|
172
|
+
```
|
173
|
+
|
174
|
+
### Publishing an event
|
175
|
+
|
176
|
+
To publish an event you need a keypair.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
# Set up the private key
|
180
|
+
private_key = 'a630b06e2f883378d0aa335b9adaf7734603e00433350b684fe53e184f08c58f'
|
181
|
+
user = Nostr::User.new(private_key)
|
182
|
+
|
183
|
+
# Create a signed event
|
184
|
+
event = user.create_event(
|
185
|
+
created_at: 1667422587, # optional, defaults to the current time
|
186
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
187
|
+
tags: [], # optional, defaults to []
|
188
|
+
content: 'Your feedback is appreciated, now pay $8'
|
189
|
+
)
|
190
|
+
|
191
|
+
# Send it to the Relay
|
192
|
+
client.publish(event)
|
193
|
+
```
|
194
|
+
|
195
|
+
### Creating/updating your contact list
|
196
|
+
|
197
|
+
Every new contact list that gets published overwrites the past ones, so it should contain all entries.
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
# Creating a contact list event with 2 contacts
|
201
|
+
update_contacts_event = user.create_event(
|
202
|
+
kind: Nostr::EventKind::CONTACT_LIST,
|
203
|
+
tags: [
|
204
|
+
[
|
205
|
+
"p", # mandatory
|
206
|
+
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
|
207
|
+
"wss://alicerelay.com/", # can be an empty string or can be omitted
|
208
|
+
"alice" # can be an empty string or can be omitted
|
209
|
+
],
|
210
|
+
[
|
211
|
+
"p", # mandatory
|
212
|
+
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
|
213
|
+
"wss://bobrelay.com/nostr", # can be an empty string or can be omitted
|
214
|
+
"bob" # can be an empty string or can be omitted
|
215
|
+
],
|
216
|
+
],
|
217
|
+
)
|
218
|
+
|
219
|
+
# Send it to the Relay
|
220
|
+
client.publish(update_contacts_event)
|
221
|
+
```
|
222
|
+
|
223
|
+
## NIPS
|
224
|
+
|
225
|
+
- [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
226
|
+
- [x] [NIP-02 - Client](https://github.com/nostr-protocol/nips/blob/master/02.md)
|
227
|
+
|
26
228
|
## Development
|
27
229
|
|
28
230
|
After checking out the repo, run `bin/setup` to install dependencies.
|
@@ -39,17 +241,34 @@ The health and maintainability of the codebase is ensured through a set of
|
|
39
241
|
Rake tasks to test, lint and audit the gem for security vulnerabilities and documentation:
|
40
242
|
|
41
243
|
```
|
42
|
-
rake
|
43
|
-
rake
|
44
|
-
rake
|
45
|
-
rake
|
46
|
-
rake
|
47
|
-
rake
|
48
|
-
rake
|
49
|
-
rake
|
50
|
-
rake
|
244
|
+
rake build # Build nostr.gem into the pkg directory
|
245
|
+
rake build:checksum # Generate SHA512 checksum if nostr.gem into the checksums directory
|
246
|
+
rake bundle:audit:check # Checks the Gemfile.lock for insecure dependencies
|
247
|
+
rake bundle:audit:update # Updates the bundler-audit vulnerability database
|
248
|
+
rake clean # Remove any temporary products
|
249
|
+
rake clobber # Remove any generated files
|
250
|
+
rake coverage # Run spec with coverage
|
251
|
+
rake install # Build and install nostr.gem into system gems
|
252
|
+
rake install:local # Build and install nostr.gem into system gems without network access
|
253
|
+
rake qa # Test, lint and perform security and documentation audits
|
254
|
+
rake release[remote] # Create a tag, build and push nostr.gem to rubygems.org
|
255
|
+
rake rubocop # Run RuboCop
|
256
|
+
rake rubocop:autocorrect # Autocorrect RuboCop offenses (only when it's safe)
|
257
|
+
rake rubocop:autocorrect_all # Autocorrect RuboCop offenses (safe and unsafe)
|
258
|
+
rake spec # Run RSpec code examples
|
259
|
+
rake verify_measurements # Verify that yardstick coverage is at least 100%
|
260
|
+
rake yard # Generate YARD Documentation
|
261
|
+
rake yard:junk # Check the junk in your YARD Documentation
|
262
|
+
rake yardstick_measure # Measure docs in lib/**/*.rb with yardstick
|
51
263
|
```
|
52
264
|
|
265
|
+
### Type checking
|
266
|
+
|
267
|
+
This gem leverages [RBS](https://github.com/ruby/rbs), a language to describe the structure of Ruby programs. It is
|
268
|
+
used to provide type checking and autocompletion in your editor. Run `bundle exec typeprof FILENAME` to generate
|
269
|
+
an RBS definition for the given Ruby file. And validate all definitions using [Steep](https://github.com/soutaro/steep)
|
270
|
+
with the command `bundle exec steep check`.
|
271
|
+
|
53
272
|
## Contributing
|
54
273
|
|
55
274
|
Bug reports and pull requests are welcome on GitHub at https://github.com/wilsonsilva/nostr.
|
data/Steepfile
ADDED
data/lib/nostr/client.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'event_emitter'
|
4
|
+
require 'faye/websocket'
|
5
|
+
|
6
|
+
module Nostr
|
7
|
+
# Clients can talk with relays and can subscribe to any set of events using a subscription filters.
|
8
|
+
# The filter represents all the set of nostr events that a client is interested in.
|
9
|
+
#
|
10
|
+
# There is no sign-up or account creation for a client. Every time a client connects to a relay, it submits its
|
11
|
+
# subscription filters and the relay streams the "interested events" to the client as long as they are connected.
|
12
|
+
#
|
13
|
+
class Client
|
14
|
+
include EventEmitter
|
15
|
+
|
16
|
+
# Instantiates a new Client
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
#
|
20
|
+
# @example Instantiating a client that logs all the events it sends and receives
|
21
|
+
# client = Nostr::Client.new(debug: true)
|
22
|
+
#
|
23
|
+
def initialize
|
24
|
+
@subscriptions = {}
|
25
|
+
|
26
|
+
initialize_channels
|
27
|
+
end
|
28
|
+
|
29
|
+
# Connects to the Relay's websocket endpoint
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
#
|
33
|
+
# @example Connecting to a relay
|
34
|
+
# relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
|
35
|
+
# client.connect(relay)
|
36
|
+
#
|
37
|
+
# @param [Relay] relay The relay to connect to
|
38
|
+
#
|
39
|
+
# @return [void]
|
40
|
+
#
|
41
|
+
def connect(relay)
|
42
|
+
execute_within_an_em_thread do
|
43
|
+
client = Faye::WebSocket::Client.new(relay.url, [], { tls: { verify_peer: false } })
|
44
|
+
parent_to_child_channel.subscribe { |msg| client.send(msg) }
|
45
|
+
|
46
|
+
client.on :open do
|
47
|
+
child_to_parent_channel.push(type: :open)
|
48
|
+
end
|
49
|
+
|
50
|
+
client.on :message do |event|
|
51
|
+
child_to_parent_channel.push(type: :message, data: event.data)
|
52
|
+
end
|
53
|
+
|
54
|
+
client.on :error do |event|
|
55
|
+
child_to_parent_channel.push(type: :error, message: event.message)
|
56
|
+
end
|
57
|
+
|
58
|
+
client.on :close do |event|
|
59
|
+
child_to_parent_channel.push(type: :close, code: event.code, reason: event.reason)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Subscribes to a set of events using a filter
|
65
|
+
#
|
66
|
+
# @api public
|
67
|
+
#
|
68
|
+
# @example Creating a subscription with no id and no filters
|
69
|
+
# subscription = client.subscribe
|
70
|
+
#
|
71
|
+
# @example Creating a subscription with an ID
|
72
|
+
# subscription = client.subscribe(subscription_id: 'my-subscription')
|
73
|
+
#
|
74
|
+
# @example Subscribing to all events created after a certain time
|
75
|
+
# subscription = client.subscribe(filter: Nostr::Filter.new(since: 1230981305))
|
76
|
+
#
|
77
|
+
# @param subscription_id [String] The subscription id. A random string.
|
78
|
+
# @param filter [Filter] A set of attributes that represent the events that the client is interested in.
|
79
|
+
#
|
80
|
+
# @return [Subscription] The subscription object
|
81
|
+
#
|
82
|
+
def subscribe(subscription_id: SecureRandom.hex, filter: Filter.new)
|
83
|
+
subscriptions[subscription_id] = Subscription.new(id: subscription_id, filter:)
|
84
|
+
parent_to_child_channel.push([ClientMessageType::REQ, subscription_id, filter.to_h].to_json)
|
85
|
+
subscriptions[subscription_id]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Stops a previous subscription
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
#
|
92
|
+
# @example Stopping a subscription
|
93
|
+
# client.unsubscribe(subscription.id)
|
94
|
+
#
|
95
|
+
# @example Stopping a subscription
|
96
|
+
# client.unsubscribe('my-subscription')
|
97
|
+
#
|
98
|
+
# @param subscription_id [String] ID of a previously created subscription.
|
99
|
+
#
|
100
|
+
# @return [void]
|
101
|
+
#
|
102
|
+
def unsubscribe(subscription_id)
|
103
|
+
subscriptions.delete(subscription_id)
|
104
|
+
parent_to_child_channel.push([ClientMessageType::CLOSE, subscription_id].to_json)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Sends an event to a Relay
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
#
|
111
|
+
# @example Sending an event to a relay
|
112
|
+
# client.publish(event)
|
113
|
+
#
|
114
|
+
# @param event [Event] The event to be sent to a Relay
|
115
|
+
#
|
116
|
+
# @return [void]
|
117
|
+
#
|
118
|
+
def publish(event)
|
119
|
+
parent_to_child_channel.push([ClientMessageType::EVENT, event.to_h].to_json)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# The subscriptions that the client has created
|
125
|
+
#
|
126
|
+
# @api private
|
127
|
+
#
|
128
|
+
# @return [Hash{String=>Subscription}>]
|
129
|
+
#
|
130
|
+
attr_reader :subscriptions
|
131
|
+
|
132
|
+
# The channel that the parent thread uses to send messages to the child thread
|
133
|
+
#
|
134
|
+
# @api private
|
135
|
+
#
|
136
|
+
# @return [EventMachine::Channel]
|
137
|
+
#
|
138
|
+
attr_reader :parent_to_child_channel
|
139
|
+
|
140
|
+
# The channel that the child thread uses to send messages to the parent thread
|
141
|
+
#
|
142
|
+
# @api private
|
143
|
+
#
|
144
|
+
# @return [EventMachine::Channel]
|
145
|
+
#
|
146
|
+
attr_reader :child_to_parent_channel
|
147
|
+
|
148
|
+
# Executes a block of code within the EventMachine thread
|
149
|
+
#
|
150
|
+
# @api private
|
151
|
+
#
|
152
|
+
# @return [Thread]
|
153
|
+
#
|
154
|
+
def execute_within_an_em_thread(&block)
|
155
|
+
Thread.new { EventMachine.run(block) }
|
156
|
+
end
|
157
|
+
|
158
|
+
# Creates the communication channels between threads
|
159
|
+
#
|
160
|
+
# @api private
|
161
|
+
#
|
162
|
+
# @return [void]
|
163
|
+
#
|
164
|
+
def initialize_channels
|
165
|
+
@parent_to_child_channel = EventMachine::Channel.new
|
166
|
+
@child_to_parent_channel = EventMachine::Channel.new
|
167
|
+
|
168
|
+
child_to_parent_channel.subscribe do |msg|
|
169
|
+
emit :connect if msg[:type] == :open
|
170
|
+
emit :message, msg[:data] if msg[:type] == :message
|
171
|
+
emit :error, msg[:message] if msg[:type] == :error
|
172
|
+
emit :close, msg[:code], msg[:reason] if msg[:type] == :close
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Clients can send 3 types of messages, which must be JSON arrays
|
5
|
+
module ClientMessageType
|
6
|
+
# @return [String] Used to publish events
|
7
|
+
EVENT = 'EVENT'
|
8
|
+
|
9
|
+
# @return [String] Used to request events and subscribe to new updates
|
10
|
+
REQ = 'REQ'
|
11
|
+
|
12
|
+
# @return [String] Used to stop previous subscriptions
|
13
|
+
CLOSE = 'CLOSE'
|
14
|
+
end
|
15
|
+
end
|
data/lib/nostr/event.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# The only object type that exists in Nostr is an event. Events are immutable.
|
5
|
+
class Event < EventFragment
|
6
|
+
# 32-bytes sha256 of the the serialized event data.
|
7
|
+
# To obtain the event.id, we sha256 the serialized event. The serialization is done over the UTF-8 JSON-serialized
|
8
|
+
# string (with no white space or line breaks)
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
#
|
12
|
+
# @example Getting the event id
|
13
|
+
# event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
#
|
17
|
+
attr_reader :id
|
18
|
+
|
19
|
+
# 64-bytes signature of the sha256 hash of the serialized event data, which is
|
20
|
+
# the same as the "id" field
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
#
|
24
|
+
# @example Getting the event signature
|
25
|
+
# event.sig # => ''
|
26
|
+
#
|
27
|
+
# @return [String]
|
28
|
+
#
|
29
|
+
attr_reader :sig
|
30
|
+
|
31
|
+
# Instantiates a new Event
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
#
|
35
|
+
# @example Instantiating a new event
|
36
|
+
# Nostr::Event.new(
|
37
|
+
# id: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
38
|
+
# pubkey: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
|
39
|
+
# created_at: 1230981305,
|
40
|
+
# kind: 1,
|
41
|
+
# tags: [],
|
42
|
+
# content: 'Your feedback is appreciated, now pay $8',
|
43
|
+
# sig: '123ac2923b792ce730b3da34f16155470ab13c8f97f9c53eaeb334f1fb3a5dc9a7f643
|
44
|
+
# 937c6d6e9855477638f5655c5d89c9aa5501ea9b578a66aced4f1cd7b3'
|
45
|
+
# )
|
46
|
+
#
|
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
|
50
|
+
# the same as the "id" field
|
51
|
+
#
|
52
|
+
def initialize(id:, sig:, **kwargs)
|
53
|
+
super(**kwargs)
|
54
|
+
|
55
|
+
@id = id
|
56
|
+
@sig = sig
|
57
|
+
end
|
58
|
+
|
59
|
+
# Converts the event to a hash
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
#
|
63
|
+
# @example Converting the event to a hash
|
64
|
+
# event.to_h
|
65
|
+
#
|
66
|
+
# @return [Hash] The event as a hash.
|
67
|
+
#
|
68
|
+
def to_h
|
69
|
+
{
|
70
|
+
id:,
|
71
|
+
pubkey:,
|
72
|
+
created_at:,
|
73
|
+
kind:,
|
74
|
+
tags:,
|
75
|
+
content:,
|
76
|
+
sig:
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Compares two events. Returns true if all attributes are equal and false otherwise
|
81
|
+
#
|
82
|
+
# @api public
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# event1 == event2 # => true
|
86
|
+
#
|
87
|
+
# @return [Boolean] True if all attributes are equal and false otherwise
|
88
|
+
#
|
89
|
+
def ==(other)
|
90
|
+
to_h == other.to_h
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|