nostrb 0.1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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