nostrb 0.1.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 +7 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/lib/nostrb/event.rb +154 -0
- data/lib/nostrb/filter.rb +144 -0
- data/lib/nostrb/json.rb +19 -0
- data/lib/nostrb/names.rb +71 -0
- data/lib/nostrb/oj.rb +6 -0
- data/lib/nostrb/relay.rb +142 -0
- data/lib/nostrb/sequel.rb +222 -0
- data/lib/nostrb/source.rb +111 -0
- data/lib/nostrb/sqlite.rb +441 -0
- data/lib/nostrb.rb +111 -0
- data/nostrb.gemspec +18 -0
- data/test/common.rb +25 -0
- data/test/event.rb +193 -0
- data/test/nostrb.rb +87 -0
- data/test/relay.rb +360 -0
- data/test/source.rb +136 -0
- metadata +74 -0
@@ -0,0 +1,441 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
require 'nostrb/filter'
|
3
|
+
|
4
|
+
module Nostrb
|
5
|
+
module SQLite
|
6
|
+
class Pragma
|
7
|
+
ENUM = {
|
8
|
+
'auto_vacuum' => %w[none full incremental],
|
9
|
+
'synchronous' => %w[off normal full],
|
10
|
+
'temp_store' => %w[default file memory],
|
11
|
+
}
|
12
|
+
|
13
|
+
RO = %w[data_version freelist_count page_count]
|
14
|
+
RW = %w[application_id analysis_limit auto_vacuum automatic_index
|
15
|
+
busy_timeout cache_size cache_spill cell_size_check
|
16
|
+
checkpoint_fullfsync defer_foreign_keys encoding foreign_keys
|
17
|
+
fullfsync hard_heap_limit ignore_check_constraints journal_mode
|
18
|
+
journal_size_limit locking_mode max_page_count mmap_size
|
19
|
+
page_size query_only read_uncommitted recursive_triggers
|
20
|
+
reverse_unordered_selects secure_delete soft_heap_limit
|
21
|
+
synchronous temp_store threads trusted_schema user_version
|
22
|
+
wal_autocheckpoint]
|
23
|
+
SCALAR = (RO + RW).sort
|
24
|
+
|
25
|
+
# debug: parser_trace schema_version stats vdbe_* writable_schema
|
26
|
+
# legacy: legacy_alter_table legacy_file_format
|
27
|
+
# deprecated: case_sensitive_like count_changes data_store_directory
|
28
|
+
# default_cache_size empty_result_callbacks full_column_names
|
29
|
+
# short_column_names temp_store_directory
|
30
|
+
|
31
|
+
# either no args (nil) or a single arg (symbol)
|
32
|
+
REPORT = {
|
33
|
+
compile_options: nil,
|
34
|
+
# list
|
35
|
+
collation_list: nil,
|
36
|
+
database_list: nil,
|
37
|
+
function_list: nil,
|
38
|
+
module_list: nil,
|
39
|
+
pragma_list: nil,
|
40
|
+
table_list: nil,
|
41
|
+
index_list: :table_name,
|
42
|
+
foreign_key_list: :table_name,
|
43
|
+
# info
|
44
|
+
index_info: :index_name,
|
45
|
+
index_xinfo: :index_name,
|
46
|
+
table_info: :table_name,
|
47
|
+
table_xinfo: :table_name,
|
48
|
+
}
|
49
|
+
|
50
|
+
# either no args (nil) or an optional single arg (symbol)
|
51
|
+
COMMAND = {
|
52
|
+
# checks
|
53
|
+
foreign_key_check: :optional, # table_name => report
|
54
|
+
integrity_check: :optional, # table_name | num_errors => ok
|
55
|
+
quick_check: :optional, # table_name | num_errors => ok
|
56
|
+
# manipulation
|
57
|
+
incremental_vacuum: :optional, # page_count => empty
|
58
|
+
optimize: :optional, # mask => empty
|
59
|
+
shrink_memory: nil, # empty
|
60
|
+
wal_checkpoint: :optional, # PASSIVE | FULL | RESTART | TRUNCATE => row
|
61
|
+
}
|
62
|
+
|
63
|
+
def initialize(db)
|
64
|
+
@db = db
|
65
|
+
end
|
66
|
+
|
67
|
+
def get(pragma)
|
68
|
+
@db.execute("PRAGMA #{pragma}")[0][0]
|
69
|
+
end
|
70
|
+
|
71
|
+
def set(pragma, val)
|
72
|
+
@db.execute("PRAGMA #{pragma} = #{val}")
|
73
|
+
get(pragma)
|
74
|
+
end
|
75
|
+
|
76
|
+
# just the rows
|
77
|
+
def list(pragma, arg = nil)
|
78
|
+
if arg.nil?
|
79
|
+
@db.execute("PRAGMA #{pragma}")
|
80
|
+
else
|
81
|
+
@db.execute("PRAGMA #{pragma}(#{arg})")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# include a header row
|
86
|
+
def report(pragma, arg = nil)
|
87
|
+
if arg.nil?
|
88
|
+
@db.execute2("PRAGMA #{pragma}")
|
89
|
+
else
|
90
|
+
@db.execute2("PRAGMA #{pragma}(#{arg})")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
SCALAR.each { |pragma| define_method(pragma) { get(pragma) }}
|
95
|
+
RW.each { |pragma|
|
96
|
+
define_method(pragma + '=') { |val| set(pragma, val) }
|
97
|
+
}
|
98
|
+
|
99
|
+
(REPORT.merge(COMMAND)).each { |pragma, arg|
|
100
|
+
if arg.nil?
|
101
|
+
define_method(pragma) { report(pragma) }
|
102
|
+
elsif arg == :optional
|
103
|
+
define_method(pragma) { |val=nil| report(pragma, val) }
|
104
|
+
else
|
105
|
+
define_method(pragma) { |val| report(pragma, val) }
|
106
|
+
end
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
class Storage
|
111
|
+
KB = 1024
|
112
|
+
MB = KB * 1024
|
113
|
+
GB = MB * 1024
|
114
|
+
|
115
|
+
FILENAME = 'tmp.db'
|
116
|
+
CONFIG = {
|
117
|
+
default_transaction_mode: :immediate,
|
118
|
+
}
|
119
|
+
SQLITE_USAGE = /\Asqlite_/
|
120
|
+
|
121
|
+
PRAGMAS = {
|
122
|
+
foreign_keys: true, # enable FK constraints
|
123
|
+
mmap_size: 128 * MB, # enable mmap I/O, 128 MB
|
124
|
+
|
125
|
+
# Write Ahead Log, append-only so safe for infrequent fsync
|
126
|
+
journal_mode: 'wal', # enable WAL, less read/write contention
|
127
|
+
journal_size_limit: 64 * MB, # enable, 64 MB
|
128
|
+
synchronous: 1, # 1=normal, default, good for WAL
|
129
|
+
wal_autocheckpoint: 1000, # default, pages per fsync
|
130
|
+
}
|
131
|
+
|
132
|
+
attr_reader :filename, :db, :pragma
|
133
|
+
|
134
|
+
def initialize(filename = FILENAME, set_pragmas: true, **kwargs)
|
135
|
+
@filename = filename
|
136
|
+
@db = SQLite3::Database.new(@filename, **CONFIG.merge(kwargs))
|
137
|
+
@db.busy_handler_timeout = 5000 # 5 seconds, release GVL every ms
|
138
|
+
@pragma = Pragma.new(@db)
|
139
|
+
self.set_pragmas if set_pragmas
|
140
|
+
end
|
141
|
+
|
142
|
+
def set_pragmas
|
143
|
+
PRAGMAS.each { |name, val| @pragma.set(name, val) }
|
144
|
+
end
|
145
|
+
|
146
|
+
def setup
|
147
|
+
Setup.new(@filename)
|
148
|
+
end
|
149
|
+
|
150
|
+
def reader
|
151
|
+
Reader.new(@filename)
|
152
|
+
end
|
153
|
+
|
154
|
+
def writer
|
155
|
+
Writer.new(@filename)
|
156
|
+
end
|
157
|
+
|
158
|
+
# below methods all return an array of strings
|
159
|
+
def compile_options = @pragma.list(:compile_options).map { |a| a[0] }
|
160
|
+
def database_files = @pragma.list(:database_list).map { |a| a[2] }
|
161
|
+
def all_table_names = @pragma.list(:table_list).map { |a| a[1] }
|
162
|
+
|
163
|
+
def table_names
|
164
|
+
all_table_names().select { |name| !SQLITE_USAGE.match name }
|
165
|
+
end
|
166
|
+
|
167
|
+
def all_index_names(table_name)
|
168
|
+
@pragma.list(:index_list, table_name).map { |a| a[1] }
|
169
|
+
end
|
170
|
+
|
171
|
+
def index_names(table_name)
|
172
|
+
all_index_names(table_name).select { |name| !SQLITE_USAGE.match name }
|
173
|
+
end
|
174
|
+
|
175
|
+
def pragma_scalars
|
176
|
+
Pragma::SCALAR.map { |pragma|
|
177
|
+
val, enum = @pragma.get(pragma), Pragma::ENUM[pragma]
|
178
|
+
val = format("%i (%s)", val, enum[val]) if enum
|
179
|
+
format("%s: %s", pragma, val)
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
def report
|
184
|
+
lines = ['compile_options', '---']
|
185
|
+
lines += self.compile_options
|
186
|
+
|
187
|
+
lines += ['', 'database_files', '---']
|
188
|
+
lines += self.database_files
|
189
|
+
|
190
|
+
lines += ['', "table_names", '---']
|
191
|
+
tables = self.table_names
|
192
|
+
lines += tables
|
193
|
+
|
194
|
+
tables.each { |tbl|
|
195
|
+
lines += ['', "table_info(#{tbl})", '---']
|
196
|
+
lines += @pragma.table_info(tbl).map(&:inspect)
|
197
|
+
|
198
|
+
fks = @pragma.foreign_key_list(tbl).map(&:inspect)
|
199
|
+
if fks.length > 1
|
200
|
+
lines += ['', "foreign_key_list(#{tbl})", '---']
|
201
|
+
lines += fks
|
202
|
+
end
|
203
|
+
|
204
|
+
idxs = self.index_names(tbl)
|
205
|
+
if !idxs.empty?
|
206
|
+
lines += ['', "index_names(#{tbl})", '---']
|
207
|
+
lines += idxs
|
208
|
+
end
|
209
|
+
|
210
|
+
idxs.each { |idx|
|
211
|
+
lines += ['', "index_info(#{idx})", '---']
|
212
|
+
lines += @pragma.index_info(idx).map(&:inspect)
|
213
|
+
}
|
214
|
+
}
|
215
|
+
|
216
|
+
lines += ['', "pragma values", '---']
|
217
|
+
lines += self.pragma_scalars
|
218
|
+
lines
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
class Setup < Storage
|
223
|
+
def setup
|
224
|
+
drop_tables
|
225
|
+
create_tables
|
226
|
+
create_indices
|
227
|
+
report
|
228
|
+
end
|
229
|
+
|
230
|
+
def drop_tables
|
231
|
+
%w[events tags r_events r_tags].each { |tbl|
|
232
|
+
@db.execute "DROP TABLE IF EXISTS #{tbl}"
|
233
|
+
}
|
234
|
+
end
|
235
|
+
|
236
|
+
def create_tables
|
237
|
+
@db.execute <<SQL
|
238
|
+
CREATE TABLE events (content TEXT NOT NULL,
|
239
|
+
kind INT NOT NULL,
|
240
|
+
tags TEXT NOT NULL,
|
241
|
+
pubkey TEXT NOT NULL,
|
242
|
+
created_at INT NOT NULL,
|
243
|
+
id TEXT PRIMARY KEY NOT NULL,
|
244
|
+
sig TEXT NOT NULL)
|
245
|
+
SQL
|
246
|
+
|
247
|
+
@db.execute <<SQL
|
248
|
+
CREATE TABLE tags (event_id TEXT NOT NULL REFERENCES events (id)
|
249
|
+
ON DELETE CASCADE ON UPDATE CASCADE,
|
250
|
+
created_at INT NOT NULL,
|
251
|
+
tag TEXT NOT NULL,
|
252
|
+
value TEXT NOT NULL,
|
253
|
+
json TEXT NOT NULL)
|
254
|
+
SQL
|
255
|
+
|
256
|
+
@db.execute <<SQL
|
257
|
+
CREATE TABLE r_events (content TEXT NOT NULL,
|
258
|
+
kind INT NOT NULL,
|
259
|
+
tags TEXT NOT NULL,
|
260
|
+
d_tag TEXT DEFAULT NULL,
|
261
|
+
pubkey TEXT NOT NULL,
|
262
|
+
created_at INT NOT NULL,
|
263
|
+
id TEXT PRIMARY KEY NOT NULL,
|
264
|
+
sig TEXT NOT NULL)
|
265
|
+
SQL
|
266
|
+
|
267
|
+
@db.execute <<SQL
|
268
|
+
CREATE TABLE r_tags (r_event_id TEXT NOT NULL REFERENCES r_events (id)
|
269
|
+
ON DELETE CASCADE ON UPDATE CASCADE,
|
270
|
+
created_at INT NOT NULL,
|
271
|
+
tag TEXT NOT NULL,
|
272
|
+
value TEXT NOT NULL,
|
273
|
+
json TEXT NOT NULL)
|
274
|
+
SQL
|
275
|
+
end
|
276
|
+
|
277
|
+
def create_indices
|
278
|
+
@db.execute "CREATE INDEX idx_events_created_at
|
279
|
+
ON events (created_at)"
|
280
|
+
@db.execute "CREATE INDEX idx_tags_created_at
|
281
|
+
ON tags (created_at)"
|
282
|
+
@db.execute "CREATE INDEX idx_r_events_created_at
|
283
|
+
ON r_events (created_at)"
|
284
|
+
@db.execute "CREATE INDEX idx_r_tags_created_at
|
285
|
+
ON r_tags (created_at)"
|
286
|
+
|
287
|
+
@db.execute "CREATE UNIQUE INDEX unq_r_events_kind_pubkey_d_tag
|
288
|
+
ON r_events (kind, pubkey, d_tag)"
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
class Reader < Storage
|
293
|
+
PRAGMAS = Storage::PRAGMAS.merge(query_only: true)
|
294
|
+
|
295
|
+
# parse the JSON tags into a Ruby array
|
296
|
+
def self.hydrate(hash)
|
297
|
+
hash["tags"] = Nostrb.parse(hash.fetch("tags"))
|
298
|
+
hash
|
299
|
+
end
|
300
|
+
|
301
|
+
def self.event_clauses(filter)
|
302
|
+
clauses = []
|
303
|
+
if !filter.ids.empty?
|
304
|
+
clauses << format("id IN ('%s')", filter.ids.join("','"))
|
305
|
+
end
|
306
|
+
if !filter.authors.empty?
|
307
|
+
clauses << format("pubkey in ('%s')", filter.authors.join("','"))
|
308
|
+
end
|
309
|
+
if !filter.kinds.empty?
|
310
|
+
clauses << format("kind in (%s)", filter.kinds.join(','))
|
311
|
+
end
|
312
|
+
if filter.since
|
313
|
+
clauses << format("created_at >= %i", filter.since)
|
314
|
+
end
|
315
|
+
if filter.until
|
316
|
+
clauses << format("created_at <= %i", filter.until)
|
317
|
+
end
|
318
|
+
clauses.join(' AND ')
|
319
|
+
end
|
320
|
+
|
321
|
+
# filter_tags: { 'a' => [String] }
|
322
|
+
def self.tag_clauses(filter_tags)
|
323
|
+
clauses = []
|
324
|
+
filter_tags.each { |tag, values|
|
325
|
+
clauses << format("tag = %s", tag)
|
326
|
+
clauses << format("value in (%s)", values.join(','))
|
327
|
+
}
|
328
|
+
clauses.join(' AND ')
|
329
|
+
end
|
330
|
+
|
331
|
+
def initialize(filename = FILENAME, **kwargs)
|
332
|
+
super(filename, **kwargs.merge(readonly: true))
|
333
|
+
end
|
334
|
+
|
335
|
+
#
|
336
|
+
# Regular Events
|
337
|
+
#
|
338
|
+
|
339
|
+
def select_events_table(table = 'events', filter = nil)
|
340
|
+
sql = format("SELECT content, kind, tags, pubkey, created_at, id, sig
|
341
|
+
FROM %s", table)
|
342
|
+
if !filter.nil?
|
343
|
+
sql += format(" WHERE %s", Reader.event_clauses(filter))
|
344
|
+
end
|
345
|
+
@db.query sql
|
346
|
+
end
|
347
|
+
|
348
|
+
def process_events_table(table = 'events', filter = nil)
|
349
|
+
a = []
|
350
|
+
select_events_table(table, filter).each_hash { |h|
|
351
|
+
a << Reader.hydrate(h)
|
352
|
+
}
|
353
|
+
a
|
354
|
+
end
|
355
|
+
|
356
|
+
# these are presumably filtered so cannot be prepared
|
357
|
+
# use Database#query to get a ResultSet
|
358
|
+
def select_events(filter = nil)
|
359
|
+
select_events_table('events', filter)
|
360
|
+
end
|
361
|
+
|
362
|
+
def process_events(filter = nil)
|
363
|
+
process_events_table('events', filter)
|
364
|
+
end
|
365
|
+
|
366
|
+
# use a prepared statement to get a ResultSet
|
367
|
+
def select_tags(event_id:, created_at:)
|
368
|
+
@select_tags ||= @db.prepare("SELECT tag, value, json
|
369
|
+
FROM tags
|
370
|
+
WHERE event_id = :event_id
|
371
|
+
AND created_at = :created_at")
|
372
|
+
@select_tags.execute(event_id: event_id, created_at: created_at)
|
373
|
+
end
|
374
|
+
|
375
|
+
#
|
376
|
+
# Replaceable Events
|
377
|
+
#
|
378
|
+
|
379
|
+
def select_r_events(filter = nil)
|
380
|
+
select_events_table('r_events', filter)
|
381
|
+
end
|
382
|
+
|
383
|
+
def process_r_events(filter = nil)
|
384
|
+
process_events_table('r_events', filter)
|
385
|
+
end
|
386
|
+
|
387
|
+
def select_r_tags(event_id:, created_at:)
|
388
|
+
@select_r_tags ||= @db.prepare("SELECT tag, value, json
|
389
|
+
FROM r_tags
|
390
|
+
WHERE r_event_id = :event_id
|
391
|
+
AND created_at = :created_at")
|
392
|
+
@select_r_tags.execute(event_id: event_id, created_at: created_at)
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
class Writer < Storage
|
397
|
+
# a valid hash, as returned from SignedEvent.validate!
|
398
|
+
def add_event(valid)
|
399
|
+
@add_event ||= @db.prepare("INSERT INTO events
|
400
|
+
VALUES (:content, :kind, :tags, :pubkey,
|
401
|
+
:created_at, :id, :sig)")
|
402
|
+
@add_tag ||= @db.prepare("INSERT INTO tags
|
403
|
+
VALUES (:event_id, :created_at,
|
404
|
+
:tag, :value, :json)")
|
405
|
+
tags = valid["tags"]
|
406
|
+
valid["tags"] = Nostrb.json(tags)
|
407
|
+
@add_event.execute(valid) # insert event
|
408
|
+
tags.each { |a| # insert tags
|
409
|
+
@add_tag.execute(event_id: valid.fetch('id'),
|
410
|
+
created_at: valid.fetch('created_at'),
|
411
|
+
tag: a[0],
|
412
|
+
value: a[1],
|
413
|
+
json: Nostrb.json(a))
|
414
|
+
}
|
415
|
+
end
|
416
|
+
|
417
|
+
# add replaceable event
|
418
|
+
def add_r_event(valid)
|
419
|
+
@add_r_event ||=
|
420
|
+
@db.prepare("INSERT OR REPLACE INTO r_events
|
421
|
+
VALUES (:content, :kind, :tags, :d_tag,
|
422
|
+
:pubkey, :created_at, :id, :sig)")
|
423
|
+
@add_rtag ||= @db.prepare("INSERT INTO r_tags
|
424
|
+
VALUES (:r_event_id, :created_at,
|
425
|
+
:tag, :value, :json)")
|
426
|
+
tags = valid.fetch('tags')
|
427
|
+
d_tags = tags.select { |a| a[0] == 'd' }
|
428
|
+
valid['d_tag'] = d_tags.empty? ? nil : d_tags[0][1]
|
429
|
+
valid['tags'] = Nostrb.json(tags)
|
430
|
+
@add_r_event.execute(valid) # upsert event
|
431
|
+
tags.each { |a| # insert tags
|
432
|
+
@add_rtag.execute(r_event_id: valid.fetch('id'),
|
433
|
+
created_at: valid.fetch('created_at'),
|
434
|
+
tag: a[0],
|
435
|
+
value: a[1],
|
436
|
+
json: Nostrb.json(a))
|
437
|
+
}
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
data/lib/nostrb.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'digest' # stdlib
|
2
|
+
require 'schnorr_sig' # gem
|
3
|
+
begin
|
4
|
+
require 'nostrb/oj' # try Oj gem
|
5
|
+
rescue LoadError
|
6
|
+
require 'nostrb/json' # fall back to stdlib
|
7
|
+
end
|
8
|
+
|
9
|
+
module Nostrb
|
10
|
+
GEMS = %w[rbsecp256k1 oj sqlite3 sequel]
|
11
|
+
|
12
|
+
class Error < RuntimeError; end
|
13
|
+
class SizeError < Error; end
|
14
|
+
class FormatError < Error; end
|
15
|
+
|
16
|
+
# return 32 bytes binary
|
17
|
+
def self.digest(str) = Digest::SHA256.digest(str)
|
18
|
+
|
19
|
+
def self.check!(val, cls)
|
20
|
+
val.is_a?(cls) ? val : raise(TypeError, "#{cls} expected: #{val.inspect}")
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.int!(int, max: nil)
|
24
|
+
check!(int, Integer)
|
25
|
+
raise(SizeError, "#{int} > #{max} (max)") if !max.nil? and int > max
|
26
|
+
int
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.kind!(kind)
|
30
|
+
int!(kind, max: 65535)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.ary!(ary, max: nil)
|
34
|
+
check!(ary, Array)
|
35
|
+
if !max.nil? and ary.length > max
|
36
|
+
raise(SizeError, "#{ary.length} > #{max} (max)")
|
37
|
+
end
|
38
|
+
ary
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.str!(str, binary: nil, length: nil, max: nil)
|
42
|
+
check!(str, String)
|
43
|
+
if !binary.nil? and !binary == (str.encoding == Encoding::BINARY)
|
44
|
+
raise(EncodingError, str.encoding)
|
45
|
+
end
|
46
|
+
raise(SizeError, str.length) if !length.nil? and str.length != length
|
47
|
+
raise(SizeError, str.length) if !max.nil? and str.length > max
|
48
|
+
str
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.bin!(str, length: nil, max: nil)
|
52
|
+
str!(str, binary: true, length: length, max: max)
|
53
|
+
end
|
54
|
+
def self.key!(str) = bin!(str, length: 32)
|
55
|
+
|
56
|
+
|
57
|
+
def self.txt!(str, length: nil, max: nil)
|
58
|
+
str!(str, binary: false, length: length, max: max)
|
59
|
+
end
|
60
|
+
def self.pubkey!(str) = txt!(str, length: 64)
|
61
|
+
def self.id!(str) = txt!(str, length: 64)
|
62
|
+
def self.sid!(str) = txt!(str, max: 64)
|
63
|
+
def self.sig!(str) = txt!(str, length: 128)
|
64
|
+
|
65
|
+
HELP_MSG = /\A[a-zA-Z0-9\-_]+: [[:print:]]*\z/
|
66
|
+
|
67
|
+
def self.help!(str)
|
68
|
+
raise(FormatError, str) unless txt!(str, max: 1024).match HELP_MSG
|
69
|
+
str
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.tags!(ary)
|
73
|
+
ary!(ary, max: 9999).each { |a| ary!(a, max: 99).each { |s| txt!(s) } }
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.rbsecp256k1?
|
77
|
+
begin
|
78
|
+
require 'rbsecp256k1'; Secp256k1
|
79
|
+
rescue LoadError, NameError
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.oj?
|
85
|
+
begin
|
86
|
+
require 'oj'; Oj
|
87
|
+
rescue LoadError, NameError
|
88
|
+
false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.sqlite3?
|
93
|
+
begin
|
94
|
+
require 'sqlite3'; SQLite3
|
95
|
+
rescue LoadError, NameError
|
96
|
+
false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.sequel?
|
101
|
+
begin
|
102
|
+
require 'sequel'; Sequel
|
103
|
+
rescue LoadError, NameError
|
104
|
+
false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.gem_check
|
109
|
+
GEMS.map { |gem| [gem, self.send("#{gem}?")] }.to_h
|
110
|
+
end
|
111
|
+
end
|
data/nostrb.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'nostrb'
|
3
|
+
s.summary = "Minimal Nostr library in Ruby"
|
4
|
+
s.description = "TBD"
|
5
|
+
s.authors = ["Rick Hull"]
|
6
|
+
s.homepage = "https://github.com/rickhull/nostrb"
|
7
|
+
s.license = "LGPL-2.1-only"
|
8
|
+
|
9
|
+
s.required_ruby_version = "~> 3.0"
|
10
|
+
|
11
|
+
s.version = File.read(File.join(__dir__, 'VERSION')).chomp
|
12
|
+
|
13
|
+
s.files = %w[nostrb.gemspec VERSION Rakefile]
|
14
|
+
s.files += Dir['lib/**/*.rb']
|
15
|
+
s.files += Dir['test/**/*.rb']
|
16
|
+
|
17
|
+
s.add_dependency "schnorr_sig", "~> 1.0"
|
18
|
+
end
|
data/test/common.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'nostrb/event'
|
2
|
+
|
3
|
+
module Nostrb
|
4
|
+
module Test
|
5
|
+
SK, PK = SchnorrSig.keypair
|
6
|
+
|
7
|
+
def self.new_event(content = 'testing')
|
8
|
+
Event.new(content, pk: PK).sign(SK)
|
9
|
+
end
|
10
|
+
|
11
|
+
EVENT = Event.new('testing', pk: PK)
|
12
|
+
SIGNED = EVENT.sign(SK)
|
13
|
+
HASH = SIGNED.to_h
|
14
|
+
|
15
|
+
STATIC_HASH = {
|
16
|
+
"content" => "hello world",
|
17
|
+
"pubkey" => "18a2f562682d3ccaee89297eeee89a7961bc417bad98e9a3a93f010b0ea5313d",
|
18
|
+
"kind" => 1,
|
19
|
+
"tags" => [],
|
20
|
+
"created_at" => 1725496781,
|
21
|
+
"id" => "7f6f1c7ee406a450b581c62754fa66ffaaff0504b40ced02a6d0fc3806f1d44b",
|
22
|
+
"sig" => "8bb25f403e90cbe83629098264327b56240a703820b26f440a348ae81a64ec490c18e61d2942fe300f26b93a1534a94406aec12f5a32272357263bea88fccfda"
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|