nostr 0.1.0 → 0.2.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 +9 -0
- data/README.md +165 -4
- 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 +28 -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 +12 -2
- metadata +145 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90770e269f0c966f76013797314b9e3b4b0743fa57f5b84f2460371b413bc5f1
|
4
|
+
data.tar.gz: 7a4a908a6c1b086781d26dc6f45c289a85859e12e1e854451a0e9f8e75f5fa06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 35a4401f078587bed79d6063f4051cca3f8dc170c074c552a202fee7872b75668eb8204a5f6db09f810ed0c5bb84026438d7e942f48a4825d63ba64552d032c7
|
7
|
+
data.tar.gz: b9400efc3228dea17e23e40803e2e7b94ce9966e6a729fcf5ea0b79ffcea07b7e51f7fd9703d01af1d287ffdaf85b6b8d49d325813b8e32518a0be240537e86e
|
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,15 @@ 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.2.0] - 2023-01-12
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) compliant client
|
12
|
+
|
7
13
|
## [0.1.0] - 2023-01-06
|
8
14
|
|
9
15
|
- Initial release
|
16
|
+
|
17
|
+
[0.2.0]: https://github.com/wilsonsilva/nostr/compare/v0.1.0...v0.2.0
|
18
|
+
[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,171 @@ 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
|
+
|
31
|
+
### Generating a keypair
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
keygen = Nostr::Keygen.new
|
35
|
+
keypair = keygen.generate_keypair
|
36
|
+
|
37
|
+
keypair.private_key
|
38
|
+
keypair.public_key
|
39
|
+
```
|
40
|
+
|
41
|
+
### Generating a private key and a public key
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
keygen = Nostr::Keygen.new
|
45
|
+
|
46
|
+
private_key = keygen.generate_private_key
|
47
|
+
public_key = keygen.extract_public_key(private_key)
|
48
|
+
```
|
49
|
+
|
50
|
+
### Connecting to a Relay
|
51
|
+
|
52
|
+
Clients can connect to multiple Relays. In this version, a Client can only connect to a single Relay at a time.
|
53
|
+
|
54
|
+
You may instantiate multiple Clients and multiple Relays.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
client = Nostr::Client.new
|
58
|
+
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
|
59
|
+
|
60
|
+
client.connect(relay)
|
61
|
+
```
|
62
|
+
|
63
|
+
### WebSocket events
|
64
|
+
|
65
|
+
All communication between clients and relays happen in WebSockets.
|
66
|
+
|
67
|
+
The `:connect` event is fired when a connection with a WebSocket is opened. You must call `Nostr::Client#connect` first.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
client.on :connect do
|
71
|
+
# all the code goes here
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
The `:close` event is fired when a connection with a WebSocket has been closed because of an error.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
client.on :error do |error_message|
|
79
|
+
puts error_message
|
80
|
+
end
|
81
|
+
|
82
|
+
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the server certificate for 'rsslay.fiatjaf.com'
|
83
|
+
```
|
84
|
+
|
85
|
+
The `:message` event is fired whenwhen data is received through a WebSocket.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
client.on :message do |message|
|
89
|
+
puts message
|
90
|
+
end
|
91
|
+
|
92
|
+
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the server certificate for 'rsslay.fiatjaf.com'
|
93
|
+
```
|
94
|
+
|
95
|
+
The `:close` event is fired when a connection with a WebSocket is closed.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
client.on :close do |code, reason|
|
99
|
+
# you may attempt to reconnect
|
100
|
+
|
101
|
+
client.connect(relay)
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
### Requesting for events / creating a subscription
|
106
|
+
|
107
|
+
A client can request events and subscribe to new updates after it has established a connection with the Relay.
|
108
|
+
|
109
|
+
You may use a `Nostr::Filter` instance with as many attributes as you wish:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
client.on :connect do
|
113
|
+
filter = Nostr::Filter.new(
|
114
|
+
ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
|
115
|
+
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
|
116
|
+
kinds: [Nostr::EventKind::TEXT_NOTE],
|
117
|
+
e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
|
118
|
+
p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
|
119
|
+
since: 1230981305,
|
120
|
+
until: 1292190341,
|
121
|
+
limit: 420,
|
122
|
+
)
|
123
|
+
|
124
|
+
subscription = client.subscribe('a_random_subscription_id', filter)
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
With just a few:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
client.on :connect do
|
132
|
+
filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
|
133
|
+
subscription = client.subscribe('a_random_subscription_id', filter)
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
Or omit the filter:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
client.on :connect do
|
141
|
+
subscription = client.subscribe('a_random_subscription_id')
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
Or even omit the subscription id:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
client.on :connect do
|
149
|
+
subscription = client.subscribe('a_random_subscription_id')
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
### Stop previous subscriptions
|
154
|
+
|
155
|
+
You can stop receiving messages from a subscription by calling `#unsubscribe`:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
client.unsubscribe('your_subscription_id')
|
159
|
+
```
|
160
|
+
|
161
|
+
### Publishing an event
|
162
|
+
|
163
|
+
To publish an event you need a keypair.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
# Set up the private key
|
167
|
+
private_key = 'a630b06e2f883378d0aa335b9adaf7734603e00433350b684fe53e184f08c58f'
|
168
|
+
user = Nostr::User.new(private_key)
|
169
|
+
|
170
|
+
# Create a signed event
|
171
|
+
event = user.create_event(
|
172
|
+
created_at: 1667422587, # optional, defaults to the current time
|
173
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
174
|
+
tags: [], # optional, defaults to []
|
175
|
+
content: 'Your feedback is appreciated, now pay $8'
|
176
|
+
)
|
177
|
+
|
178
|
+
# Send it to the Relay
|
179
|
+
client.publish(event)
|
180
|
+
```
|
181
|
+
|
182
|
+
## NIPS
|
183
|
+
|
184
|
+
- [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
185
|
+
- [ ] [NIP-01 - Relay](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
186
|
+
|
26
187
|
## Development
|
27
188
|
|
28
189
|
After checking out the repo, run `bin/setup` to install dependencies.
|
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
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Part of an +Event+. A complete +Event+ must have an +id+ and a +sig+.
|
5
|
+
class EventFragment
|
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 2
|
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
|
+
|
64
|
+
# Instantiates a new EventFragment
|
65
|
+
#
|
66
|
+
# @api public
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# Nostr::EventFragment.new(
|
70
|
+
# pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
71
|
+
# created_at: 1230981305,
|
72
|
+
# kind: 1,
|
73
|
+
# tags: [['e', '189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408']],
|
74
|
+
# content: 'Your feedback is appreciated, now pay $8'
|
75
|
+
# )
|
76
|
+
#
|
77
|
+
# @param pubkey [String] 32-bytes hex-encoded public key of the event creator.
|
78
|
+
# @param created_at [Integer] Date of the creation of the vent. A UNIX timestamp, in seconds.
|
79
|
+
# @param kind [Integer] The kind of the event. An integer from 0 to 2.
|
80
|
+
# @param tags [Array<Array>] An array of tags. Each tag is an array of strings.
|
81
|
+
# @param content [String] Arbitrary string.
|
82
|
+
#
|
83
|
+
def initialize(pubkey:, kind:, content:, created_at: Time.now.to_i, tags: [])
|
84
|
+
@pubkey = pubkey
|
85
|
+
@created_at = created_at
|
86
|
+
@kind = kind
|
87
|
+
@tags = tags
|
88
|
+
@content = content
|
89
|
+
end
|
90
|
+
|
91
|
+
# Serializes the event fragment, to obtain a SHA256 hash of it
|
92
|
+
#
|
93
|
+
# @api public
|
94
|
+
#
|
95
|
+
# @example Converting the event to a hash
|
96
|
+
# event_fragment.serialize
|
97
|
+
#
|
98
|
+
# @return [Array] The event fragment as an array.
|
99
|
+
#
|
100
|
+
def serialize
|
101
|
+
[
|
102
|
+
0,
|
103
|
+
pubkey,
|
104
|
+
created_at,
|
105
|
+
kind,
|
106
|
+
tags,
|
107
|
+
content
|
108
|
+
]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Defines the event kinds that can be emitted by clients.
|
5
|
+
module EventKind
|
6
|
+
# The content is set to a stringified JSON object +{name: <username>, about: <string>,
|
7
|
+
# picture: <url, string>}+ describing the user who created the event. A relay may delete past set_metadata
|
8
|
+
# events once it gets a new one for the same pubkey.
|
9
|
+
#
|
10
|
+
# @return [Integer]
|
11
|
+
#
|
12
|
+
SET_METADATA = 0
|
13
|
+
|
14
|
+
# The content is set to the text content of a note (anything the user wants to say).
|
15
|
+
# Non-plaintext notes should instead use kind 1000-10000 as described in NIP-16.
|
16
|
+
#
|
17
|
+
# @return [Integer]
|
18
|
+
#
|
19
|
+
TEXT_NOTE = 1
|
20
|
+
|
21
|
+
# The content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to
|
22
|
+
# recommend to its followers.
|
23
|
+
#
|
24
|
+
# @return [Integer]
|
25
|
+
#
|
26
|
+
RECOMMEND_SERVER = 2
|
27
|
+
end
|
28
|
+
end
|