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,222 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'nostrb/sqlite'
|
3
|
+
|
4
|
+
module Nostrb
|
5
|
+
module Sequel
|
6
|
+
class Storage < SQLite::Storage
|
7
|
+
FILENAME = 'sequel.db'
|
8
|
+
TABLES = [:events, :tags, :r_events, :r_tags]
|
9
|
+
|
10
|
+
def self.schema_line(col, cfg)
|
11
|
+
[col.to_s.ljust(9, ' '),
|
12
|
+
cfg.map { |(k,v)| [k, v.inspect].join(': ') }.join("\t")
|
13
|
+
].join("\t")
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(filename = FILENAME, set_pragmas: true)
|
17
|
+
@filename = filename
|
18
|
+
@db = ::Sequel.sqlite(@filename)
|
19
|
+
@db.transaction_mode = :immediate
|
20
|
+
self.set_pragmas if set_pragmas
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_pragmas
|
24
|
+
@db.pool.available_connections.each { |s3db|
|
25
|
+
pragma = SQLite::Pragma.new(s3db)
|
26
|
+
PRAGMAS.each { |name, val| pragma.set(name, val) }
|
27
|
+
}
|
28
|
+
PRAGMAS.clone
|
29
|
+
end
|
30
|
+
|
31
|
+
def pragma_scalars
|
32
|
+
pragma = SQLite::Pragma.new(@db.pool.available_connections.sample)
|
33
|
+
SQLite::Pragma::SCALAR.map { |p|
|
34
|
+
val, enum = pragma.get(p), SQLite::Pragma::ENUM[p]
|
35
|
+
val = format("%i (%s)", val, enum[val]) if enum
|
36
|
+
format("%s: %s", p, val)
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup
|
41
|
+
Setup.new(@filename)
|
42
|
+
end
|
43
|
+
|
44
|
+
def reader
|
45
|
+
Reader.new(@filename)
|
46
|
+
end
|
47
|
+
|
48
|
+
def writer
|
49
|
+
Writer.new(@filename)
|
50
|
+
end
|
51
|
+
|
52
|
+
def schema(table)
|
53
|
+
@db.schema(table).map { |a| Storage.schema_line(*a) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def report
|
57
|
+
lines = []
|
58
|
+
TABLES.each { |t|
|
59
|
+
lines << t
|
60
|
+
lines += schema(t)
|
61
|
+
lines << ''
|
62
|
+
}
|
63
|
+
lines += self.pragma_scalars
|
64
|
+
lines
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Setup < Storage
|
69
|
+
def setup
|
70
|
+
drop_tables
|
71
|
+
create_tables
|
72
|
+
report
|
73
|
+
end
|
74
|
+
|
75
|
+
def drop_tables
|
76
|
+
@db.drop_table?(*TABLES)
|
77
|
+
end
|
78
|
+
|
79
|
+
def create_tables
|
80
|
+
@db.create_table :events do
|
81
|
+
text :content, null: false
|
82
|
+
int :kind, null: false
|
83
|
+
text :tags, null: false
|
84
|
+
text :pubkey, null: false
|
85
|
+
int :created_at, null: false, index: {
|
86
|
+
name: :idx_events_created_at
|
87
|
+
}
|
88
|
+
text :id, null: false, primary_key: true
|
89
|
+
text :sig, null: false
|
90
|
+
end
|
91
|
+
|
92
|
+
@db.create_table :tags do
|
93
|
+
# text :event_id, null: false # fk
|
94
|
+
foreign_key :event_id, :events,
|
95
|
+
key: :id,
|
96
|
+
type: :text,
|
97
|
+
null: false,
|
98
|
+
on_delete: :cascade,
|
99
|
+
on_update: :cascade
|
100
|
+
int :created_at, null: false, index: { name: :idx_tags_created_at }
|
101
|
+
text :tag, null: false
|
102
|
+
text :value, null: false
|
103
|
+
text :json, null: false
|
104
|
+
end
|
105
|
+
|
106
|
+
@db.create_table :r_events do
|
107
|
+
text :content, null: false
|
108
|
+
int :kind, null: false
|
109
|
+
text :tags, null: false
|
110
|
+
text :d_tag, null: true, default: nil
|
111
|
+
text :pubkey, null: false
|
112
|
+
int :created_at, null: false, index: {
|
113
|
+
name: :idx_r_events_created_at
|
114
|
+
}
|
115
|
+
text :id, null: false, primary_key: true
|
116
|
+
text :sig, null: false
|
117
|
+
index [:kind, :pubkey, :d_tag], {
|
118
|
+
unique: true,
|
119
|
+
name: :unq_r_events_kind_pubkey_d_tag,
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
@db.create_table :r_tags do
|
124
|
+
foreign_key :r_event_id, :r_events,
|
125
|
+
key: :id,
|
126
|
+
type: :text,
|
127
|
+
null: false,
|
128
|
+
on_delete: :cascade,
|
129
|
+
on_update: :cascade
|
130
|
+
int :created_at, null: false, index: {
|
131
|
+
name: :idx_r_tags_created_at
|
132
|
+
}
|
133
|
+
text :tag, null: false
|
134
|
+
text :value, null: false
|
135
|
+
text :json, null: false
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class Reader < Storage
|
141
|
+
def self.hydrate(event_row)
|
142
|
+
Hash[ 'content' => event_row.fetch(:content),
|
143
|
+
'kind' => event_row.fetch(:kind),
|
144
|
+
'tags' => Nostrb.parse(event_row.fetch(:tags)),
|
145
|
+
'pubkey' => event_row.fetch(:pubkey),
|
146
|
+
'created_at' => event_row.fetch(:created_at),
|
147
|
+
'id' => event_row.fetch(:id),
|
148
|
+
'sig' => event_row.fetch(:sig), ]
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.event_clauses(filter)
|
152
|
+
hsh = {}
|
153
|
+
hsh[:id] = filter.ids unless filter.ids.empty?
|
154
|
+
hsh[:pubkey] = filter.authors unless filter.authors.empty?
|
155
|
+
hsh[:kind] = filter.kinds unless filter.kinds.empty?
|
156
|
+
a = filter.since || Filter.ago(years: 10)
|
157
|
+
b = filter.until || Time.now.to_i
|
158
|
+
hsh[:created_at] = a..b
|
159
|
+
hsh
|
160
|
+
end
|
161
|
+
|
162
|
+
def select_events_table(table = :events, filter = nil)
|
163
|
+
if !filter.nil?
|
164
|
+
@db[table].where(self.class.event_clauses(filter))
|
165
|
+
else
|
166
|
+
@db[table]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def select_events(filter = nil)
|
171
|
+
select_events_table(:events, filter)
|
172
|
+
end
|
173
|
+
|
174
|
+
def process_events(filter = nil)
|
175
|
+
a = []
|
176
|
+
select_events(filter).each { |row| a << Reader.hydrate(row) }
|
177
|
+
a
|
178
|
+
end
|
179
|
+
|
180
|
+
def select_r_events(filter = nil)
|
181
|
+
select_events_table(:r_events, filter)
|
182
|
+
end
|
183
|
+
|
184
|
+
def process_r_events(filter = nil)
|
185
|
+
a = []
|
186
|
+
select_r_events(filter).each { |row| a << Reader.hydrate(row) }
|
187
|
+
a
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class Writer < Storage
|
192
|
+
def add_event(valid)
|
193
|
+
@db[:events].insert(valid.merge('tags' => Nostrb.json(valid['tags'])))
|
194
|
+
valid['tags'].each { |a|
|
195
|
+
@db[:tags].insert(event_id: valid['id'],
|
196
|
+
created_at: valid['created_at'],
|
197
|
+
tag: a[0],
|
198
|
+
value: a[1],
|
199
|
+
json: Nostrb.json(a))
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
# use insert_conflict to replace latest event
|
204
|
+
def add_r_event(valid)
|
205
|
+
tags = valid.fetch('tags')
|
206
|
+
d_tag = Event.d_tag(tags)
|
207
|
+
hsh = {
|
208
|
+
'tags' => Nostrb.json(tags),
|
209
|
+
'd_tag' => d_tag,
|
210
|
+
}
|
211
|
+
@db[:r_events].insert_conflict.insert(valid.merge(hsh))
|
212
|
+
valid['tags'].each { |a|
|
213
|
+
@db[:r_tags].insert(r_event_id: valid['id'],
|
214
|
+
created_at: valid['created_at'],
|
215
|
+
tag: a[0],
|
216
|
+
value: a[1],
|
217
|
+
json: Nostrb.json(a))
|
218
|
+
}
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'nostrb/event'
|
2
|
+
require 'nostrb/filter'
|
3
|
+
|
4
|
+
module Nostrb
|
5
|
+
|
6
|
+
#
|
7
|
+
# A Source holds a public key and creates Events.
|
8
|
+
#
|
9
|
+
|
10
|
+
class Source
|
11
|
+
#######################
|
12
|
+
# Client Requests
|
13
|
+
|
14
|
+
def self.publish(signed) = ["EVENT", signed.to_h]
|
15
|
+
|
16
|
+
def self.subscribe(sid, *filters)
|
17
|
+
["REQ", Nostrb.sid!(sid), *filters.map { |f|
|
18
|
+
Nostrb.check!(f, Filter).to_h
|
19
|
+
}]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.close(sid) = ["CLOSE", Nostrb.sid!(sid)]
|
23
|
+
|
24
|
+
#######################
|
25
|
+
# Utils / Init
|
26
|
+
|
27
|
+
def self.random_sid
|
28
|
+
SchnorrSig.bin2hex Random.bytes(32)
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :pk
|
32
|
+
|
33
|
+
def initialize(pk)
|
34
|
+
@pk = Nostrb.key!(pk)
|
35
|
+
end
|
36
|
+
|
37
|
+
def pubkey = SchnorrSig.bin2hex(@pk)
|
38
|
+
|
39
|
+
############################
|
40
|
+
# Event Creation
|
41
|
+
|
42
|
+
def event(content, kind)
|
43
|
+
Event.new(content, kind: kind, pk: @pk)
|
44
|
+
end
|
45
|
+
|
46
|
+
# NIP-01
|
47
|
+
# Input
|
48
|
+
# content: string
|
49
|
+
# Output
|
50
|
+
# Event
|
51
|
+
# content: <content>
|
52
|
+
# kind: 1
|
53
|
+
def text_note(content)
|
54
|
+
event(content, 1)
|
55
|
+
end
|
56
|
+
|
57
|
+
# NIP-01
|
58
|
+
# Input
|
59
|
+
# name: string
|
60
|
+
# about: string
|
61
|
+
# picture: string, URL
|
62
|
+
# Output
|
63
|
+
# Event
|
64
|
+
# content: {"name":<username>,"about":<string>,"picture":<url>}
|
65
|
+
# kind: 0, user metadata
|
66
|
+
def user_metadata(name:, about:, picture:, **kwargs)
|
67
|
+
full = kwargs.merge(name: Nostrb.txt!(name),
|
68
|
+
about: Nostrb.txt!(about),
|
69
|
+
picture: Nostrb.txt!(picture))
|
70
|
+
event(Nostrb.json(full), 0)
|
71
|
+
end
|
72
|
+
alias_method :profile, :user_metadata
|
73
|
+
|
74
|
+
# NIP-02
|
75
|
+
# Input
|
76
|
+
# pubkey_hsh: a ruby hash of the form: pubkey => [relay_url, petname]
|
77
|
+
# "deadbeef1234abcdef" => ["wss://alicerelay.com/", "alice"]
|
78
|
+
# Output
|
79
|
+
# Event
|
80
|
+
# content: ""
|
81
|
+
# kind: 3, follow list
|
82
|
+
# tags: [['p', pubkey, relay_url, petname]]
|
83
|
+
def follow_list(pubkey_hsh)
|
84
|
+
list = event('', 3)
|
85
|
+
pubkey_hsh.each { |pubkey, (url, name)|
|
86
|
+
list.ref_pubkey(Nostrb.pubkey!(pubkey),
|
87
|
+
Nostrb.txt!(url),
|
88
|
+
Nostrb.txt!(name))
|
89
|
+
}
|
90
|
+
list
|
91
|
+
end
|
92
|
+
alias_method :follows, :follow_list
|
93
|
+
|
94
|
+
# NIP-09
|
95
|
+
# Input
|
96
|
+
# explanation: content string
|
97
|
+
# *event_ids: array of event ids, hex format
|
98
|
+
# Output
|
99
|
+
# Event
|
100
|
+
# content: explanation
|
101
|
+
# kind: 5, deletion request
|
102
|
+
# tags: [['e', event_id]]
|
103
|
+
# TODO: support deletion of replaceable events ('a' tags)
|
104
|
+
def deletion_request(explanation, *event_ids)
|
105
|
+
e = event(explanation, 5)
|
106
|
+
event_ids.each { |eid| e.ref_event(eid) }
|
107
|
+
e
|
108
|
+
end
|
109
|
+
alias_method :delete, :deletion_request
|
110
|
+
end
|
111
|
+
end
|