ratproto 0.1.2 → 0.2.0
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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +1 -1
- data/exe/rat +2 -315
- data/lib/ratproto/cli.rb +57 -0
- data/lib/ratproto/commands/base.rb +54 -0
- data/lib/ratproto/commands/fetch.rb +38 -0
- data/lib/ratproto/commands/resolve.rb +41 -0
- data/lib/ratproto/commands/stream.rb +98 -0
- data/lib/ratproto/version.rb +1 -1
- metadata +27 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ae75289d3bfe8a1adb0a27e3f33111b194756524fc6b040bb7131cd16fe0b6a
|
|
4
|
+
data.tar.gz: 0a424e54c22bb32ffcfa5ee69f078b12705d0c19c3413a16867b9f4fc331d1a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f1e9a749a2d46ba339a194f5331569b89d05480f419b84a208062b39306893bd2bc88139f144426330c234ff5cdc63303a728ce3d47228bd664e8e59d7ec600
|
|
7
|
+
data.tar.gz: '08d7134fd2647848dfd1df605c05b8b2b03893d0be2b2ffd0d55da50d36fd6168679b292765283231693fbeba80883f26a5464d6098a9f49cdd9853a5ef1c770'
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ It builds on top of the existing ATProto Ruby gems:
|
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
|
-
To run this tool, you need some reasonably recent version of Ruby installed – it should run on Ruby 2.6 and above, although it's recommended to use a version that still gets maintainance updates, i.e. currently 3.
|
|
17
|
+
To run this tool, you need some reasonably recent version of Ruby installed – it should run on Ruby 2.6 and above, although it's recommended to use a version that still gets maintainance updates, i.e. currently 3.3+.
|
|
18
18
|
|
|
19
19
|
An older version of Ruby (2.6.x) that should work is shipped with macOS versions 11.0 or later, a recent version of Ruby is also likely to be already installed on most Linux systems, or at least available through the OS's package manager. More installation options can be found on [ruby-lang.org](https://www.ruby-lang.org/en/downloads/).
|
|
20
20
|
|
data/exe/rat
CHANGED
|
@@ -2,319 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
4
4
|
|
|
5
|
-
require '
|
|
6
|
-
require 'json'
|
|
7
|
-
require 'minisky'
|
|
8
|
-
require 'optparse'
|
|
9
|
-
require 'time'
|
|
10
|
-
require 'uri'
|
|
5
|
+
require 'ratproto/cli'
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
DID_REGEXP = /\Adid:[a-z]+:[a-zA-Z0-9.\-_]+\z/
|
|
15
|
-
NSID_REGEXP = /\A[a-z0-9]+(\.[a-z0-9]+)+\z/
|
|
16
|
-
HANDLE_REGEXP = /\A[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)+\z/
|
|
17
|
-
|
|
18
|
-
def print_help
|
|
19
|
-
puts <<~HELP
|
|
20
|
-
rat #{RatProto::VERSION} 🐀
|
|
21
|
-
|
|
22
|
-
Usage:
|
|
23
|
-
rat fetch at://uri
|
|
24
|
-
rat stream <firehose-host> [options]
|
|
25
|
-
rat resolve <did>|<handle>
|
|
26
|
-
|
|
27
|
-
Commands:
|
|
28
|
-
fetch Fetch a single record given its at:// URI
|
|
29
|
-
stream Stream events from a relay / PDS firehose
|
|
30
|
-
resolve Resolve a DID or @handle
|
|
31
|
-
help Show this help
|
|
32
|
-
version Show version
|
|
33
|
-
|
|
34
|
-
Stream options:
|
|
35
|
-
-j, --jetstream Use a Jetstream source instead of a CBOR firehose
|
|
36
|
-
-r, --cursor CURSOR Start from a given cursor
|
|
37
|
-
-d, --did DID[,DID...] Filter only events from given DID(s)
|
|
38
|
-
(can be passed multiple times)
|
|
39
|
-
-c, --collection NSID[,NSID...] Filter only events of given collection(s)
|
|
40
|
-
(can be passed multiple times)
|
|
41
|
-
HELP
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def abort_with_error(message)
|
|
45
|
-
puts message
|
|
46
|
-
exit 1
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def check_stream_did(did)
|
|
50
|
-
if did =~ DID_REGEXP
|
|
51
|
-
did
|
|
52
|
-
elsif did =~ HANDLE_REGEXP
|
|
53
|
-
if resolved_did = DID.resolve_handle(did)
|
|
54
|
-
resolved_did.to_s
|
|
55
|
-
else
|
|
56
|
-
abort_with_error "Error: couldn't resolve handle @#{did}"
|
|
57
|
-
end
|
|
58
|
-
else
|
|
59
|
-
abort_with_error "Error: #{did.inspect} is not a valid DID"
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def validate_nsid(collection)
|
|
64
|
-
unless collection =~ NSID_REGEXP
|
|
65
|
-
abort_with_error "Error: #{collection.inspect} is not a valid collection NSID"
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def parse_at_uri(uri)
|
|
70
|
-
unless uri.start_with?('at://')
|
|
71
|
-
abort_with_error "Error: not an at:// URI: #{uri.inspect}"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
unless uri =~ /\Aat:\/\/([^\/]+)\/([^\/]+)\/([^\/]+)\z/
|
|
75
|
-
abort_with_error "Error: invalid at:// URI: #{uri.inspect}"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
[$1, $2, $3]
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def run_fetch(args)
|
|
82
|
-
uri = args.shift
|
|
83
|
-
|
|
84
|
-
if uri.nil?
|
|
85
|
-
abort_with_error "Usage: #{$PROGRAM_NAME} fetch <at://uri>"
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
if !args.empty?
|
|
89
|
-
abort_with_error "Error: Unexpected arguments for fetch: #{args.join(' ')}"
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
repo, collection, rkey = parse_at_uri(uri)
|
|
93
|
-
|
|
94
|
-
begin
|
|
95
|
-
pds = DID.new(repo).document.pds_host
|
|
96
|
-
sky = Minisky.new(pds, nil)
|
|
97
|
-
|
|
98
|
-
response = sky.get_request('com.atproto.repo.getRecord', {
|
|
99
|
-
repo: repo, collection: collection, rkey: rkey
|
|
100
|
-
})
|
|
101
|
-
rescue StandardError => e
|
|
102
|
-
abort_with_error "Error loading record: #{e.class}: #{e.message}"
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
puts JSON.pretty_generate(response['value'])
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def parse_resolve_options(args)
|
|
109
|
-
options = {}
|
|
110
|
-
|
|
111
|
-
parser = OptionParser.new do |opts|
|
|
112
|
-
opts.banner = "Usage: #{$PROGRAM_NAME} resolve <did>|<handle> [options]"
|
|
113
|
-
|
|
114
|
-
opts.on('-d', '--did', 'Print only the DID resolved from the handle') do
|
|
115
|
-
options[:only_did] = true
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
opts.on('-p', '--pds', 'Print only the PDS endpoint') do
|
|
119
|
-
options[:only_pds] = true
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
opts.on('-h', '--help', 'Show resolve-specific help') do
|
|
123
|
-
puts opts
|
|
124
|
-
exit
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
remaining = []
|
|
129
|
-
|
|
130
|
-
begin
|
|
131
|
-
parser.order!(args) { |other| remaining << other }
|
|
132
|
-
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
133
|
-
puts "Error: #{e.message}"
|
|
134
|
-
puts parser
|
|
135
|
-
exit 1
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
if options[:only_did] && options[:only_pds]
|
|
139
|
-
abort_with_error "--pds and --did options cannot be used together"
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
[options, remaining]
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def run_resolve(args)
|
|
146
|
-
options, arguments = parse_resolve_options(args)
|
|
147
|
-
target = arguments.shift
|
|
148
|
-
|
|
149
|
-
if target.nil?
|
|
150
|
-
abort_with_error "Usage: #{$PROGRAM_NAME} resolve <did>|<handle> [options]"
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
if !args.empty?
|
|
154
|
-
abort_with_error "Error: Unexpected arguments for resolve: #{args.join(' ')}"
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
did = DID.resolve_handle(target)
|
|
158
|
-
|
|
159
|
-
if did.nil?
|
|
160
|
-
abort_with_error "Couldn't resolve #{target}"
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
if options[:only_did]
|
|
164
|
-
puts did
|
|
165
|
-
elsif options[:only_pds]
|
|
166
|
-
puts did.document.pds_endpoint
|
|
167
|
-
else
|
|
168
|
-
puts JSON.pretty_generate(did.document.json)
|
|
169
|
-
end
|
|
170
|
-
rescue StandardError => e
|
|
171
|
-
abort_with_error "Error resolving #{target.inspect}: #{e.class}: #{e.message}"
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def parse_stream_options(args)
|
|
175
|
-
options = {}
|
|
176
|
-
|
|
177
|
-
parser = OptionParser.new do |opts|
|
|
178
|
-
opts.banner = "Usage: #{$PROGRAM_NAME} stream <relay.host> [options]"
|
|
179
|
-
|
|
180
|
-
opts.on('-j', '--jetstream', 'Use a Jetstream source') do
|
|
181
|
-
options[:jetstream] = true
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
opts.on('-rCURSOR', '--cursor=CURSOR', 'Start from a given cursor') do |cursor|
|
|
185
|
-
options[:cursor] = cursor
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
opts.on('-dLIST', '--did=LIST', 'Filter only events from given DID(s) (comma-separated or repeated)') do |list|
|
|
189
|
-
items = list.split(',').map(&:strip).reject(&:empty?)
|
|
190
|
-
|
|
191
|
-
if items.empty?
|
|
192
|
-
abort_with_error "Error: empty argument to -d/--did"
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
options[:dids] ||= []
|
|
196
|
-
options[:dids] += items
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
opts.on('-cLIST', '--collection=LIST', 'Filter only events of given collections') do |list|
|
|
200
|
-
items = list.split(',').map(&:strip).reject(&:empty?)
|
|
201
|
-
|
|
202
|
-
if items.empty?
|
|
203
|
-
abort_with_error "Error: empty argument to -c/--collection"
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
options[:collections] ||= []
|
|
207
|
-
options[:collections] += items
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
opts.on('-h', '--help', 'Show stream-specific help') do
|
|
211
|
-
puts opts
|
|
212
|
-
exit
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
remaining = []
|
|
217
|
-
|
|
218
|
-
begin
|
|
219
|
-
parser.order!(args) { |other| remaining << other }
|
|
220
|
-
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
221
|
-
puts "Error: #{e.message}"
|
|
222
|
-
puts parser
|
|
223
|
-
exit 1
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
[options, remaining]
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def run_stream(args)
|
|
230
|
-
options, arguments = parse_stream_options(args)
|
|
231
|
-
|
|
232
|
-
service = arguments.shift
|
|
233
|
-
|
|
234
|
-
if service.nil?
|
|
235
|
-
abort_with_error "Usage: #{$PROGRAM_NAME} stream <firehose-host> [options]"
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
if !arguments.empty?
|
|
239
|
-
abort_with_error "Error: Unexpected arguments for stream: #{arguments.join(' ')}"
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
if options[:cursor] && options[:cursor] !~ /\A\d+\z/
|
|
243
|
-
abort_with_error "Error: cursor must be a decimal integer, got: #{options[:cursor].inspect}"
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
if options[:dids]
|
|
247
|
-
options[:dids] = options[:dids].map { |did| check_stream_did(did) }
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
if options[:collections]
|
|
251
|
-
options[:collections].each { |collection| validate_nsid(collection) }
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
if options[:jetstream]
|
|
255
|
-
jet_opts = {}
|
|
256
|
-
jet_opts[:cursor] = options[:cursor].to_i if options[:cursor]
|
|
257
|
-
|
|
258
|
-
# pass DID/collection filters to Jetstream to filter events server-side
|
|
259
|
-
jet_opts[:wanted_dids] = options[:dids] if options[:dids]
|
|
260
|
-
jet_opts[:wanted_collections] = options[:collections] if options[:collections]
|
|
261
|
-
|
|
262
|
-
sky = Skyfall::Jetstream.new(service, jet_opts)
|
|
263
|
-
else
|
|
264
|
-
cursor = options[:cursor]&.to_i
|
|
265
|
-
sky = Skyfall::Firehose.new(service, :subscribe_repos, cursor)
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
sky.on_connecting { |url| puts "Connecting to #{url}..." }
|
|
269
|
-
sky.on_connect { puts "Connected" }
|
|
270
|
-
sky.on_disconnect { puts "Disconnected" }
|
|
271
|
-
sky.on_reconnect { puts "Connection lost, trying to reconnect..." }
|
|
272
|
-
sky.on_timeout { puts "Connection stalled, triggering a reconnect..." }
|
|
273
|
-
sky.on_error { |e| puts "ERROR: #{e}" }
|
|
274
|
-
|
|
275
|
-
sky.on_message do |msg|
|
|
276
|
-
next unless msg.type == :commit
|
|
277
|
-
next if options[:dids] && !options[:dids].include?(msg.repo)
|
|
278
|
-
|
|
279
|
-
time = msg.time.getlocal.iso8601
|
|
280
|
-
|
|
281
|
-
msg.operations.each do |op|
|
|
282
|
-
next if options[:collections] && !options[:collections].include?(op.collection)
|
|
283
|
-
|
|
284
|
-
json = op.raw_record && JSON.generate(op.raw_record)
|
|
285
|
-
puts "[#{time}] #{msg.repo} #{op.action.inspect} #{op.collection} #{op.rkey} #{json}"
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
trap('SIGINT') do
|
|
290
|
-
puts 'Disconnecting...'
|
|
291
|
-
sky.disconnect
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
sky.connect
|
|
295
|
-
|
|
296
|
-
puts "Stopped stream at cursor #{sky.cursor}"
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
if ARGV.empty?
|
|
300
|
-
print_help
|
|
301
|
-
exit
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
cmd = ARGV.shift
|
|
305
|
-
|
|
306
|
-
case cmd
|
|
307
|
-
when 'help', '--help', '-h'
|
|
308
|
-
print_help
|
|
309
|
-
when 'version', '--version'
|
|
310
|
-
puts "RatProto #{RatProto::VERSION} 🐀"
|
|
311
|
-
when 'fetch'
|
|
312
|
-
run_fetch(ARGV)
|
|
313
|
-
when 'resolve'
|
|
314
|
-
run_resolve(ARGV)
|
|
315
|
-
when 'stream'
|
|
316
|
-
require 'skyfall'
|
|
317
|
-
run_stream(ARGV)
|
|
318
|
-
else
|
|
319
|
-
abort_with_error "Error: unknown command: #{cmd}"
|
|
320
|
-
end
|
|
7
|
+
RatProto::CLI.run
|
data/lib/ratproto/cli.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'clamp'
|
|
4
|
+
|
|
5
|
+
require_relative 'version'
|
|
6
|
+
require_relative 'commands/base'
|
|
7
|
+
require_relative 'commands/fetch'
|
|
8
|
+
require_relative 'commands/resolve'
|
|
9
|
+
require_relative 'commands/stream'
|
|
10
|
+
|
|
11
|
+
Clamp.allow_options_after_parameters = true
|
|
12
|
+
|
|
13
|
+
module RatProto
|
|
14
|
+
module Commands
|
|
15
|
+
class Version < Base
|
|
16
|
+
banner 'Show version'
|
|
17
|
+
|
|
18
|
+
def execute
|
|
19
|
+
puts "RatProto #{RatProto::VERSION} 🐀"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Help < Base
|
|
24
|
+
banner 'Show help for rat or one of its commands'
|
|
25
|
+
|
|
26
|
+
parameter '[COMMAND]', 'command to describe'
|
|
27
|
+
|
|
28
|
+
def execute
|
|
29
|
+
program_name = File.basename($PROGRAM_NAME)
|
|
30
|
+
|
|
31
|
+
if command
|
|
32
|
+
command_class = RatProto::CLI.find_subcommand_class(command)
|
|
33
|
+
signal_usage_error "unknown command: #{command}" unless command_class
|
|
34
|
+
|
|
35
|
+
puts command_class.help("#{program_name} #{command}")
|
|
36
|
+
else
|
|
37
|
+
puts RatProto::CLI.help(program_name)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class CLI < Commands::Base
|
|
44
|
+
banner "RatProto #{RatProto::VERSION} 🐀 — Ruby ATProto CLI"
|
|
45
|
+
|
|
46
|
+
option '--version', :flag, 'show version' do
|
|
47
|
+
Commands::Version.run("", [])
|
|
48
|
+
exit
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
subcommand 'fetch', 'fetch a single record given its at:// URI', Commands::Fetch
|
|
52
|
+
subcommand 'stream', 'stream events from a relay or PDS firehose', Commands::Stream
|
|
53
|
+
subcommand 'resolve', 'resolve a DID or @handle', Commands::Resolve
|
|
54
|
+
subcommand 'help', 'show help', Commands::Help
|
|
55
|
+
subcommand 'version', 'show version', Commands::Version
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'clamp'
|
|
4
|
+
|
|
5
|
+
module RatProto
|
|
6
|
+
module Commands
|
|
7
|
+
class Base < Clamp::Command
|
|
8
|
+
DID_REGEXP = /\Adid:[a-z]+:[a-zA-Z0-9.\-_]+\z/
|
|
9
|
+
NSID_REGEXP = /\A[a-z0-9]+(\.[a-z0-9]+)+\z/
|
|
10
|
+
HANDLE_REGEXP = /\A[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)+\z/
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def parse_at_uri(value)
|
|
15
|
+
signal_usage_error "not an at:// URI: #{value.inspect}" unless value.start_with?('at://')
|
|
16
|
+
|
|
17
|
+
match = value.match(/\Aat:\/\/([^\/]+)\/([^\/]+)\/([^\/]+)\z/)
|
|
18
|
+
signal_usage_error "invalid at:// URI: #{value.inspect}" unless match
|
|
19
|
+
|
|
20
|
+
match.captures
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def comma_separated(values, option_name)
|
|
24
|
+
values = (values || []).map { |value|
|
|
25
|
+
args = value.split(',').map(&:strip).reject(&:empty?)
|
|
26
|
+
signal_usage_error "empty argument to #{option_name}" if args.empty?
|
|
27
|
+
args
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
values.flatten
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def map_argument_to_did(did)
|
|
34
|
+
if did =~ DID_REGEXP
|
|
35
|
+
did
|
|
36
|
+
elsif did =~ HANDLE_REGEXP
|
|
37
|
+
if resolved_did = DID.resolve_handle(did)
|
|
38
|
+
resolved_did.to_s
|
|
39
|
+
else
|
|
40
|
+
signal_error "couldn't resolve handle @#{did}"
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
signal_usage_error "#{did.inspect} is not a valid DID"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_nsid(collection)
|
|
48
|
+
return collection if collection =~ NSID_REGEXP
|
|
49
|
+
|
|
50
|
+
signal_usage_error "#{collection.inspect} is not a valid collection NSID"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'didkit'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
require_relative 'base'
|
|
7
|
+
|
|
8
|
+
module RatProto
|
|
9
|
+
module Commands
|
|
10
|
+
class Fetch < Base
|
|
11
|
+
banner 'Fetch a single record given its at:// URI'
|
|
12
|
+
|
|
13
|
+
parameter 'URI', 'ATProto record URI'
|
|
14
|
+
|
|
15
|
+
def execute
|
|
16
|
+
require 'minisky'
|
|
17
|
+
|
|
18
|
+
repo, collection, rkey = parse_at_uri(uri)
|
|
19
|
+
response = load_record(repo, collection, rkey)
|
|
20
|
+
puts JSON.pretty_generate(response['value'])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def load_record(repo, collection, rkey)
|
|
27
|
+
pds = DID.new(repo).document.pds_host
|
|
28
|
+
sky = Minisky.new(pds, nil)
|
|
29
|
+
|
|
30
|
+
sky.get_request('com.atproto.repo.getRecord', {
|
|
31
|
+
repo: repo, collection: collection, rkey: rkey
|
|
32
|
+
})
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
signal_error "loading record: #{e.class}: #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'didkit'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
require_relative 'base'
|
|
7
|
+
|
|
8
|
+
module RatProto
|
|
9
|
+
module Commands
|
|
10
|
+
class Resolve < Base
|
|
11
|
+
banner 'Resolve a DID or @handle'
|
|
12
|
+
|
|
13
|
+
parameter 'TARGET', 'DID or handle'
|
|
14
|
+
|
|
15
|
+
option ['-d', '--did'], :flag, 'print only the DID resolved from the handle'
|
|
16
|
+
option ['-p', '--pds'], :flag, 'print only the PDS endpoint'
|
|
17
|
+
|
|
18
|
+
def execute
|
|
19
|
+
signal_usage_error '--pds and --did options cannot be used together' if did? && pds?
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
resolved_did = DID.resolve_handle(target)
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
signal_error "resolving #{target.inspect}: #{e.class}: #{e.message}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if resolved_did.nil?
|
|
28
|
+
signal_error "couldn't resolve #{target}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if did?
|
|
32
|
+
puts resolved_did
|
|
33
|
+
elsif pds?
|
|
34
|
+
puts resolved_did.document.pds_endpoint
|
|
35
|
+
else
|
|
36
|
+
puts JSON.pretty_generate(resolved_did.document.json)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'didkit'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
require_relative 'base'
|
|
8
|
+
|
|
9
|
+
module RatProto
|
|
10
|
+
module Commands
|
|
11
|
+
class Stream < Base
|
|
12
|
+
banner 'Stream events from a relay or PDS firehose'
|
|
13
|
+
|
|
14
|
+
parameter 'SERVICE', 'relay, PDS or Jetstream hostname'
|
|
15
|
+
|
|
16
|
+
option ['-j', '--jetstream'], :flag, 'use a Jetstream source'
|
|
17
|
+
|
|
18
|
+
option ['-r', '--cursor'], 'CURSOR', 'start from a given cursor' do |value|
|
|
19
|
+
raise ArgumentError, 'must be a decimal integer' unless value =~ /\A\d+\z/
|
|
20
|
+
|
|
21
|
+
value.to_i
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
option ['-d', '--did'], 'DID[,DID...]', 'filter events by DID (comma-separated or repeated)',
|
|
25
|
+
multivalued: true, attribute_name: :did_values
|
|
26
|
+
|
|
27
|
+
option ['-c', '--collection'], 'NSID[,NSID...]', 'filter events by collection (comma-separated or repeated)',
|
|
28
|
+
multivalued: true, attribute_name: :collection_values
|
|
29
|
+
|
|
30
|
+
def execute
|
|
31
|
+
require 'skyfall'
|
|
32
|
+
|
|
33
|
+
dids = comma_separated(did_values, '-d/--did')
|
|
34
|
+
collections = comma_separated(collection_values, '-c/--collection')
|
|
35
|
+
|
|
36
|
+
dids = (dids.length > 0) ? dids.map { |did| map_argument_to_did(did) } : nil
|
|
37
|
+
collections = (collections.length > 0) ? collections.map { |collection| validate_nsid(collection) } : nil
|
|
38
|
+
|
|
39
|
+
sky = build_stream(dids, collections)
|
|
40
|
+
configure_callbacks(sky, dids, collections)
|
|
41
|
+
configure_interrupt_handler(sky)
|
|
42
|
+
|
|
43
|
+
sky.connect
|
|
44
|
+
|
|
45
|
+
puts "Stopped stream at cursor #{sky.cursor}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build_stream(dids, collections)
|
|
52
|
+
if jetstream?
|
|
53
|
+
options = {}
|
|
54
|
+
options[:cursor] = cursor if cursor
|
|
55
|
+
|
|
56
|
+
# pass DID/collection filters to Jetstream to filter events server-side
|
|
57
|
+
options[:wanted_dids] = dids if dids
|
|
58
|
+
options[:wanted_collections] = collections if collections
|
|
59
|
+
|
|
60
|
+
Skyfall::Jetstream.new(service, options)
|
|
61
|
+
else
|
|
62
|
+
Skyfall::Firehose.new(service, :subscribe_repos, cursor)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def configure_callbacks(sky, dids, collections)
|
|
67
|
+
sky.on_connecting { |url| puts "Connecting to #{url}..." }
|
|
68
|
+
sky.on_connect { puts "Connected" }
|
|
69
|
+
sky.on_disconnect { puts "Disconnected" }
|
|
70
|
+
sky.on_reconnect { puts "Connection lost, trying to reconnect..." }
|
|
71
|
+
sky.on_timeout { puts "Connection stalled, triggering a reconnect..." }
|
|
72
|
+
sky.on_error { |e| puts "ERROR: #{e}" }
|
|
73
|
+
|
|
74
|
+
sky.on_message do |msg|
|
|
75
|
+
next unless msg.type == :commit
|
|
76
|
+
next if dids && !dids.include?(msg.repo)
|
|
77
|
+
|
|
78
|
+
time = msg.time.getlocal.iso8601
|
|
79
|
+
|
|
80
|
+
msg.operations.each do |op|
|
|
81
|
+
next if collections && !collections.include?(op.collection)
|
|
82
|
+
|
|
83
|
+
json = op.raw_record && JSON.generate(op.raw_record)
|
|
84
|
+
puts "[#{time}] (#{msg.seq}) #{msg.repo} #{op.action.inspect} #{op.collection} #{op.rkey} #{json}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def configure_interrupt_handler(sky)
|
|
90
|
+
trap('SIGINT') do
|
|
91
|
+
puts
|
|
92
|
+
puts 'Disconnecting...'
|
|
93
|
+
sky.disconnect
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/ratproto/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ratproto
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kuba Suder
|
|
@@ -69,6 +69,26 @@ dependencies:
|
|
|
69
69
|
- - "<"
|
|
70
70
|
- !ruby/object:Gem::Version
|
|
71
71
|
version: '2.0'
|
|
72
|
+
- !ruby/object:Gem::Dependency
|
|
73
|
+
name: clamp
|
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '1.5'
|
|
79
|
+
- - "<"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '2.0'
|
|
82
|
+
type: :runtime
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.5'
|
|
89
|
+
- - "<"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '2.0'
|
|
72
92
|
email:
|
|
73
93
|
- jakub.suder@gmail.com
|
|
74
94
|
executables:
|
|
@@ -81,6 +101,11 @@ files:
|
|
|
81
101
|
- README.md
|
|
82
102
|
- exe/rat
|
|
83
103
|
- lib/ratproto.rb
|
|
104
|
+
- lib/ratproto/cli.rb
|
|
105
|
+
- lib/ratproto/commands/base.rb
|
|
106
|
+
- lib/ratproto/commands/fetch.rb
|
|
107
|
+
- lib/ratproto/commands/resolve.rb
|
|
108
|
+
- lib/ratproto/commands/stream.rb
|
|
84
109
|
- lib/ratproto/version.rb
|
|
85
110
|
- sig/ratproto.rbs
|
|
86
111
|
homepage: https://ruby.sdk.blue
|
|
@@ -104,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
104
129
|
- !ruby/object:Gem::Version
|
|
105
130
|
version: '0'
|
|
106
131
|
requirements: []
|
|
107
|
-
rubygems_version:
|
|
132
|
+
rubygems_version: 3.6.9
|
|
108
133
|
specification_version: 4
|
|
109
134
|
summary: Ruby CLI tool for accessing Bluesky API / ATProto
|
|
110
135
|
test_files: []
|