ratproto 0.1.1 → 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 +11 -0
- data/README.md +1 -1
- data/exe/rat +2 -260
- 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
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## [0.2.0] - 2026-07-28
|
|
2
|
+
|
|
3
|
+
- converted the codebase from a single script in `exe/rat` to per-command classes using Clamp framework
|
|
4
|
+
|
|
5
|
+
## [0.1.2] - 2026-05-24
|
|
6
|
+
|
|
7
|
+
- `resolve`: added `-d` and `-p` options to print only the DID or the PDS endpoint respectively
|
|
8
|
+
- `stream`: print the last received cursor when stopped
|
|
9
|
+
- `stream`: accept also handles instead of DIDs for the `-d` argument
|
|
10
|
+
- lazy-load `skyfall` only in `stream`
|
|
11
|
+
|
|
1
12
|
## [0.1.1] - 2026-01-03
|
|
2
13
|
|
|
3
14
|
- fixed rat emoji in the help output :D
|
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,264 +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 'skyfall'
|
|
10
|
-
require 'time'
|
|
11
|
-
require 'uri'
|
|
5
|
+
require 'ratproto/cli'
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
DID_REGEXP = /\Adid:[a-z]+:[a-zA-Z0-9.\-_]+\z/
|
|
16
|
-
NSID_REGEXP = /\A[a-z0-9]+(\.[a-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 validate_did(did)
|
|
50
|
-
unless did =~ DID_REGEXP
|
|
51
|
-
abort_with_error "Error: #{did.inspect} is not a valid DID"
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def validate_nsid(collection)
|
|
56
|
-
unless collection =~ NSID_REGEXP
|
|
57
|
-
abort_with_error "Error: #{collection.inspect} is not a valid collection NSID"
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def parse_at_uri(uri)
|
|
62
|
-
unless uri.start_with?('at://')
|
|
63
|
-
abort_with_error "Error: not an at:// URI: #{uri.inspect}"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
unless uri =~ /\Aat:\/\/([^\/]+)\/([^\/]+)\/([^\/]+)\z/
|
|
67
|
-
abort_with_error "Error: invalid at:// URI: #{uri.inspect}"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
[$1, $2, $3]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def run_fetch(args)
|
|
74
|
-
uri = args.shift
|
|
75
|
-
|
|
76
|
-
if uri.nil?
|
|
77
|
-
abort_with_error "Usage: #{$PROGRAM_NAME} fetch <at://uri>"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
if !args.empty?
|
|
81
|
-
abort_with_error "Error: Unexpected arguments for fetch: #{args.join(' ')}"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
repo, collection, rkey = parse_at_uri(uri)
|
|
85
|
-
|
|
86
|
-
begin
|
|
87
|
-
pds = DID.new(repo).document.pds_host
|
|
88
|
-
sky = Minisky.new(pds, nil)
|
|
89
|
-
|
|
90
|
-
response = sky.get_request('com.atproto.repo.getRecord', {
|
|
91
|
-
repo: repo, collection: collection, rkey: rkey
|
|
92
|
-
})
|
|
93
|
-
rescue StandardError => e
|
|
94
|
-
abort_with_error "Error loading record: #{e.class}: #{e.message}"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
puts JSON.pretty_generate(response['value'])
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def run_resolve(args)
|
|
101
|
-
target = args.shift
|
|
102
|
-
|
|
103
|
-
if target.nil?
|
|
104
|
-
abort_with_error "Usage: #{$PROGRAM_NAME} resolve <did>|<handle>"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
if !args.empty?
|
|
108
|
-
abort_with_error "Error: Unexpected arguments for resolve: #{args.join(' ')}"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
did = DID.resolve_handle(target)
|
|
112
|
-
|
|
113
|
-
if did.nil?
|
|
114
|
-
abort_with_error "Couldn't resolve #{target}"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
puts JSON.pretty_generate(did.document.json)
|
|
118
|
-
rescue StandardError => e
|
|
119
|
-
abort_with_error "Error resolving #{target.inspect}: #{e.class}: #{e.message}"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def parse_stream_options(args)
|
|
123
|
-
options = {}
|
|
124
|
-
|
|
125
|
-
parser = OptionParser.new do |opts|
|
|
126
|
-
opts.banner = "Usage: #{$PROGRAM_NAME} stream <relay.host> [options]"
|
|
127
|
-
|
|
128
|
-
opts.on('-j', '--jetstream', 'Use a Jetstream source') do
|
|
129
|
-
options[:jetstream] = true
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
opts.on('-rCURSOR', '--cursor=CURSOR', 'Start from a given cursor') do |cursor|
|
|
133
|
-
options[:cursor] = cursor
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
opts.on('-dLIST', '--did=LIST', 'Filter only events from given DID(s) (comma-separated or repeated)') do |list|
|
|
137
|
-
items = list.split(',').map(&:strip).reject(&:empty?)
|
|
138
|
-
|
|
139
|
-
if items.empty?
|
|
140
|
-
abort_with_error "Error: empty argument to -d/--did"
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
options[:dids] ||= []
|
|
144
|
-
options[:dids] += items
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
opts.on('-cLIST', '--collection=LIST', 'Filter only events of given collections') do |list|
|
|
148
|
-
items = list.split(',').map(&:strip).reject(&:empty?)
|
|
149
|
-
|
|
150
|
-
if items.empty?
|
|
151
|
-
abort_with_error "Error: empty argument to -c/--collection"
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
options[:collections] ||= []
|
|
155
|
-
options[:collections] += items
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
opts.on('-h', '--help', 'Show stream-specific help') do
|
|
159
|
-
puts opts
|
|
160
|
-
exit
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
remaining = []
|
|
165
|
-
|
|
166
|
-
begin
|
|
167
|
-
parser.order!(args) { |other| remaining << other }
|
|
168
|
-
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
169
|
-
puts "Error: #{e.message}"
|
|
170
|
-
puts parser
|
|
171
|
-
exit 1
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
[options, remaining]
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def run_stream(args)
|
|
178
|
-
options, arguments = parse_stream_options(args)
|
|
179
|
-
|
|
180
|
-
service = arguments.shift
|
|
181
|
-
|
|
182
|
-
if service.nil?
|
|
183
|
-
abort_with_error "Usage: #{$PROGRAM_NAME} stream <firehose-host> [options]"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
if !arguments.empty?
|
|
187
|
-
abort_with_error "Error: Unexpected arguments for stream: #{arguments.join(' ')}"
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
if options[:cursor] && options[:cursor] !~ /\A\d+\z/
|
|
191
|
-
abort_with_error "Error: cursor must be a decimal integer, got: #{options[:cursor].inspect}"
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
if options[:dids]
|
|
195
|
-
options[:dids].each { |did| validate_did(did) }
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
if options[:collections]
|
|
199
|
-
options[:collections].each { |collection| validate_nsid(collection) }
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
if options[:jetstream]
|
|
203
|
-
jet_opts = {}
|
|
204
|
-
jet_opts[:cursor] = options[:cursor].to_i if options[:cursor]
|
|
205
|
-
|
|
206
|
-
# pass DID/collection filters to Jetstream to filter events server-side
|
|
207
|
-
jet_opts[:wanted_dids] = options[:dids] if options[:dids]
|
|
208
|
-
jet_opts[:wanted_collections] = options[:collections] if options[:collections]
|
|
209
|
-
|
|
210
|
-
sky = Skyfall::Jetstream.new(service, jet_opts)
|
|
211
|
-
else
|
|
212
|
-
cursor = options[:cursor]&.to_i
|
|
213
|
-
sky = Skyfall::Firehose.new(service, :subscribe_repos, cursor)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
sky.on_connecting { |url| puts "Connecting to #{url}..." }
|
|
217
|
-
sky.on_connect { puts "Connected" }
|
|
218
|
-
sky.on_disconnect { puts "Disconnected" }
|
|
219
|
-
sky.on_reconnect { puts "Connection lost, trying to reconnect..." }
|
|
220
|
-
sky.on_timeout { puts "Connection stalled, triggering a reconnect..." }
|
|
221
|
-
sky.on_error { |e| puts "ERROR: #{e}" }
|
|
222
|
-
|
|
223
|
-
sky.on_message do |msg|
|
|
224
|
-
next unless msg.type == :commit
|
|
225
|
-
next if options[:dids] && !options[:dids].include?(msg.repo)
|
|
226
|
-
|
|
227
|
-
time = msg.time.getlocal.iso8601
|
|
228
|
-
|
|
229
|
-
msg.operations.each do |op|
|
|
230
|
-
next if options[:collections] && !options[:collections].include?(op.collection)
|
|
231
|
-
|
|
232
|
-
json = op.raw_record && JSON.generate(op.raw_record)
|
|
233
|
-
puts "[#{time}] #{msg.repo} #{op.action.inspect} #{op.collection} #{op.rkey} #{json}"
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
trap('SIGINT') do
|
|
238
|
-
puts 'Disconnecting...'
|
|
239
|
-
sky.disconnect
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
sky.connect
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
if ARGV.empty?
|
|
246
|
-
print_help
|
|
247
|
-
exit
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
cmd = ARGV.shift
|
|
251
|
-
|
|
252
|
-
case cmd
|
|
253
|
-
when 'help', '--help', '-h'
|
|
254
|
-
print_help
|
|
255
|
-
when 'version', '--version'
|
|
256
|
-
puts "RatProto #{RatProto::VERSION} 🐀"
|
|
257
|
-
when 'fetch'
|
|
258
|
-
run_fetch(ARGV)
|
|
259
|
-
when 'resolve'
|
|
260
|
-
run_resolve(ARGV)
|
|
261
|
-
when 'stream'
|
|
262
|
-
run_stream(ARGV)
|
|
263
|
-
else
|
|
264
|
-
abort_with_error "Error: unknown command: #{cmd}"
|
|
265
|
-
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: []
|