nostrb 0.1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6f6159cbd2c246ae573103bd68e56e7cd97b0d3d81d36d8e0816abc5bd23c3c1
|
4
|
+
data.tar.gz: 8bcf8fe77b3baa61882ea849df549b2800ebbf5e788d55c2c1c00efddc99e1f8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 369121a3fa1b9ead9bc245a3bd5f18ce6a6e2cc3571e1c91019afb4752845de7edbef086b755ad3b90d4b1a072997b2cce8047e19c4db293cc14328332febb56
|
7
|
+
data.tar.gz: 8b19fa2c2b738e53711fdb4bbdac6a196ce8499b8a212b742be734318a526bd495bed6ddfd97f771c005323ee2583f6207feea8f79850b95e7c3aced24ac3cec
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
Rake::TestTask.new :test do |t|
|
4
|
+
t.pattern = "test/*.rb"
|
5
|
+
t.warning = true
|
6
|
+
end
|
7
|
+
|
8
|
+
task :relay do |t|
|
9
|
+
sh "ruby -I lib examples/relay.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
task default: [:test, :relay]
|
13
|
+
|
14
|
+
begin
|
15
|
+
require 'buildar'
|
16
|
+
|
17
|
+
Buildar.new do |b|
|
18
|
+
b.gemspec_file = 'nostrb.gemspec'
|
19
|
+
b.version_file = 'VERSION'
|
20
|
+
b.use_git = true
|
21
|
+
end
|
22
|
+
rescue LoadError
|
23
|
+
warn "buildar tasks unavailable"
|
24
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0.1
|
data/lib/nostrb/event.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'nostrb'
|
2
|
+
|
3
|
+
module Nostrb
|
4
|
+
class Event
|
5
|
+
|
6
|
+
# Event
|
7
|
+
# content: any string
|
8
|
+
# kind: 0..65535
|
9
|
+
# tags: Array[Array[string]]
|
10
|
+
# pubkey: 64 hex chars (32B binary)
|
11
|
+
|
12
|
+
def self.digest(ary) = Nostrb.digest(Nostrb.json(Nostrb.ary!(ary)))
|
13
|
+
|
14
|
+
def self.tag_values(tag, tags)
|
15
|
+
tags.select { |a| a[0] == tag }.map { |a| a[1] }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.d_tag(tags)
|
19
|
+
tag_values('d', tags).first
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :content, :kind, :tags, :pk
|
23
|
+
|
24
|
+
def initialize(content = '', kind: 1, tags: [], pk:)
|
25
|
+
@content = Nostrb.txt!(content)
|
26
|
+
@kind = Nostrb.kind!(kind)
|
27
|
+
@tags = Nostrb.tags!(tags)
|
28
|
+
@pk = Nostrb.key!(pk)
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :to_s, :content
|
32
|
+
|
33
|
+
def serialize(created_at)
|
34
|
+
[0, self.pubkey, Nostrb.int!(created_at), @kind, @tags, @content]
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_a = serialize(Time.now.to_i)
|
38
|
+
def pubkey = SchnorrSig.bin2hex(@pk)
|
39
|
+
def digest(created_at) = Event.digest(serialize(created_at))
|
40
|
+
def sign(sk) = SignedEvent.new(self, sk)
|
41
|
+
|
42
|
+
#
|
43
|
+
# Tags
|
44
|
+
#
|
45
|
+
|
46
|
+
# add an array of 2+ strings to @tags
|
47
|
+
def add_tag(tag, value, *rest)
|
48
|
+
@tags.push([Nostrb.txt!(tag), Nostrb.txt!(value)] +
|
49
|
+
rest.each { |s| Nostrb.txt!(s) })
|
50
|
+
end
|
51
|
+
|
52
|
+
# add an event tag based on event id, hex encoded
|
53
|
+
def ref_event(eid_hex, *rest)
|
54
|
+
add_tag('e', Nostrb.id!(eid_hex), *rest)
|
55
|
+
end
|
56
|
+
|
57
|
+
# add a pubkey tag based on pubkey, 64 bytes hex encoded
|
58
|
+
def ref_pubkey(pubkey, *rest)
|
59
|
+
add_tag('p', Nostrb.pubkey!(pubkey), *rest)
|
60
|
+
end
|
61
|
+
|
62
|
+
# kind: and pubkey: required
|
63
|
+
def ref_replace(*rest, kind:, pubkey:, d_tag: '')
|
64
|
+
val = [Nostrb.kind!(kind), Nostrb.pubkey!(pubkey), d_tag].join(':')
|
65
|
+
add_tag('a', val, *rest)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# SignedEvent
|
70
|
+
# id: 64 hex chars (32B binary)
|
71
|
+
# created_at: unix seconds, integer
|
72
|
+
# sig: 128 hex chars (64B binary)
|
73
|
+
|
74
|
+
class SignedEvent
|
75
|
+
class Error < RuntimeError; end
|
76
|
+
class IdCheck < Error; end
|
77
|
+
class SignatureCheck < Error; end
|
78
|
+
|
79
|
+
def self.validate!(parsed)
|
80
|
+
Nostrb.check!(parsed, Hash)
|
81
|
+
Nostrb.txt!(parsed.fetch("content"))
|
82
|
+
Nostrb.pubkey!(parsed.fetch("pubkey"))
|
83
|
+
Nostrb.kind!(parsed.fetch("kind"))
|
84
|
+
Nostrb.tags!(parsed.fetch("tags"))
|
85
|
+
Nostrb.int!(parsed.fetch("created_at"))
|
86
|
+
Nostrb.id!(parsed.fetch("id"))
|
87
|
+
Nostrb.sig!(parsed.fetch("sig"))
|
88
|
+
parsed
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.digest(valid) = Nostrb.digest(Nostrb.json(serialize(valid)))
|
92
|
+
|
93
|
+
def self.serialize(valid)
|
94
|
+
Array[ 0,
|
95
|
+
valid["pubkey"],
|
96
|
+
valid["created_at"],
|
97
|
+
valid["kind"],
|
98
|
+
valid["tags"],
|
99
|
+
valid["content"], ]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Validate the id (optional) and signature
|
103
|
+
# May raise explicitly: IdCheck, SignatureCheck
|
104
|
+
# May raise implicitly: Nostrb::SizeError, EncodingError, TypeError,
|
105
|
+
# SchnorrSig::Error
|
106
|
+
# Return a _completely validated_ hash
|
107
|
+
def self.verify(valid, check_id: true)
|
108
|
+
id, pubkey, sig = valid["id"], valid["pubkey"], valid["sig"]
|
109
|
+
|
110
|
+
# extract binary values for signature verification
|
111
|
+
digest = SchnorrSig.hex2bin id
|
112
|
+
pk = SchnorrSig.hex2bin pubkey
|
113
|
+
signature = SchnorrSig.hex2bin sig
|
114
|
+
|
115
|
+
# verify the signature
|
116
|
+
unless SchnorrSig.verify?(pk, digest, signature)
|
117
|
+
raise(SignatureCheck, sig)
|
118
|
+
end
|
119
|
+
# (optional) verify the id / digest
|
120
|
+
raise(IdCheck, id) if check_id and digest != SignedEvent.digest(valid)
|
121
|
+
valid
|
122
|
+
end
|
123
|
+
|
124
|
+
attr_reader :event, :created_at, :digest, :signature
|
125
|
+
|
126
|
+
# sk is used to generate @signature and then discarded
|
127
|
+
def initialize(event, sk)
|
128
|
+
@event = Nostrb.check!(event, Event)
|
129
|
+
@created_at = Time.now.to_i
|
130
|
+
@digest = @event.digest(@created_at)
|
131
|
+
@signature = SchnorrSig.sign(Nostrb.key!(sk), @digest)
|
132
|
+
end
|
133
|
+
|
134
|
+
def content = @event.content
|
135
|
+
def kind = @event.kind
|
136
|
+
def tags = @event.tags
|
137
|
+
def pubkey = @event.pubkey
|
138
|
+
def to_s = @event.to_s
|
139
|
+
def serialize = @event.serialize(@created_at)
|
140
|
+
|
141
|
+
def id = SchnorrSig.bin2hex(@digest)
|
142
|
+
def sig = SchnorrSig.bin2hex(@signature)
|
143
|
+
|
144
|
+
def to_h
|
145
|
+
Hash[ "content" => @event.content,
|
146
|
+
"kind" => @event.kind,
|
147
|
+
"tags" => @event.tags,
|
148
|
+
"pubkey" => @event.pubkey,
|
149
|
+
"created_at" => @created_at,
|
150
|
+
"id" => self.id,
|
151
|
+
"sig" => self.sig ]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Nostrb
|
2
|
+
module Seconds
|
3
|
+
def milliseconds(i) = i / 1000r
|
4
|
+
def seconds(i) = i
|
5
|
+
def minutes(i) = 60 * i
|
6
|
+
def hours(i) = 60 * minutes(i)
|
7
|
+
def days(i) = 24 * hours(i)
|
8
|
+
def weeks(i) = 7 * days(i)
|
9
|
+
def months(i) = years(i) / 12
|
10
|
+
def years(i) = 365 * days(i)
|
11
|
+
|
12
|
+
def process(hsh)
|
13
|
+
seconds = 0
|
14
|
+
[:seconds, :minutes, :hours, :days, :weeks, :months, :years].each { |p|
|
15
|
+
seconds += send(p, hsh[p]) if hsh.key?(p)
|
16
|
+
}
|
17
|
+
seconds
|
18
|
+
end
|
19
|
+
end
|
20
|
+
Seconds.extend(Seconds)
|
21
|
+
|
22
|
+
class Filter
|
23
|
+
TAG = /\A#([a-zA-Z])\z/
|
24
|
+
|
25
|
+
def self.ago(hsh)
|
26
|
+
Time.now.to_i - Seconds.process(hsh)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.ingest(hash)
|
30
|
+
f = Filter.new
|
31
|
+
|
32
|
+
if ids = hash.delete("ids")
|
33
|
+
f.add_ids(*ids)
|
34
|
+
end
|
35
|
+
if authors = hash.delete("authors")
|
36
|
+
f.add_authors(*authors)
|
37
|
+
end
|
38
|
+
if kinds = hash.delete("kinds")
|
39
|
+
f.add_kinds(*kinds)
|
40
|
+
end
|
41
|
+
if since = hash.delete("since")
|
42
|
+
f.since = since
|
43
|
+
end
|
44
|
+
if _until = hash.delete("until")
|
45
|
+
f.until = _until
|
46
|
+
end
|
47
|
+
if limit = hash.delete("limit")
|
48
|
+
f.limit = limit
|
49
|
+
end
|
50
|
+
|
51
|
+
# anything left in hash should only be single letter tags
|
52
|
+
hash.each { |tag, ary|
|
53
|
+
if matches = tag.match(TAG)
|
54
|
+
f.add_tag(matches[1], ary)
|
55
|
+
else
|
56
|
+
warn "unmatched tag: #{tag}"
|
57
|
+
end
|
58
|
+
}
|
59
|
+
f
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_reader :ids, :authors, :kinds, :tags, :limit
|
63
|
+
|
64
|
+
def initialize
|
65
|
+
@ids = []
|
66
|
+
@authors = []
|
67
|
+
@kinds = []
|
68
|
+
@tags = {}
|
69
|
+
@since = nil
|
70
|
+
@until = nil
|
71
|
+
@limit = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_ids(*event_ids)
|
75
|
+
@ids += event_ids.each { |id| Nostrb.id!(id) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_authors(*pubkeys)
|
79
|
+
@authors += pubkeys.each { |pubkey| Nostrb.pubkey!(pubkey) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_kinds(*kinds)
|
83
|
+
@kinds += kinds.each { |k| Nostrb.kind!(k) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_tag(letter, list)
|
87
|
+
@tags[Nostrb.txt!(letter, length: 1)] =
|
88
|
+
Nostrb.ary!(list, max: 99).each { |s| Nostrb.txt!(s) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def since(hsh = nil) = hsh.nil? ? @since : (@since = Filter.ago(hsh))
|
92
|
+
def since=(int)
|
93
|
+
@since = int.nil? ? nil : Nostrb.int!(int)
|
94
|
+
end
|
95
|
+
|
96
|
+
def until(hsh = nil) = hsh.nil? ? @until : (@until = Filter.ago(hsh))
|
97
|
+
def until=(int)
|
98
|
+
@until = int.nil? ? nil : Nostrb.int!(int)
|
99
|
+
end
|
100
|
+
|
101
|
+
def limit=(int)
|
102
|
+
@limit = int.nil? ? nil : Nostrb.int!(int)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Input
|
106
|
+
# Ruby hash as returned from SignedEvent.validate!
|
107
|
+
def match?(valid)
|
108
|
+
return false if !@ids.empty? and !@ids.include?(valid["id"])
|
109
|
+
return false if !@authors.empty? and !@authors.include?(valid["pubkey"])
|
110
|
+
return false if !@kinds.empty? and !@kinds.include?(valid["kind"])
|
111
|
+
return false if !@since.nil? and @since > valid["created_at"]
|
112
|
+
return false if !@until.nil? and @until < valid["created_at"]
|
113
|
+
if !@tags.empty?
|
114
|
+
tags = valid["tags"]
|
115
|
+
@tags.each { |letter, ary|
|
116
|
+
tag_match = false
|
117
|
+
tags.each { |(tag, val)|
|
118
|
+
next if tag_match
|
119
|
+
if tag == letter
|
120
|
+
return false if !ary.include?(val)
|
121
|
+
tag_match = true
|
122
|
+
end
|
123
|
+
}
|
124
|
+
return false unless tag_match
|
125
|
+
}
|
126
|
+
end
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_h
|
131
|
+
h = Hash.new
|
132
|
+
h["ids"] = @ids if !@ids.empty?
|
133
|
+
h["authors"] = @authors if !@authors.empty?
|
134
|
+
h["kinds"] = @kinds if !@kinds.empty?
|
135
|
+
@tags.each { |letter, ary|
|
136
|
+
h['#' + letter.to_s] = ary if !ary.empty?
|
137
|
+
}
|
138
|
+
h["since"] = @since unless @since.nil?
|
139
|
+
h["until"] = @until unless @until.nil?
|
140
|
+
h["limit"] = @limit unless @limit.nil?
|
141
|
+
h
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/nostrb/json.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Nostrb
|
4
|
+
# per NIP-01
|
5
|
+
JSON_OPTIONS = {
|
6
|
+
allow_nan: false,
|
7
|
+
max_nesting: 4, # event is 3 deep, wire format is 4 deep
|
8
|
+
script_safe: false,
|
9
|
+
ascii_only: false,
|
10
|
+
array_nl: '',
|
11
|
+
object_nl: '',
|
12
|
+
indent: '',
|
13
|
+
space: '',
|
14
|
+
space_before: '',
|
15
|
+
}
|
16
|
+
|
17
|
+
def self.parse(json) = JSON.parse(json, **JSON_OPTIONS)
|
18
|
+
def self.json(object) = JSON.generate(object, **JSON_OPTIONS)
|
19
|
+
end
|
data/lib/nostrb/names.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'nostrb'
|
2
|
+
|
3
|
+
module Nostrb
|
4
|
+
# NIP-05
|
5
|
+
module Names
|
6
|
+
LOCAL = /\A[_a-z0-0\-.]+\z/i
|
7
|
+
DOMAIN = /\A[a-z0-9\-.]+\z/i
|
8
|
+
|
9
|
+
# given bob@example.com, return [bob, example.com]
|
10
|
+
def self.identifier(str)
|
11
|
+
a = str.split('@')
|
12
|
+
raise "unexpected #{a.inspect}" unless a.length == 2
|
13
|
+
raise "bad local: #{a[0]}" unless LOCAL.match a[0]
|
14
|
+
raise "bad domain #{a[1]}" unless DOMAIN.match a[1]
|
15
|
+
a
|
16
|
+
end
|
17
|
+
|
18
|
+
# content: <JSON>
|
19
|
+
# kind: 0 (set_metadata)
|
20
|
+
# JSON:
|
21
|
+
# name:
|
22
|
+
# about:
|
23
|
+
# picture:
|
24
|
+
# nip05: bob@example.com
|
25
|
+
def self.extract_nip05(json)
|
26
|
+
addr = Nostrb.parse(json)['nip05']
|
27
|
+
identifier(addr) if addr
|
28
|
+
end
|
29
|
+
|
30
|
+
# when we get bob's profile back,
|
31
|
+
# if it has a nip05 field
|
32
|
+
# visit: https://example.com/.well-known/nostr.json?name=bob
|
33
|
+
def self.well_known_url(local, domain)
|
34
|
+
format("https://%s/.well-known/nostr.json?name=%s", domain, local)
|
35
|
+
end
|
36
|
+
|
37
|
+
# look for a pubkey:
|
38
|
+
# {"names":{"bob":"b0b0...b0"}}
|
39
|
+
# the pubkey at the URL should match the pubkey from the profile event
|
40
|
+
def self.extract_names(json)
|
41
|
+
hsh = Nostrb.parse(json).fetch("names")
|
42
|
+
hsh.each { |name, pubkey|
|
43
|
+
Nostrb.txt!(name)
|
44
|
+
Nostrb.pubkey!(pubkey)
|
45
|
+
}
|
46
|
+
hsh
|
47
|
+
end
|
48
|
+
|
49
|
+
# or even relays too:
|
50
|
+
# {"names":{"bob":"b0b0...b0"}
|
51
|
+
# "relays":{
|
52
|
+
# "b0b0...b0":["wss://relay.example.com/","wss://relay2.example.com/"]
|
53
|
+
# }
|
54
|
+
# }
|
55
|
+
def self.extract_relays(json)
|
56
|
+
hsh = Nostrb.parse(json).fetch("relays")
|
57
|
+
hsh.each { |pubkey, urls|
|
58
|
+
Nostrb.pubkey!(pubkey)
|
59
|
+
Nostrb.ary!(urls)
|
60
|
+
urls.each { |u| Nostrb.txt!(u) }
|
61
|
+
}
|
62
|
+
hsh
|
63
|
+
end
|
64
|
+
|
65
|
+
# or, if we see bob@example.com
|
66
|
+
# visit: https://example.com/.well-known/nostr.json?name=bob
|
67
|
+
# get the pubkey
|
68
|
+
# subscribe to that user's profile events
|
69
|
+
# check if that user has nip05 and it matches bob@example.com
|
70
|
+
end
|
71
|
+
end
|
data/lib/nostrb/oj.rb
ADDED
data/lib/nostrb/relay.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'nostrb/event'
|
2
|
+
require 'nostrb/filter'
|
3
|
+
require 'nostrb/sqlite'
|
4
|
+
require 'set' # jruby wants this
|
5
|
+
|
6
|
+
# Kind:
|
7
|
+
# 1,4..44,1000..9999: regular -- relay stores all
|
8
|
+
# 0,3: replaceable -- relay stores only the last message from pubkey
|
9
|
+
# 2: deprecated
|
10
|
+
# 10_000..19_999: replaceable -- relay stores latest(pubkey, kind)
|
11
|
+
# 20_000..29_999: ephemeral -- relay doesn't store
|
12
|
+
# 30_000..39_999: parameterized replaceable -- latest(pubkey, kind, dtag)
|
13
|
+
|
14
|
+
# for replaceable events with same timestamp, lowest id wins
|
15
|
+
|
16
|
+
module Nostrb
|
17
|
+
class Server
|
18
|
+
def self.event(sid, event) = ["EVENT", Nostrb.sid!(sid), event.to_h]
|
19
|
+
def self.ok(eid, msg = "", ok: true)
|
20
|
+
["OK", Nostrb.id!(eid), !!ok, ok ? Nostrb.txt!(msg) : Nostrb.help!(msg)]
|
21
|
+
end
|
22
|
+
def self.eose(sid) = ["EOSE", Nostrb.sid!(sid)]
|
23
|
+
def self.closed(sid, msg) = ["CLOSED", Nostrb.sid!(sid), Nostrb.help!(msg)]
|
24
|
+
def self.notice(msg) = ["NOTICE", Nostrb.txt!(msg)]
|
25
|
+
def self.error(e) = notice(message(e))
|
26
|
+
|
27
|
+
def self.message(excp)
|
28
|
+
format("%s: %s", excp.class.name.split('::').last, excp.message)
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(db_filename = nil, storage: :sqlite)
|
32
|
+
case storage
|
33
|
+
when :sqlite
|
34
|
+
mod = Nostrb::SQLite
|
35
|
+
when :sequel
|
36
|
+
require 'nostrb/sequel'
|
37
|
+
mod = Nostrb::Sequel
|
38
|
+
else
|
39
|
+
raise "unexpected: #{storage.inspect}"
|
40
|
+
end
|
41
|
+
db_filename ||= mod::Storage::FILENAME
|
42
|
+
@reader = mod::Reader.new(db_filename)
|
43
|
+
@writer = mod::Writer.new(db_filename)
|
44
|
+
end
|
45
|
+
|
46
|
+
# accepts a single json array
|
47
|
+
# returns a ruby array of response strings (json array)
|
48
|
+
def ingest(json)
|
49
|
+
begin
|
50
|
+
a = Nostrb.ary!(Nostrb.parse(json))
|
51
|
+
case a[0]
|
52
|
+
when 'EVENT'
|
53
|
+
[handle_event(Nostrb.check!(a[1], Hash))]
|
54
|
+
when 'REQ'
|
55
|
+
sid = Nostrb.sid!(a[1])
|
56
|
+
filters = a[2..-1].map { |f| Filter.ingest(f) }
|
57
|
+
handle_req(sid, *filters)
|
58
|
+
when 'CLOSE'
|
59
|
+
[handle_close(Nostrb.sid!(a[1]))]
|
60
|
+
else
|
61
|
+
[Server.notice("unexpected: #{a[0].inspect}")]
|
62
|
+
end
|
63
|
+
rescue StandardError => e
|
64
|
+
[Server.error(e)]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# return a single response
|
69
|
+
def handle_event(hsh)
|
70
|
+
begin
|
71
|
+
hsh = SignedEvent.validate!(hsh)
|
72
|
+
rescue Nostrb::Error, KeyError, RuntimeError => e
|
73
|
+
return Server.error(e)
|
74
|
+
end
|
75
|
+
|
76
|
+
eid = hsh.fetch('id')
|
77
|
+
|
78
|
+
begin
|
79
|
+
hsh = SignedEvent.verify(hsh)
|
80
|
+
case hsh['kind']
|
81
|
+
when 1, (4..44), (1000..9999)
|
82
|
+
# regular, store all
|
83
|
+
@writer.add_event(hsh)
|
84
|
+
when 0, 3, (10_000..19_999)
|
85
|
+
# replaceable, store latest (pubkey, kind)
|
86
|
+
@writer.add_r_event(hsh)
|
87
|
+
when 20_000..29_999
|
88
|
+
# ephemeral, don't store
|
89
|
+
when 30_000..30_999
|
90
|
+
# parameterized replaceable, store latest (pubkey, kind, dtag)
|
91
|
+
# TODO: implement dtag stuff
|
92
|
+
@writer.add_r_event(hsh)
|
93
|
+
else
|
94
|
+
raise(SignedEvent::Error, "kind: #{hsh['kind']}")
|
95
|
+
end
|
96
|
+
|
97
|
+
Server.ok(eid)
|
98
|
+
rescue SignedEvent::Error => e
|
99
|
+
Server.ok(eid, Server.message(e), ok: false)
|
100
|
+
rescue Nostrb::Error, KeyError, RuntimeError => e
|
101
|
+
Server.error(e)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# return an array of response
|
106
|
+
# filter1:
|
107
|
+
# ids: [id1, id2]
|
108
|
+
# authors: [pubkey1]
|
109
|
+
# filter2:
|
110
|
+
# ids: [id3, id4]
|
111
|
+
# authors: [pubkey2]
|
112
|
+
|
113
|
+
# run filter1
|
114
|
+
# for any fields specified (ids, authors)
|
115
|
+
# if any values match, the event is a match
|
116
|
+
# all fields provided must match for the event to match
|
117
|
+
# ids must have a match and authors must have a match
|
118
|
+
|
119
|
+
# run filter2 just like filter1
|
120
|
+
# the result set is the union of filter1 and filter2
|
121
|
+
|
122
|
+
def handle_req(sid, *filters)
|
123
|
+
responses = Set.new
|
124
|
+
|
125
|
+
filters.each { |f|
|
126
|
+
@reader.process_events(f).each { |h|
|
127
|
+
responses << Server.event(sid, h) if f.match? h
|
128
|
+
}
|
129
|
+
@reader.process_r_events(f).each { |h|
|
130
|
+
responses << Server.event(sid, h) if f.match? h
|
131
|
+
}
|
132
|
+
}
|
133
|
+
responses = responses.to_a
|
134
|
+
responses << Server.eose(sid)
|
135
|
+
end
|
136
|
+
|
137
|
+
# single response
|
138
|
+
def handle_close(sid)
|
139
|
+
Server.closed(sid, "reason: CLOSE requested")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|