nostrb 0.1.0.1 → 0.2.0.1
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/Rakefile +7 -2
- data/VERSION +1 -1
- data/lib/nostrb/client.rb +75 -0
- data/lib/nostrb/event.rb +25 -11
- data/lib/nostrb/filter.rb +5 -1
- data/lib/nostrb/json.rb +2 -2
- data/lib/nostrb/oj.rb +2 -2
- data/lib/nostrb/relay.rb +11 -11
- data/lib/nostrb/sequel.rb +6 -7
- data/lib/nostrb/source.rb +6 -6
- data/lib/nostrb/sqlite.rb +20 -19
- data/lib/nostrb.rb +34 -30
- data/test/relay.rb +41 -33
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a89ed8e93f661c8de971b8cba920363431d8a6a21ba22487877b09c041b25123
|
4
|
+
data.tar.gz: 4d2f947fc9244d1db2c003b27c004f7a666fa67e1f3e36283fecfb396228647c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d0a5f3137c9350da36a431790b3dc18dc3b0d38a32d605ba6bc3ee5c1c9b13950026e0aff037f486f55f6b5d0943323bbd4342de0e558289c7c785f7809c548
|
7
|
+
data.tar.gz: 6f835fda78aca5c38d43e05a73af41cc88a13a5ec7a880406bc7b35c9c984cddd0b5ebdce3a56b30faff9a276971584c750c419f010f0f5e52532465c63a2e53
|
data/Rakefile
CHANGED
@@ -5,11 +5,16 @@ Rake::TestTask.new :test do |t|
|
|
5
5
|
t.warning = true
|
6
6
|
end
|
7
7
|
|
8
|
+
Rake::TestTask.new :test_less do |t|
|
9
|
+
t.pattern = "test/{[!r][!e][!l][!a][!y]}*.rb"
|
10
|
+
t.warning = true
|
11
|
+
end
|
12
|
+
|
8
13
|
task :relay do |t|
|
9
|
-
sh "
|
14
|
+
sh "bundle exec falcon serve --bind wss://localhost:7070"
|
10
15
|
end
|
11
16
|
|
12
|
-
task default: [:test
|
17
|
+
task default: [:test]
|
13
18
|
|
14
19
|
begin
|
15
20
|
require 'buildar'
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0.1
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'async'
|
2
|
+
require 'async/http/endpoint'
|
3
|
+
require 'async/websocket/client'
|
4
|
+
|
5
|
+
require 'nostrb/source'
|
6
|
+
|
7
|
+
module Nostrb
|
8
|
+
class Client
|
9
|
+
attr_reader :url, :endpoint, :sid
|
10
|
+
|
11
|
+
def initialize(relay_url, sid: nil)
|
12
|
+
@url = relay_url
|
13
|
+
@endpoint = Async::HTTP::Endpoint.parse(@url)
|
14
|
+
@sid = sid.nil? ? Source.random_sid : sid
|
15
|
+
end
|
16
|
+
|
17
|
+
def log msg
|
18
|
+
warn msg
|
19
|
+
end
|
20
|
+
|
21
|
+
# open, send req, get response, return response
|
22
|
+
def single(req)
|
23
|
+
Sync do
|
24
|
+
Async::WebSocket::Client.connect(@endpoint) do |conn|
|
25
|
+
conn.write(Nostrb.json(req))
|
26
|
+
conn.flush
|
27
|
+
resp = conn.read
|
28
|
+
conn.shutdown
|
29
|
+
Nostrb.parse(resp.buffer)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def publish(signed_event)
|
35
|
+
case single(Nostrb::Source.publish(signed_event))
|
36
|
+
in ['OK', String => event_id, ok, String => msg]
|
37
|
+
log "id mismatch: #{event_id}" unless event_id == signed_event.id
|
38
|
+
log msg unless ok
|
39
|
+
ok
|
40
|
+
in ['NOTICE', String => msg]
|
41
|
+
log msg
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def subscribe(*filters, &blk)
|
46
|
+
Sync do
|
47
|
+
Async::WebSocket::Client.connect(@endpoint) do |conn|
|
48
|
+
conn.write(Nostrb.json(Nostrb::Source.subscribe(@sid, *filters)))
|
49
|
+
conn.flush
|
50
|
+
eose = false
|
51
|
+
while !eose and resp = conn.read
|
52
|
+
case Nostrb.parse(resp.buffer)
|
53
|
+
in ['EVENT', String => sid, Hash => event]
|
54
|
+
log "sid mismatch: #{sid}" unless sid == @sid
|
55
|
+
yield event
|
56
|
+
in ['EOSE', String => sid]
|
57
|
+
log "sid mismatch: #{sid}" unless sid == @sid
|
58
|
+
eose = true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
conn.shutdown
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def close
|
67
|
+
case single(Nostrb::Source.close(@sid))
|
68
|
+
in ['CLOSED', String => sid, String => msg]
|
69
|
+
log "sid mismatch: #{sid}" unless sid == @sid
|
70
|
+
log msg unless msg.empty?
|
71
|
+
true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/nostrb/event.rb
CHANGED
@@ -19,25 +19,38 @@ module Nostrb
|
|
19
19
|
tag_values('d', tags).first
|
20
20
|
end
|
21
21
|
|
22
|
+
def self.freeze_tags(tags)
|
23
|
+
tags.each { |a|
|
24
|
+
a.each { |s| s.freeze }
|
25
|
+
a.freeze
|
26
|
+
}
|
27
|
+
tags.freeze
|
28
|
+
end
|
29
|
+
|
22
30
|
attr_reader :content, :kind, :tags, :pk
|
23
31
|
|
24
32
|
def initialize(content = '', kind: 1, tags: [], pk:)
|
25
|
-
@content = Nostrb.txt!(content)
|
33
|
+
@content = Nostrb.txt!(content) # frozen
|
26
34
|
@kind = Nostrb.kind!(kind)
|
27
35
|
@tags = Nostrb.tags!(tags)
|
28
|
-
@pk = Nostrb.key!(pk)
|
36
|
+
@pk = Nostrb.key!(pk) # frozen
|
29
37
|
end
|
30
38
|
|
31
39
|
alias_method :to_s, :content
|
32
40
|
|
33
41
|
def serialize(created_at)
|
34
|
-
[0, self.pubkey, Nostrb.int!(created_at), @kind, @tags, @content]
|
42
|
+
[0, self.pubkey, Nostrb.int!(created_at), @kind, @tags, @content].freeze
|
43
|
+
end
|
44
|
+
|
45
|
+
def freeze
|
46
|
+
Event.freeze_tags(@tags)
|
47
|
+
self
|
35
48
|
end
|
36
49
|
|
37
50
|
def to_a = serialize(Time.now.to_i)
|
38
51
|
def pubkey = SchnorrSig.bin2hex(@pk)
|
39
52
|
def digest(created_at) = Event.digest(serialize(created_at))
|
40
|
-
def sign(sk) = SignedEvent.new(self, sk)
|
53
|
+
def sign(sk) = SignedEvent.new(self.freeze, sk)
|
41
54
|
|
42
55
|
#
|
43
56
|
# Tags
|
@@ -82,10 +95,11 @@ module Nostrb
|
|
82
95
|
Nostrb.pubkey!(parsed.fetch("pubkey"))
|
83
96
|
Nostrb.kind!(parsed.fetch("kind"))
|
84
97
|
Nostrb.tags!(parsed.fetch("tags"))
|
98
|
+
Event.freeze_tags(parsed['tags'])
|
85
99
|
Nostrb.int!(parsed.fetch("created_at"))
|
86
100
|
Nostrb.id!(parsed.fetch("id"))
|
87
101
|
Nostrb.sig!(parsed.fetch("sig"))
|
88
|
-
parsed
|
102
|
+
parsed.freeze
|
89
103
|
end
|
90
104
|
|
91
105
|
def self.digest(valid) = Nostrb.digest(Nostrb.json(serialize(valid)))
|
@@ -96,7 +110,7 @@ module Nostrb
|
|
96
110
|
valid["created_at"],
|
97
111
|
valid["kind"],
|
98
112
|
valid["tags"],
|
99
|
-
valid["content"], ]
|
113
|
+
valid["content"], ].freeze
|
100
114
|
end
|
101
115
|
|
102
116
|
# Validate the id (optional) and signature
|
@@ -127,8 +141,8 @@ module Nostrb
|
|
127
141
|
def initialize(event, sk)
|
128
142
|
@event = Nostrb.check!(event, Event)
|
129
143
|
@created_at = Time.now.to_i
|
130
|
-
@digest = @event.digest(@created_at)
|
131
|
-
@signature = SchnorrSig.sign(Nostrb.key!(sk), @digest)
|
144
|
+
@digest = @event.digest(@created_at).freeze
|
145
|
+
@signature = SchnorrSig.sign(Nostrb.key!(sk), @digest).freeze
|
132
146
|
end
|
133
147
|
|
134
148
|
def content = @event.content
|
@@ -138,8 +152,8 @@ module Nostrb
|
|
138
152
|
def to_s = @event.to_s
|
139
153
|
def serialize = @event.serialize(@created_at)
|
140
154
|
|
141
|
-
def id = SchnorrSig.bin2hex(@digest)
|
142
|
-
def sig = SchnorrSig.bin2hex(@signature)
|
155
|
+
def id = SchnorrSig.bin2hex(@digest).freeze
|
156
|
+
def sig = SchnorrSig.bin2hex(@signature).freeze
|
143
157
|
|
144
158
|
def to_h
|
145
159
|
Hash[ "content" => @event.content,
|
@@ -148,7 +162,7 @@ module Nostrb
|
|
148
162
|
"pubkey" => @event.pubkey,
|
149
163
|
"created_at" => @created_at,
|
150
164
|
"id" => self.id,
|
151
|
-
"sig" => self.sig ]
|
165
|
+
"sig" => self.sig ].freeze
|
152
166
|
end
|
153
167
|
end
|
154
168
|
end
|
data/lib/nostrb/filter.rb
CHANGED
@@ -71,6 +71,10 @@ module Nostrb
|
|
71
71
|
@limit = nil
|
72
72
|
end
|
73
73
|
|
74
|
+
def to_s
|
75
|
+
self.to_h.to_s
|
76
|
+
end
|
77
|
+
|
74
78
|
def add_ids(*event_ids)
|
75
79
|
@ids += event_ids.each { |id| Nostrb.id!(id) }
|
76
80
|
end
|
@@ -138,7 +142,7 @@ module Nostrb
|
|
138
142
|
h["since"] = @since unless @since.nil?
|
139
143
|
h["until"] = @until unless @until.nil?
|
140
144
|
h["limit"] = @limit unless @limit.nil?
|
141
|
-
h
|
145
|
+
h.freeze
|
142
146
|
end
|
143
147
|
end
|
144
148
|
end
|
data/lib/nostrb/json.rb
CHANGED
@@ -14,6 +14,6 @@ module Nostrb
|
|
14
14
|
space_before: '',
|
15
15
|
}
|
16
16
|
|
17
|
-
def self.parse(json) = JSON.parse(json, **JSON_OPTIONS)
|
18
|
-
def self.json(object) = JSON.generate(object, **JSON_OPTIONS)
|
17
|
+
def self.parse(json) = JSON.parse(json, **JSON_OPTIONS).freeze
|
18
|
+
def self.json(object) = JSON.generate(object, **JSON_OPTIONS).freeze
|
19
19
|
end
|
data/lib/nostrb/oj.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'oj'
|
2
2
|
|
3
3
|
module Nostrb
|
4
|
-
def self.parse(json) = Oj.load(json, mode: :strict)
|
5
|
-
def self.json(object) = Oj.dump(object, mode: :strict)
|
4
|
+
def self.parse(json) = Oj.load(json, mode: :strict).freeze
|
5
|
+
def self.json(object) = Oj.dump(object, mode: :strict).freeze
|
6
6
|
end
|
data/lib/nostrb/relay.rb
CHANGED
@@ -14,7 +14,7 @@ require 'set' # jruby wants this
|
|
14
14
|
# for replaceable events with same timestamp, lowest id wins
|
15
15
|
|
16
16
|
module Nostrb
|
17
|
-
class
|
17
|
+
class Relay
|
18
18
|
def self.event(sid, event) = ["EVENT", Nostrb.sid!(sid), event.to_h]
|
19
19
|
def self.ok(eid, msg = "", ok: true)
|
20
20
|
["OK", Nostrb.id!(eid), !!ok, ok ? Nostrb.txt!(msg) : Nostrb.help!(msg)]
|
@@ -58,10 +58,10 @@ module Nostrb
|
|
58
58
|
when 'CLOSE'
|
59
59
|
[handle_close(Nostrb.sid!(a[1]))]
|
60
60
|
else
|
61
|
-
[
|
61
|
+
[Relay.notice("unexpected: #{a[0].inspect}")]
|
62
62
|
end
|
63
63
|
rescue StandardError => e
|
64
|
-
[
|
64
|
+
[Relay.error(e)]
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
@@ -70,7 +70,7 @@ module Nostrb
|
|
70
70
|
begin
|
71
71
|
hsh = SignedEvent.validate!(hsh)
|
72
72
|
rescue Nostrb::Error, KeyError, RuntimeError => e
|
73
|
-
return
|
73
|
+
return Relay.error(e)
|
74
74
|
end
|
75
75
|
|
76
76
|
eid = hsh.fetch('id')
|
@@ -94,11 +94,11 @@ module Nostrb
|
|
94
94
|
raise(SignedEvent::Error, "kind: #{hsh['kind']}")
|
95
95
|
end
|
96
96
|
|
97
|
-
|
97
|
+
Relay.ok(eid)
|
98
98
|
rescue SignedEvent::Error => e
|
99
|
-
|
99
|
+
Relay.ok(eid, Relay.message(e), ok: false)
|
100
100
|
rescue Nostrb::Error, KeyError, RuntimeError => e
|
101
|
-
|
101
|
+
Relay.error(e)
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
@@ -124,19 +124,19 @@ module Nostrb
|
|
124
124
|
|
125
125
|
filters.each { |f|
|
126
126
|
@reader.process_events(f).each { |h|
|
127
|
-
responses <<
|
127
|
+
responses << Relay.event(sid, h) if f.match? h
|
128
128
|
}
|
129
129
|
@reader.process_r_events(f).each { |h|
|
130
|
-
responses <<
|
130
|
+
responses << Relay.event(sid, h) if f.match? h
|
131
131
|
}
|
132
132
|
}
|
133
133
|
responses = responses.to_a
|
134
|
-
responses <<
|
134
|
+
responses << Relay.eose(sid)
|
135
135
|
end
|
136
136
|
|
137
137
|
# single response
|
138
138
|
def handle_close(sid)
|
139
|
-
|
139
|
+
Relay.closed(sid, "reason: CLOSE requested")
|
140
140
|
end
|
141
141
|
end
|
142
142
|
end
|
data/lib/nostrb/sequel.rb
CHANGED
@@ -4,7 +4,7 @@ require 'nostrb/sqlite'
|
|
4
4
|
module Nostrb
|
5
5
|
module Sequel
|
6
6
|
class Storage < SQLite::Storage
|
7
|
-
FILENAME = 'sequel.db'
|
7
|
+
FILENAME = 'sequel.tmp.db'
|
8
8
|
TABLES = [:events, :tags, :r_events, :r_tags]
|
9
9
|
|
10
10
|
def self.schema_line(col, cfg)
|
@@ -77,7 +77,7 @@ module Nostrb
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def create_tables
|
80
|
-
@db.create_table :events do
|
80
|
+
@db.create_table :events, strict: true do
|
81
81
|
text :content, null: false
|
82
82
|
int :kind, null: false
|
83
83
|
text :tags, null: false
|
@@ -89,8 +89,7 @@ module Nostrb
|
|
89
89
|
text :sig, null: false
|
90
90
|
end
|
91
91
|
|
92
|
-
@db.create_table :tags do
|
93
|
-
# text :event_id, null: false # fk
|
92
|
+
@db.create_table :tags, strict: true do
|
94
93
|
foreign_key :event_id, :events,
|
95
94
|
key: :id,
|
96
95
|
type: :text,
|
@@ -103,7 +102,7 @@ module Nostrb
|
|
103
102
|
text :json, null: false
|
104
103
|
end
|
105
104
|
|
106
|
-
@db.create_table :r_events do
|
105
|
+
@db.create_table :r_events, strict: true do
|
107
106
|
text :content, null: false
|
108
107
|
int :kind, null: false
|
109
108
|
text :tags, null: false
|
@@ -120,7 +119,7 @@ module Nostrb
|
|
120
119
|
}
|
121
120
|
end
|
122
121
|
|
123
|
-
@db.create_table :r_tags do
|
122
|
+
@db.create_table :r_tags, strict: true do
|
124
123
|
foreign_key :r_event_id, :r_events,
|
125
124
|
key: :id,
|
126
125
|
type: :text,
|
@@ -145,7 +144,7 @@ module Nostrb
|
|
145
144
|
'pubkey' => event_row.fetch(:pubkey),
|
146
145
|
'created_at' => event_row.fetch(:created_at),
|
147
146
|
'id' => event_row.fetch(:id),
|
148
|
-
'sig' => event_row.fetch(:sig), ]
|
147
|
+
'sig' => event_row.fetch(:sig), ].freeze
|
149
148
|
end
|
150
149
|
|
151
150
|
def self.event_clauses(filter)
|
data/lib/nostrb/source.rb
CHANGED
@@ -11,30 +11,30 @@ module Nostrb
|
|
11
11
|
#######################
|
12
12
|
# Client Requests
|
13
13
|
|
14
|
-
def self.publish(signed) = ["EVENT", signed.to_h]
|
14
|
+
def self.publish(signed) = ["EVENT", signed.to_h].freeze
|
15
15
|
|
16
16
|
def self.subscribe(sid, *filters)
|
17
17
|
["REQ", Nostrb.sid!(sid), *filters.map { |f|
|
18
18
|
Nostrb.check!(f, Filter).to_h
|
19
|
-
}]
|
19
|
+
}].freeze
|
20
20
|
end
|
21
21
|
|
22
|
-
def self.close(sid) = ["CLOSE", Nostrb.sid!(sid)]
|
22
|
+
def self.close(sid) = ["CLOSE", Nostrb.sid!(sid)].freeze
|
23
23
|
|
24
24
|
#######################
|
25
25
|
# Utils / Init
|
26
26
|
|
27
27
|
def self.random_sid
|
28
|
-
SchnorrSig.bin2hex
|
28
|
+
SchnorrSig.bin2hex(Random.bytes(32)).freeze
|
29
29
|
end
|
30
30
|
|
31
31
|
attr_reader :pk
|
32
32
|
|
33
33
|
def initialize(pk)
|
34
|
-
@pk = Nostrb.key!(pk)
|
34
|
+
@pk = Nostrb.key!(pk).freeze
|
35
35
|
end
|
36
36
|
|
37
|
-
def pubkey = SchnorrSig.bin2hex(@pk)
|
37
|
+
def pubkey = SchnorrSig.bin2hex(@pk).freeze
|
38
38
|
|
39
39
|
############################
|
40
40
|
# Event Creation
|
data/lib/nostrb/sqlite.rb
CHANGED
@@ -112,7 +112,7 @@ module Nostrb
|
|
112
112
|
MB = KB * 1024
|
113
113
|
GB = MB * 1024
|
114
114
|
|
115
|
-
FILENAME = 'tmp.db'
|
115
|
+
FILENAME = 'sqlite.tmp.db'
|
116
116
|
CONFIG = {
|
117
117
|
default_transaction_mode: :immediate,
|
118
118
|
}
|
@@ -241,7 +241,7 @@ CREATE TABLE events (content TEXT NOT NULL,
|
|
241
241
|
pubkey TEXT NOT NULL,
|
242
242
|
created_at INT NOT NULL,
|
243
243
|
id TEXT PRIMARY KEY NOT NULL,
|
244
|
-
sig TEXT NOT NULL)
|
244
|
+
sig TEXT NOT NULL) STRICT
|
245
245
|
SQL
|
246
246
|
|
247
247
|
@db.execute <<SQL
|
@@ -250,7 +250,7 @@ CREATE TABLE tags (event_id TEXT NOT NULL REFERENCES events (id)
|
|
250
250
|
created_at INT NOT NULL,
|
251
251
|
tag TEXT NOT NULL,
|
252
252
|
value TEXT NOT NULL,
|
253
|
-
json TEXT NOT NULL)
|
253
|
+
json TEXT NOT NULL) STRICT
|
254
254
|
SQL
|
255
255
|
|
256
256
|
@db.execute <<SQL
|
@@ -261,7 +261,7 @@ CREATE TABLE r_events (content TEXT NOT NULL,
|
|
261
261
|
pubkey TEXT NOT NULL,
|
262
262
|
created_at INT NOT NULL,
|
263
263
|
id TEXT PRIMARY KEY NOT NULL,
|
264
|
-
sig TEXT NOT NULL)
|
264
|
+
sig TEXT NOT NULL) STRICT
|
265
265
|
SQL
|
266
266
|
|
267
267
|
@db.execute <<SQL
|
@@ -270,7 +270,7 @@ CREATE TABLE r_tags (r_event_id TEXT NOT NULL REFERENCES r_events (id)
|
|
270
270
|
created_at INT NOT NULL,
|
271
271
|
tag TEXT NOT NULL,
|
272
272
|
value TEXT NOT NULL,
|
273
|
-
json TEXT NOT NULL)
|
273
|
+
json TEXT NOT NULL) STRICT
|
274
274
|
SQL
|
275
275
|
end
|
276
276
|
|
@@ -394,6 +394,10 @@ SQL
|
|
394
394
|
end
|
395
395
|
|
396
396
|
class Writer < Storage
|
397
|
+
def self.serialize_tags(valid)
|
398
|
+
valid.merge("tags" => Nostrb.json(valid["tags"]))
|
399
|
+
end
|
400
|
+
|
397
401
|
# a valid hash, as returned from SignedEvent.validate!
|
398
402
|
def add_event(valid)
|
399
403
|
@add_event ||= @db.prepare("INSERT INTO events
|
@@ -402,12 +406,10 @@ SQL
|
|
402
406
|
@add_tag ||= @db.prepare("INSERT INTO tags
|
403
407
|
VALUES (:event_id, :created_at,
|
404
408
|
:tag, :value, :json)")
|
405
|
-
|
406
|
-
valid["tags"]
|
407
|
-
|
408
|
-
|
409
|
-
@add_tag.execute(event_id: valid.fetch('id'),
|
410
|
-
created_at: valid.fetch('created_at'),
|
409
|
+
@add_event.execute(Writer.serialize_tags(valid)) # insert event
|
410
|
+
valid["tags"].each { |a| # insert tags
|
411
|
+
@add_tag.execute(event_id: valid['id'],
|
412
|
+
created_at: valid['created_at'],
|
411
413
|
tag: a[0],
|
412
414
|
value: a[1],
|
413
415
|
json: Nostrb.json(a))
|
@@ -423,14 +425,13 @@ SQL
|
|
423
425
|
@add_rtag ||= @db.prepare("INSERT INTO r_tags
|
424
426
|
VALUES (:r_event_id, :created_at,
|
425
427
|
:tag, :value, :json)")
|
426
|
-
tags = valid
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
created_at: valid.fetch('created_at'),
|
428
|
+
tags = valid['tags']
|
429
|
+
record = Writer.serialize_tags(valid)
|
430
|
+
record['d_tag'] = Event.d_tag(tags)
|
431
|
+
@add_r_event.execute(record) # upsert event
|
432
|
+
tags.each { |a| # insert tags
|
433
|
+
@add_rtag.execute(r_event_id: valid['id'],
|
434
|
+
created_at: valid['created_at'],
|
434
435
|
tag: a[0],
|
435
436
|
value: a[1],
|
436
437
|
json: Nostrb.json(a))
|
data/lib/nostrb.rb
CHANGED
@@ -7,14 +7,12 @@ rescue LoadError
|
|
7
7
|
end
|
8
8
|
|
9
9
|
module Nostrb
|
10
|
-
GEMS = %w[rbsecp256k1 oj sqlite3 sequel]
|
11
|
-
|
12
10
|
class Error < RuntimeError; end
|
13
11
|
class SizeError < Error; end
|
14
12
|
class FormatError < Error; end
|
15
13
|
|
16
14
|
# return 32 bytes binary
|
17
|
-
def self.digest(str) = Digest::SHA256.digest(str)
|
15
|
+
def self.digest(str) = Digest::SHA256.digest(str).freeze
|
18
16
|
|
19
17
|
def self.check!(val, cls)
|
20
18
|
val.is_a?(cls) ? val : raise(TypeError, "#{cls} expected: #{val.inspect}")
|
@@ -45,7 +43,7 @@ module Nostrb
|
|
45
43
|
end
|
46
44
|
raise(SizeError, str.length) if !length.nil? and str.length != length
|
47
45
|
raise(SizeError, str.length) if !max.nil? and str.length > max
|
48
|
-
str
|
46
|
+
str.freeze
|
49
47
|
end
|
50
48
|
|
51
49
|
def self.bin!(str, length: nil, max: nil)
|
@@ -58,6 +56,7 @@ module Nostrb
|
|
58
56
|
str!(str, binary: false, length: length, max: max)
|
59
57
|
end
|
60
58
|
def self.pubkey!(str) = txt!(str, length: 64)
|
59
|
+
|
61
60
|
def self.id!(str) = txt!(str, length: 64)
|
62
61
|
def self.sid!(str) = txt!(str, max: 64)
|
63
62
|
def self.sig!(str) = txt!(str, length: 128)
|
@@ -73,39 +72,44 @@ module Nostrb
|
|
73
72
|
ary!(ary, max: 9999).each { |a| ary!(a, max: 99).each { |s| txt!(s) } }
|
74
73
|
end
|
75
74
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
75
|
+
# optional dependencies
|
76
|
+
module Optional
|
77
|
+
GEMS = %w[rbsecp256k1 oj sqlite3 sequel]
|
78
|
+
|
79
|
+
def self.rbsecp256k1?
|
80
|
+
begin
|
81
|
+
require 'rbsecp256k1'; ::Secp256k1
|
82
|
+
rescue LoadError, NameError
|
83
|
+
false
|
84
|
+
end
|
81
85
|
end
|
82
|
-
end
|
83
86
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
87
|
+
def self.oj?
|
88
|
+
begin
|
89
|
+
require 'oj'; ::Oj
|
90
|
+
rescue LoadError, NameError
|
91
|
+
false
|
92
|
+
end
|
89
93
|
end
|
90
|
-
end
|
91
94
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
95
|
+
def self.sqlite3?
|
96
|
+
begin
|
97
|
+
require 'sqlite3'; ::SQLite3
|
98
|
+
rescue LoadError, NameError
|
99
|
+
false
|
100
|
+
end
|
97
101
|
end
|
98
|
-
end
|
99
102
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
103
|
+
def self.sequel?
|
104
|
+
begin
|
105
|
+
require 'sequel'; ::Sequel
|
106
|
+
rescue LoadError, NameError
|
107
|
+
false
|
108
|
+
end
|
105
109
|
end
|
106
|
-
end
|
107
110
|
|
108
|
-
|
109
|
-
|
111
|
+
def self.gem_check
|
112
|
+
GEMS.map { |gem| [gem, self.send("#{gem}?")] }.to_h
|
113
|
+
end
|
110
114
|
end
|
111
115
|
end
|
data/test/relay.rb
CHANGED
@@ -12,7 +12,7 @@ DB_FILE = ENV['INPUT_DB_FILE'] || 'testing.db'
|
|
12
12
|
# use SQLite backing
|
13
13
|
SQLite::Setup.new(DB_FILE).setup
|
14
14
|
|
15
|
-
describe
|
15
|
+
describe Relay do
|
16
16
|
def valid_response!(resp)
|
17
17
|
types = ["EVENT", "OK", "EOSE", "CLOSED", "NOTICE"]
|
18
18
|
expect(resp).must_be_kind_of Array
|
@@ -26,7 +26,7 @@ describe Server do
|
|
26
26
|
describe "class functions" do
|
27
27
|
it "has an EVENT response, given subscriber_id and requested event" do
|
28
28
|
sid = '1234'
|
29
|
-
resp =
|
29
|
+
resp = Relay.event(sid, Test::SIGNED)
|
30
30
|
valid_response!(resp)
|
31
31
|
expect(resp[0]).must_equal "EVENT"
|
32
32
|
expect(resp[1]).must_equal sid
|
@@ -36,7 +36,7 @@ describe Server do
|
|
36
36
|
|
37
37
|
it "has an OK response, given an event_id" do
|
38
38
|
# positive ok
|
39
|
-
resp =
|
39
|
+
resp = Relay.ok(Test::SIGNED.id)
|
40
40
|
valid_response!(resp)
|
41
41
|
expect(resp[0]).must_equal "OK"
|
42
42
|
expect(resp[1]).must_equal Test::SIGNED.id
|
@@ -44,7 +44,7 @@ describe Server do
|
|
44
44
|
expect(resp[3]).must_be_kind_of String # empty by default
|
45
45
|
|
46
46
|
# negative ok
|
47
|
-
resp =
|
47
|
+
resp = Relay.ok(Test::SIGNED.id, "error: testing", ok: false)
|
48
48
|
valid_response!(resp)
|
49
49
|
expect(resp[0]).must_equal "OK"
|
50
50
|
expect(resp[1]).must_equal Test::SIGNED.id
|
@@ -54,14 +54,14 @@ describe Server do
|
|
54
54
|
|
55
55
|
# ok:false requires nonempty message
|
56
56
|
expect {
|
57
|
-
|
57
|
+
Relay.ok(Test::SIGNED.id, "", ok: false)
|
58
58
|
}.must_raise FormatError
|
59
|
-
expect {
|
59
|
+
expect { Relay.ok(Test::SIGNED.id, ok: false) }.must_raise FormatError
|
60
60
|
end
|
61
61
|
|
62
62
|
it "has an EOSE response to conclude a series of EVENT responses" do
|
63
63
|
sid = '1234'
|
64
|
-
resp =
|
64
|
+
resp = Relay.eose(sid)
|
65
65
|
valid_response!(resp)
|
66
66
|
expect(resp[0]).must_equal "EOSE"
|
67
67
|
expect(resp[1]).must_equal sid
|
@@ -70,7 +70,7 @@ describe Server do
|
|
70
70
|
it "has a CLOSED response to shut down a subscriber" do
|
71
71
|
sid = '1234'
|
72
72
|
msg = "closed: bye"
|
73
|
-
resp =
|
73
|
+
resp = Relay.closed(sid, msg)
|
74
74
|
valid_response!(resp)
|
75
75
|
expect(resp[0]).must_equal "CLOSED"
|
76
76
|
expect(resp[1]).must_equal sid
|
@@ -80,7 +80,7 @@ describe Server do
|
|
80
80
|
it "has a NOTICE response to provide any message to the user" do
|
81
81
|
msg = "all i ever really wanna do is get nice, " +
|
82
82
|
"get loose and goof this little slice of life"
|
83
|
-
resp =
|
83
|
+
resp = Relay.notice(msg)
|
84
84
|
valid_response!(resp)
|
85
85
|
expect(resp[0]).must_equal "NOTICE"
|
86
86
|
expect(resp[1]).must_equal msg
|
@@ -89,16 +89,16 @@ describe Server do
|
|
89
89
|
it "formats Exceptions to a common string representation" do
|
90
90
|
r = RuntimeError.new("stuff")
|
91
91
|
expect(r).must_be_kind_of Exception
|
92
|
-
expect(
|
92
|
+
expect(Relay.message(r)).must_equal "RuntimeError: stuff"
|
93
93
|
|
94
94
|
e = Nostrb::Error.new("things")
|
95
95
|
expect(e).must_be_kind_of Exception
|
96
|
-
expect(
|
96
|
+
expect(Relay.message(e)).must_equal "Error: things"
|
97
97
|
end
|
98
98
|
|
99
99
|
it "uses NOTICE to return errors" do
|
100
100
|
e = RuntimeError.new "stuff"
|
101
|
-
resp =
|
101
|
+
resp = Relay.error(e)
|
102
102
|
valid_response!(resp)
|
103
103
|
expect(resp[0]).must_equal "NOTICE"
|
104
104
|
expect(resp[1]).must_equal "RuntimeError: stuff"
|
@@ -106,14 +106,14 @@ describe Server do
|
|
106
106
|
end
|
107
107
|
|
108
108
|
it "has no initialization parameters" do
|
109
|
-
s =
|
110
|
-
expect(s).must_be_kind_of
|
109
|
+
s = Relay.new(DB_FILE)
|
110
|
+
expect(s).must_be_kind_of Relay
|
111
111
|
end
|
112
112
|
|
113
113
|
# respond OK: true
|
114
114
|
it "has a single response to EVENT requests" do
|
115
115
|
json = Nostrb.json(Source.publish(Test::SIGNED))
|
116
|
-
responses =
|
116
|
+
responses = Relay.new(DB_FILE).ingest(json)
|
117
117
|
expect(responses).must_be_kind_of Array
|
118
118
|
expect(responses.length).must_equal 1
|
119
119
|
|
@@ -126,7 +126,7 @@ describe Server do
|
|
126
126
|
|
127
127
|
# store and retrieve with a subscription filter
|
128
128
|
it "stores inbound events" do
|
129
|
-
s =
|
129
|
+
s = Relay.new(DB_FILE)
|
130
130
|
sk, pk = SchnorrSig.keypair
|
131
131
|
e = Event.new('sqlite', pk: pk).sign(sk)
|
132
132
|
resp = s.ingest Nostrb.json(Source.publish(e))
|
@@ -152,7 +152,7 @@ describe Server do
|
|
152
152
|
end
|
153
153
|
|
154
154
|
it "has multiple responses to REQ requets" do
|
155
|
-
s =
|
155
|
+
s = Relay.new(DB_FILE)
|
156
156
|
sk, pk = SchnorrSig.keypair
|
157
157
|
e = Event.new('first', pk: pk).sign(sk)
|
158
158
|
resp = s.ingest Nostrb.json(Source.publish(e))
|
@@ -204,7 +204,7 @@ describe Server do
|
|
204
204
|
end
|
205
205
|
|
206
206
|
it "has a single response to CLOSE requests" do
|
207
|
-
s =
|
207
|
+
s = Relay.new(DB_FILE)
|
208
208
|
sid = Test::EVENT.pubkey
|
209
209
|
responses = s.ingest(Nostrb.json(Source.close(sid)))
|
210
210
|
|
@@ -221,9 +221,9 @@ describe Server do
|
|
221
221
|
describe "error handling" do
|
222
222
|
# invalid request type
|
223
223
|
it "handles unknown unknown request types with an error notice" do
|
224
|
-
a = Source.publish
|
224
|
+
a = Source.publish(Test::SIGNED).dup
|
225
225
|
a[0] = 'NONSENSE'
|
226
|
-
responses =
|
226
|
+
responses = Relay.new(DB_FILE).ingest(Nostrb.json(a))
|
227
227
|
expect(responses).must_be_kind_of Array
|
228
228
|
expect(responses.length).must_equal 1
|
229
229
|
|
@@ -236,10 +236,10 @@ describe Server do
|
|
236
236
|
|
237
237
|
# replace leading open brace with space
|
238
238
|
it "handles JSON parse errors with an error notice" do
|
239
|
-
j = Nostrb.json(Nostrb::Source.publish(Test::SIGNED))
|
239
|
+
j = Nostrb.json(Nostrb::Source.publish(Test::SIGNED)).dup
|
240
240
|
expect(j[9]).must_equal '{'
|
241
241
|
j[9] = ' '
|
242
|
-
resp =
|
242
|
+
resp = Relay.new(DB_FILE).ingest(j)
|
243
243
|
expect(resp).must_be_kind_of Array
|
244
244
|
expect(resp.length).must_equal 1
|
245
245
|
|
@@ -251,11 +251,12 @@ describe Server do
|
|
251
251
|
|
252
252
|
# add "stuff":"things"
|
253
253
|
it "handles unexpected fields with an error notice" do
|
254
|
-
a = Nostrb::Source.publish(Test::SIGNED)
|
254
|
+
a = Nostrb::Source.publish(Test::SIGNED).dup
|
255
255
|
expect(a[1]).must_be_kind_of Hash
|
256
|
+
a[1] = a[1].dup
|
256
257
|
a[1]["stuff"] = "things"
|
257
258
|
|
258
|
-
resp =
|
259
|
+
resp = Relay.new(DB_FILE).ingest(Nostrb.json(a))
|
259
260
|
expect(resp).must_be_kind_of Array
|
260
261
|
expect(resp.length).must_equal 1
|
261
262
|
|
@@ -269,9 +270,11 @@ describe Server do
|
|
269
270
|
it "handles missing fields with an error notice" do
|
270
271
|
a = Nostrb::Source.publish(Test::SIGNED)
|
271
272
|
expect(a[1]).must_be_kind_of Hash
|
273
|
+
a = a.dup
|
274
|
+
a[1] = a[1].dup
|
272
275
|
a[1].delete("tags")
|
273
276
|
|
274
|
-
resp =
|
277
|
+
resp = Relay.new(DB_FILE).ingest(Nostrb.json(a))
|
275
278
|
expect(resp).must_be_kind_of Array
|
276
279
|
expect(resp.length).must_equal 1
|
277
280
|
|
@@ -283,11 +286,12 @@ describe Server do
|
|
283
286
|
|
284
287
|
# cut "id" in half
|
285
288
|
it "handles field format errors with an error notice" do
|
286
|
-
a = Nostrb::Source.publish(Test::SIGNED)
|
289
|
+
a = Nostrb::Source.publish(Test::SIGNED).dup
|
287
290
|
expect(a[1]).must_be_kind_of Hash
|
291
|
+
a[1] = a[1].dup
|
288
292
|
a[1]["id"] = a[1]["id"].slice(0, 32)
|
289
293
|
|
290
|
-
resp =
|
294
|
+
resp = Relay.new(DB_FILE).ingest(Nostrb.json(a))
|
291
295
|
expect(resp).must_be_kind_of Array
|
292
296
|
expect(resp.length).must_equal 1
|
293
297
|
|
@@ -301,9 +305,11 @@ describe Server do
|
|
301
305
|
it "handles invalid signature with OK:false" do
|
302
306
|
a = Nostrb::Source.publish(Test::SIGNED)
|
303
307
|
expect(a[1]).must_be_kind_of Hash
|
308
|
+
a = a.dup
|
309
|
+
a[1] = a[1].dup
|
304
310
|
a[1]["sig"] = SchnorrSig.bin2hex(Random.bytes(64))
|
305
311
|
|
306
|
-
resp =
|
312
|
+
resp = Relay.new(DB_FILE).ingest(Nostrb.json(a))
|
307
313
|
expect(resp).must_be_kind_of Array
|
308
314
|
expect(resp.length).must_equal 1
|
309
315
|
|
@@ -318,16 +324,17 @@ describe Server do
|
|
318
324
|
|
319
325
|
# "id" and "sig" spoofed from another event
|
320
326
|
it "handles spoofed id with OK:false" do
|
321
|
-
orig = Source.publish(Test.new_event('orig'))
|
322
|
-
spoof = Source.publish(Test::SIGNED)
|
327
|
+
orig = Source.publish(Test.new_event('orig')).dup
|
328
|
+
spoof = Source.publish(Test::SIGNED).dup
|
323
329
|
|
330
|
+
orig[1] = orig[1].dup
|
324
331
|
orig[1]["id"] = spoof[1]["id"]
|
325
332
|
orig[1]["sig"] = spoof[1]["sig"]
|
326
333
|
|
327
334
|
# now sig and id agree with each other, but not orig's content/metadata
|
328
335
|
# the signature should verify, but the id should not
|
329
336
|
|
330
|
-
resp =
|
337
|
+
resp = Relay.new(DB_FILE).ingest(Nostrb.json(orig))
|
331
338
|
expect(resp).must_be_kind_of Array
|
332
339
|
expect(resp.length).must_equal 1
|
333
340
|
|
@@ -342,10 +349,11 @@ describe Server do
|
|
342
349
|
|
343
350
|
# random "id"
|
344
351
|
it "handles invalid id with OK:false" do
|
345
|
-
a = Source.publish(Test::SIGNED)
|
352
|
+
a = Source.publish(Test::SIGNED).dup
|
353
|
+
a[1] = a[1].dup
|
346
354
|
a[1]["id"] = SchnorrSig.bin2hex(Random.bytes(32))
|
347
355
|
|
348
|
-
resp =
|
356
|
+
resp = Relay.new(DB_FILE).ingest(Nostrb.json(a))
|
349
357
|
expect(resp).must_be_kind_of Array
|
350
358
|
expect(resp.length).must_equal 1
|
351
359
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nostrb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rick Hull
|
@@ -33,6 +33,7 @@ files:
|
|
33
33
|
- Rakefile
|
34
34
|
- VERSION
|
35
35
|
- lib/nostrb.rb
|
36
|
+
- lib/nostrb/client.rb
|
36
37
|
- lib/nostrb/event.rb
|
37
38
|
- lib/nostrb/filter.rb
|
38
39
|
- lib/nostrb/json.rb
|