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.
@@ -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