ratproto 0.0.1 β†’ 0.1.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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -1
  3. data/README.md +157 -17
  4. data/exe/rat +120 -258
  5. data/lib/ratproto/version.rb +1 -1
  6. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1495c9728dd1630d8f9b62e14187dd7066c16afbbc31161deac5697ee67f5d0
4
- data.tar.gz: dbfa95dd48889d5c1a5fd06489ea316aeb2e4ec475cf54fe922bd3f31ca28aa8
3
+ metadata.gz: a51437de2f57f2f70c839165f4721e3b9fc05ffced806ce652971d7bfe4d5e01
4
+ data.tar.gz: 6fd9738bde5c0c366a28f36e79c496e18f0686331af7b2f2b8385f60e8827024
5
5
  SHA512:
6
- metadata.gz: 6119eda973e4279d58dfad1bc1f35aabd42de1dcbc892808b649593fb09a0a063cfdfec239e4af6c83ec203833c108201a36b4d08fbe2631b0e2c1bbf0ce4b49
7
- data.tar.gz: e7feeeb64a73705722cf5f4fc457617349c846b310815f6d2bddbb985a171c1c9e1017fb3070a949c7e994eb4cc33f1857b189529e218aeae6303557cc954dfe
6
+ metadata.gz: 38410b341c5c79fad987cea4e4d6430cda0c63735f58f204497ac8e240525729ab00e01e894512c2d57b55a1da1e913a233da977d084f1f23d909be2f55f1ffb
7
+ data.tar.gz: 0264f915d70d891447af5e87bc7b88635a1b530fd91e50eebcf9c9902c8037a21c2ab8d2e135ecf9519280dfe2d4ae765819f45c04341c81422709588efe42c7
data/CHANGELOG.md CHANGED
@@ -1 +1,11 @@
1
- ## [Unreleased]
1
+ ## [0.1.1] - 2026-01-03
2
+
3
+ - fixed rat emoji in the help output :D
4
+
5
+ ## [0.1] - 2026-01-03
6
+
7
+ - cleaned up / rewritten the code
8
+
9
+ ## [0.0.1] - 2026-01-02
10
+
11
+ - first working proof of concept
data/README.md CHANGED
@@ -1,35 +1,175 @@
1
- # Ratproto
1
+ # RatProto – Ruby ATProto Tool πŸ€
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ RatProto (`rat`) is a small command-line tool in Ruby for accessing the Bluesky API / AT Protocol.
4
+
5
+ It builds on top of the existing ATProto Ruby gems:
6
+
7
+ - [`minisky`](https://tangled.org/mackuba.eu/minisky/) β€” XRPC client
8
+ - [`skyfall`](https://tangled.org/mackuba.eu/skyfall/) β€” firehose & Jetstream streaming
9
+ - [`didkit`](https://tangled.org/mackuba.eu/didkit/) β€” DID & handle resolution
10
+
11
+ > [!NOTE]
12
+ > Part of ATProto Ruby SDK: [ruby.sdk.blue](https://ruby.sdk.blue)
4
13
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ratproto`. To experiment with that code, run `bin/console` for an interactive prompt.
6
14
 
7
15
  ## Installation
8
16
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
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.2+.
18
+
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
+
21
+ To install `rat`, run:
22
+
23
+ ```
24
+ [sudo] gem install ratproto
25
+ ```
26
+
27
+ ## Features
28
+
29
+ Currently implemented features/commands:
30
+
31
+ - resolving DIDs & handles (`rat resolve`)
32
+ - fetching & printing ATProto records (`rat fetch`)
33
+ - streaming firehose / Jetstream events with optional filters (`rat stream`)
34
+
35
+
36
+ ## Resolving a DID or handle
37
+
38
+ ```
39
+ rat resolve <did>|<handle>
40
+ ```
41
+
42
+ Pass a DID or a handle (@ optional) to look up the given account's identity & print the DID document:
43
+
44
+ ```
45
+ $ rat resolve atproto.com
46
+
47
+ {
48
+ "@context": [
49
+ "https://www.w3.org/ns/did/v1",
50
+ "https://w3id.org/security/multikey/v1",
51
+ "https://w3id.org/security/suites/secp256k1-2019/v1"
52
+ ],
53
+ "id": "did:plc:ewvi7nxzyoun6zhxrhs64oiz",
54
+ "alsoKnownAs": [
55
+ "at://atproto.com"
56
+ ],
57
+ "verificationMethod": [
58
+ {
59
+ "id": "did:plc:ewvi7nxzyoun6zhxrhs64oiz#atproto",
60
+ "type": "Multikey",
61
+ "controller": "did:plc:ewvi7nxzyoun6zhxrhs64oiz",
62
+ "publicKeyMultibase": "zQ3shunBKsXixLxKtC5qeSG9E4J5RkGN57im31pcTzbNQnm5w"
63
+ }
64
+ ],
65
+ "service": [
66
+ {
67
+ "id": "#atproto_pds",
68
+ "type": "AtprotoPersonalDataServer",
69
+ "serviceEndpoint": "https://enoki.us-east.host.bsky.network"
70
+ }
71
+ ]
72
+ }
73
+ ```
10
74
 
11
- Install the gem and add to the application's Gemfile by executing:
12
75
 
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
76
+ ## Fetching a record
77
+
78
+ ```
79
+ rat fetch at://<did>/<collection>/<rkey>
15
80
  ```
16
81
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
82
+ Pass an at:// URI as the argument to fetch a single record from the user’s PDS:
83
+
84
+ ```
85
+ % rat fetch at://did:plc:ragtjsm2j2vknwkz3zp4oxrd/app.bsky.feed.post/3lxxmboqmf22j
18
86
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
87
+ {
88
+ "text": "a gatinha gorda",
89
+ "$type": "app.bsky.feed.post",
90
+ "embed": {
91
+ "$type": "app.bsky.embed.images",
92
+ "images": [
93
+ {
94
+ "alt": "Kit lying on the ground under the chair ",
95
+ "image": {
96
+ "$type": "blob",
97
+ "ref": {
98
+ "$link": "bafkreiebzsetrrvymvq6dvwma77zqprnv73ovgeqilanzi453dm6xart4q"
99
+ },
100
+ "mimeType": "image/jpeg",
101
+ "size": 963750
102
+ },
103
+ "aspectRatio": {
104
+ "width": 2000,
105
+ "height": 1500
106
+ }
107
+ }
108
+ ]
109
+ },
110
+ "langs": [
111
+ "en"
112
+ ],
113
+ "createdAt": "2025-09-03T21:48:05.910Z"
114
+ }
21
115
  ```
22
116
 
23
- ## Usage
117
+ ## Streaming commit events
118
+
119
+ ```
120
+ rat stream <firehose-host> [-j] [-r cursor] [-c collections] [-d dids]
121
+ ```
24
122
 
25
- TODO: Write usage instructions here
123
+ Rat can connect to either a relay/PDS firehose:
124
+
125
+ ```
126
+ rat stream bsky.network
127
+ ```
128
+
129
+ or a Jetstream service (use `-j`):
130
+
131
+ ```
132
+ rat stream -j jetstream2.us-east.bsky.network
133
+ ```
134
+
135
+ You can also pass a cursor to connect with using `-r` / `--cursor` (tip: pass `-r0` to rewind as far back as the buffer allows).
136
+
137
+ Once connected, you’ll see output like:
138
+
139
+ ```
140
+ [2025-01-02T12:34:56+01:00] did:plc:xwnehmdpjluz2kv3oh2mfi47 :create app.bsky.feed.post 3mbjs5rgtin2z {"text":"hi", ...}
141
+ [2025-01-02T12:34:57+01:00] did:plc:3mfkuhmjxd2wvz2e7nhl4poi :delete app.bsky.graph.follow 3mbjs5rghri23
142
+ ```
143
+
144
+ Press Ctrl-C to disconnect.
145
+
146
+ You can also apply the filtering options below (for Jetstream, these are passed to the server to apply filtering server-side via `wantedCollections` / `wantedDids` parameters):
147
+
148
+
149
+ ### Filtering by DID
150
+
151
+ To print only events from given DID(s):
152
+
153
+ ```
154
+ rat stream bsky.network -d did:plc:abcd1234,did:plc:xyz9999,...
155
+ ```
156
+
157
+ You can either pass a comma-separated list as one parameter, or repeat `-d val` more than once.
158
+
159
+
160
+ ### Filtering by collection
161
+
162
+ To print only records from given collection(s):
163
+
164
+ ```
165
+ rat stream bsky.network -c app.bsky.feed.post -c app.bsky.actor.profile
166
+ ```
26
167
 
27
- ## Development
28
168
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
169
+ ## Credits
30
170
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
171
+ Copyright Β© 2026 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)).
32
172
 
33
- ## Contributing
173
+ The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
34
174
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ratproto.
175
+ Bug reports and pull requests are welcome 😎
data/exe/rat CHANGED
@@ -1,59 +1,39 @@
1
1
  #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # rat – Ruby ATProto Tool
5
- #
6
- # Simple CLI for Bluesky AT Protocol using:
7
- # - Minisky (XRPC client)
8
- # - Skyfall (firehose / Jetstream streaming)
9
- # - DIDKit (DID / handle resolution)
10
- #
11
- # Usage:
12
- # rat fetch at://<did-or-handle>/<collection-nsid>/<rkey>
13
- # rat stream <relay.host> [-j|--jetstream] [-r|--cursor CURSOR] \
14
- # [-d|--did DID,...] [-c|--collection NSID,...]
15
- # rat resolve <did-or-handle>
16
- # rat help | --help
17
- # rat version | --version
18
2
 
19
- require 'optparse'
20
- require 'json'
21
- require 'time'
22
- require 'uri'
3
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
23
4
 
5
+ require 'didkit'
6
+ require 'json'
24
7
  require 'minisky'
8
+ require 'optparse'
25
9
  require 'skyfall'
26
- require 'didkit' # defines DIDKit::DID and top-level DID alias
10
+ require 'time'
11
+ require 'uri'
27
12
 
28
- VERSION = '0.0.1'
13
+ require 'ratproto/version'
29
14
 
30
- DID_REGEX = /\Adid:[a-z0-9]+:[a-zA-Z0-9.\-_:]+\z/
31
- DOMAIN_REGEX = /\A[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+\z/i
32
- HOST_PORT_REGEX = /\A#{DOMAIN_REGEX.source}(?::\d+)?\z/i
15
+ DID_REGEXP = /\Adid:[a-z]+:[a-zA-Z0-9.\-_]+\z/
16
+ NSID_REGEXP = /\A[a-z0-9]+(\.[a-z0-9]+)+\z/
33
17
 
34
- def global_help
35
- <<~HELP
36
- rat #{VERSION}
18
+ def print_help
19
+ puts <<~HELP
20
+ rat #{RatProto::VERSION} πŸ€
37
21
 
38
22
  Usage:
39
- rat fetch at://<did-or-handle>/<collection-nsid>/<rkey>
40
- rat stream <relay.host> [options]
41
- rat resolve <did-or-handle>
42
- rat help | --help
43
- rat version | --version
23
+ rat fetch at://uri
24
+ rat stream <firehose-host> [options]
25
+ rat resolve <did>|<handle>
44
26
 
45
27
  Commands:
46
28
  fetch Fetch a single record given its at:// URI
47
- stream Stream commit events from a relay / PDS firehose
48
- resolve Resolve a DID or @handle using DIDKit
29
+ stream Stream events from a relay / PDS firehose
30
+ resolve Resolve a DID or @handle
49
31
  help Show this help
50
32
  version Show version
51
33
 
52
34
  Stream options:
53
- -j, --jetstream Use Skyfall::Jetstream (JSON)
54
- (uses Jetstream wanted_dids / wanted_collections
55
- when -d/-c are provided)
56
- -r, --cursor CURSOR Start from cursor (seq or time_us)
35
+ -j, --jetstream Use a Jetstream source instead of a CBOR firehose
36
+ -r, --cursor CURSOR Start from a given cursor
57
37
  -d, --did DID[,DID...] Filter only events from given DID(s)
58
38
  (can be passed multiple times)
59
39
  -c, --collection NSID[,NSID...] Filter only events of given collection(s)
@@ -61,313 +41,199 @@ def global_help
61
41
  HELP
62
42
  end
63
43
 
64
- def abort_with_help(message = nil)
65
- warn "Error: #{message}" if message
66
- warn
67
- warn global_help
44
+ def abort_with_error(message)
45
+ puts message
68
46
  exit 1
69
47
  end
70
48
 
71
- def valid_did?(s)
72
- DID_REGEX.match?(s)
73
- end
74
-
75
- def valid_handle?(s)
76
- DOMAIN_REGEX.match?(s)
77
- end
78
-
79
- def valid_nsid?(s)
80
- # NSIDs look like domain names (reverse DNS), so reuse DOMAIN_REGEX
81
- DOMAIN_REGEX.match?(s)
82
- end
83
-
84
- def validate_handle!(s, context: 'handle')
85
- unless valid_handle?(s)
86
- abort_with_help("#{s.inspect} doesn't look like a valid #{context}")
49
+ def validate_did(did)
50
+ unless did =~ DID_REGEXP
51
+ abort_with_error "Error: #{did.inspect} is not a valid DID"
87
52
  end
88
- s
89
53
  end
90
54
 
91
- def validate_did!(s)
92
- unless valid_did?(s)
93
- abort_with_help("#{s.inspect} doesn't look like a valid DID")
55
+ def validate_nsid(collection)
56
+ unless collection =~ NSID_REGEXP
57
+ abort_with_error "Error: #{collection.inspect} is not a valid collection NSID"
94
58
  end
95
- s
96
59
  end
97
60
 
98
- def validate_nsid!(nsid)
99
- unless valid_nsid?(nsid)
100
- abort_with_help("#{nsid.inspect} doesn't look like a valid collection NSID")
101
- end
102
- nsid
103
- end
104
-
105
- def parse_at_uri(str)
106
- unless str.start_with?('at://')
107
- abort_with_help("not an at:// URI: #{str.inspect}")
61
+ def parse_at_uri(uri)
62
+ unless uri.start_with?('at://')
63
+ abort_with_error "Error: not an at:// URI: #{uri.inspect}"
108
64
  end
109
65
 
110
- m = /\Aat:\/\/([^\/]+)\/([^\/]+)\/([^\/]+)\z/.match(str)
111
- unless m
112
- abort_with_help("invalid at:// URI: expected at://<repo>/<collection>/<rkey>")
66
+ unless uri =~ /\Aat:\/\/([^\/]+)\/([^\/]+)\/([^\/]+)\z/
67
+ abort_with_error "Error: invalid at:// URI: #{uri.inspect}"
113
68
  end
114
69
 
115
- repo, collection, rkey = m.captures
116
- [repo, collection, rkey]
70
+ [$1, $2, $3]
117
71
  end
118
72
 
119
- def resolve_repo_to_did_and_pds(repo_str)
120
- if repo_str.start_with?('did:')
121
- validate_did!(repo_str)
122
- did_obj = DID.new(repo_str)
123
- else
124
- handle = repo_str.sub(/\A@/, '')
125
- validate_handle!(handle, context: 'handle in at:// URI')
126
- did_obj = DID.resolve_handle(handle)
127
- end
73
+ def run_fetch(args)
74
+ uri = args.shift
128
75
 
129
- doc = did_obj.document
130
- pds_host = doc.pds_host
131
- unless pds_host && !pds_host.empty?
132
- abort_with_help("DID document for #{did_obj} does not contain a pds_host")
76
+ if uri.nil?
77
+ abort_with_error "Usage: #{$PROGRAM_NAME} fetch <at://uri>"
133
78
  end
134
79
 
135
- [did_obj.to_s, pds_host]
136
- rescue StandardError => e
137
- warn "Error resolving repo #{repo_str.inspect}: #{e.class}: #{e.message}"
138
- exit 1
139
- end
140
-
141
- def do_fetch(argv)
142
- uri = argv.shift or abort_with_help("fetch requires an at:// URI")
143
- unless argv.empty?
144
- abort_with_help("unexpected extra arguments for fetch: #{argv.join(' ')}")
80
+ if !args.empty?
81
+ abort_with_error "Error: Unexpected arguments for fetch: #{args.join(' ')}"
145
82
  end
146
83
 
147
- repo_str, collection, rkey = parse_at_uri(uri)
148
-
149
- # basic validation of collection (NSID-ish)
150
- validate_nsid!(collection)
151
-
152
- did, pds_host = resolve_repo_to_did_and_pds(repo_str)
153
-
154
- client = Minisky.new(pds_host, nil)
155
- params = { repo: did, collection: collection, rkey: rkey }
84
+ repo, collection, rkey = parse_at_uri(uri)
156
85
 
157
86
  begin
158
- res = client.get_request('com.atproto.repo.getRecord', params)
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
+ })
159
93
  rescue StandardError => e
160
- warn "Error calling com.atproto.repo.getRecord: #{e.class}: #{e.message}"
161
- exit 1
94
+ abort_with_error "Error loading record: #{e.class}: #{e.message}"
162
95
  end
163
96
 
164
- value = res['value']
165
- if value.nil?
166
- warn "Warning: response did not contain a 'value' field"
167
- puts JSON.pretty_generate(res)
168
- else
169
- puts JSON.pretty_generate(value)
170
- end
97
+ puts JSON.pretty_generate(response['value'])
171
98
  end
172
99
 
173
- def do_resolve(argv)
174
- target = argv.shift or abort_with_help("resolve requires a DID or handle")
175
- unless argv.empty?
176
- abort_with_help("unexpected extra arguments for resolve: #{argv.join(' ')}")
100
+ def run_resolve(args)
101
+ target = args.shift
102
+
103
+ if target.nil?
104
+ abort_with_error "Usage: #{$PROGRAM_NAME} resolve <did>|<handle>"
177
105
  end
178
106
 
179
- if target.start_with?('did:')
180
- validate_did!(target)
181
- begin
182
- did_obj = DID.new(target)
183
- doc = did_obj.document
184
- json = doc.respond_to?(:json) ? doc.json : doc
185
- puts did_obj.to_s
186
- puts JSON.pretty_generate(json)
187
- rescue StandardError => e
188
- warn "Error resolving DID #{target.inspect}: #{e.class}: #{e.message}"
189
- exit 1
190
- end
191
- else
192
- handle = target.sub(/\A@/, '')
193
- validate_handle!(handle)
194
- begin
195
- did_obj = DID.resolve_handle(handle)
196
- doc = did_obj.document
197
- json = doc.respond_to?(:json) ? doc.json : doc
198
- puts did_obj.to_s
199
- puts JSON.pretty_generate(json)
200
- rescue StandardError => e
201
- warn "Error resolving handle #{target.inspect}: #{e.class}: #{e.message}"
202
- exit 1
203
- end
107
+ if !args.empty?
108
+ abort_with_error "Error: Unexpected arguments for resolve: #{args.join(' ')}"
204
109
  end
205
- end
206
110
 
207
- def validate_relay_host!(host)
208
- # Accept:
209
- # - bare hostname or hostname:port
210
- # - ws://host[:port]
211
- # - wss://host[:port]
212
- if host =~ /\Aws:\/\//i || host =~ /\Awss:\/\//i
213
- uri = URI(host)
214
- if uri.path && uri.path != '' && uri.path != '/'
215
- abort_with_help("relay URL must not contain a path: #{host.inspect}")
216
- end
217
- unless uri.host && DOMAIN_REGEX.match?(uri.host)
218
- abort_with_help("invalid relay hostname in URL: #{host.inspect}")
219
- end
220
- else
221
- unless HOST_PORT_REGEX.match?(host)
222
- abort_with_help("invalid relay hostname: #{host.inspect}")
223
- end
111
+ did = DID.resolve_handle(target)
112
+
113
+ if did.nil?
114
+ abort_with_error "Couldn't resolve #{target}"
224
115
  end
225
116
 
226
- host
227
- rescue URI::InvalidURIError
228
- abort_with_help("invalid relay URL: #{host.inspect}")
117
+ puts JSON.pretty_generate(did.document.json)
118
+ rescue StandardError => e
119
+ abort_with_error "Error resolving #{target.inspect}: #{e.class}: #{e.message}"
229
120
  end
230
121
 
231
- def parse_stream_options(argv)
232
- options = {
233
- use_jetstream: false,
234
- cursor: nil,
235
- dids: [],
236
- collections: []
237
- }
122
+ def parse_stream_options(args)
123
+ options = {}
238
124
 
239
125
  parser = OptionParser.new do |opts|
240
- opts.banner = "Usage: rat stream <relay.host> [options]"
126
+ opts.banner = "Usage: #{$PROGRAM_NAME} stream <relay.host> [options]"
241
127
 
242
- opts.on('-j', '--jetstream', 'Use Skyfall::Jetstream (JSON)') do
243
- options[:use_jetstream] = true
128
+ opts.on('-j', '--jetstream', 'Use a Jetstream source') do
129
+ options[:jetstream] = true
244
130
  end
245
131
 
246
- opts.on('-rCURSOR', '--cursor=CURSOR', 'Start from cursor (seq or time_us)') do |cursor|
132
+ opts.on('-rCURSOR', '--cursor=CURSOR', 'Start from a given cursor') do |cursor|
247
133
  options[:cursor] = cursor
248
134
  end
249
135
 
250
- opts.on('-dLIST', '--did=LIST',
251
- 'Filter only events from DID(s) (comma-separated or repeated)') do |list|
136
+ opts.on('-dLIST', '--did=LIST', 'Filter only events from given DID(s) (comma-separated or repeated)') do |list|
252
137
  items = list.split(',').map(&:strip).reject(&:empty?)
138
+
253
139
  if items.empty?
254
- abort_with_help("empty argument to -d/--did")
140
+ abort_with_error "Error: empty argument to -d/--did"
255
141
  end
256
- options[:dids].concat(items)
142
+
143
+ options[:dids] ||= []
144
+ options[:dids] += items
257
145
  end
258
146
 
259
- opts.on('-cLIST', '--collection=LIST',
260
- 'Filter only events of given collection NSID(s)') do |list|
147
+ opts.on('-cLIST', '--collection=LIST', 'Filter only events of given collections') do |list|
261
148
  items = list.split(',').map(&:strip).reject(&:empty?)
149
+
262
150
  if items.empty?
263
- abort_with_help("empty argument to -c/--collection")
151
+ abort_with_error "Error: empty argument to -c/--collection"
264
152
  end
265
- options[:collections].concat(items)
153
+
154
+ options[:collections] ||= []
155
+ options[:collections] += items
266
156
  end
267
157
 
268
158
  opts.on('-h', '--help', 'Show stream-specific help') do
269
159
  puts opts
270
- exit 0
160
+ exit
271
161
  end
272
162
  end
273
163
 
274
164
  remaining = []
165
+
275
166
  begin
276
- parser.order!(argv) { |nonopt| remaining << nonopt }
167
+ parser.order!(args) { |other| remaining << other }
277
168
  rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
278
- warn "Error: #{e.message}"
279
- warn parser
169
+ puts "Error: #{e.message}"
170
+ puts parser
280
171
  exit 1
281
172
  end
282
173
 
283
174
  [options, remaining]
284
175
  end
285
176
 
286
- def do_stream(argv)
287
- options, remaining = parse_stream_options(argv)
177
+ def run_stream(args)
178
+ options, arguments = parse_stream_options(args)
288
179
 
289
- host = remaining.shift or abort_with_help("stream requires a relay hostname")
290
- validate_relay_host!(host)
180
+ service = arguments.shift
291
181
 
292
- unless remaining.empty?
293
- abort_with_help("unexpected extra arguments for stream: #{remaining.join(' ')}")
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(' ')}"
294
188
  end
295
189
 
296
- # validate cursor (if given)
297
190
  if options[:cursor] && options[:cursor] !~ /\A\d+\z/
298
- abort_with_help("cursor must be a decimal integer, got #{options[:cursor].inspect}")
191
+ abort_with_error "Error: cursor must be a decimal integer, got: #{options[:cursor].inspect}"
299
192
  end
300
193
 
301
- # validate DIDs
302
- options[:dids].each do |did|
303
- validate_did!(did)
194
+ if options[:dids]
195
+ options[:dids].each { |did| validate_did(did) }
304
196
  end
305
197
 
306
- # validate collections
307
- options[:collections].each do |nsid|
308
- validate_nsid!(nsid)
198
+ if options[:collections]
199
+ options[:collections].each { |collection| validate_nsid(collection) }
309
200
  end
310
201
 
311
- # Build Skyfall client
312
- if options[:use_jetstream]
202
+ if options[:jetstream]
313
203
  jet_opts = {}
314
204
  jet_opts[:cursor] = options[:cursor].to_i if options[:cursor]
315
205
 
316
- # Pass filters through to Jetstream server-side
317
- jet_opts[:wanted_dids] = options[:dids] unless options[:dids].empty?
318
- jet_opts[:wanted_collections] = options[:collections] unless options[:collections].empty?
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]
319
209
 
320
- sky = Skyfall::Jetstream.new(host, jet_opts)
210
+ sky = Skyfall::Jetstream.new(service, jet_opts)
321
211
  else
322
212
  cursor = options[:cursor]&.to_i
323
- sky = Skyfall::Firehose.new(host, :subscribe_repos, cursor)
213
+ sky = Skyfall::Firehose.new(service, :subscribe_repos, cursor)
324
214
  end
325
215
 
326
- # Lifecycle logging
327
216
  sky.on_connecting { |url| puts "Connecting to #{url}..." }
328
217
  sky.on_connect { puts "Connected" }
329
218
  sky.on_disconnect { puts "Disconnected" }
330
219
  sky.on_reconnect { puts "Connection lost, trying to reconnect..." }
331
- sky.on_timeout { puts "Connection stalled, triggering a reconnect..." } if sky.respond_to?(:on_timeout)
332
- sky.on_error { |e| warn "ERROR: #{e}" }
220
+ sky.on_timeout { puts "Connection stalled, triggering a reconnect..." }
221
+ sky.on_error { |e| puts "ERROR: #{e}" }
333
222
 
334
- # Message handler
335
223
  sky.on_message do |msg|
336
224
  next unless msg.type == :commit
225
+ next if options[:dids] && !options[:dids].include?(msg.repo)
337
226
 
338
- did = if msg.respond_to?(:repo) && msg.repo
339
- msg.repo
340
- elsif msg.respond_to?(:did)
341
- msg.did
342
- else
343
- nil
344
- end
345
-
346
- if !options[:dids].empty? && (!did || !options[:dids].include?(did))
347
- next
348
- end
349
-
350
- ts = msg.time ? msg.time.getlocal.iso8601 : 'unknown-time'
227
+ time = msg.time.getlocal.iso8601
351
228
 
352
229
  msg.operations.each do |op|
353
- # collection filter (still applied client-side, in case server doesn't)
354
- if !options[:collections].empty? &&
355
- !options[:collections].include?(op.collection)
356
- next
357
- end
358
-
359
- line = "[#{ts}] #{did || '-'} :#{op.action} #{op.collection} #{op.rkey}"
230
+ next if options[:collections] && !options[:collections].include?(op.collection)
360
231
 
361
- if op.respond_to?(:raw_record) && op.raw_record && !op.raw_record.empty?
362
- # compact JSON on one line
363
- line << " " << JSON.generate(op.raw_record)
364
- end
365
-
366
- puts line
232
+ json = op.raw_record && JSON.generate(op.raw_record)
233
+ puts "[#{time}] #{msg.repo} #{op.action.inspect} #{op.collection} #{op.rkey} #{json}"
367
234
  end
368
235
  end
369
236
 
370
- # Clean disconnect on Ctrl+C
371
237
  trap('SIGINT') do
372
238
  puts 'Disconnecting...'
373
239
  sky.disconnect
@@ -376,28 +242,24 @@ def do_stream(argv)
376
242
  sky.connect
377
243
  end
378
244
 
379
- # ---- main dispatcher ----
380
-
381
245
  if ARGV.empty?
382
- puts global_help
383
- exit 0
246
+ print_help
247
+ exit
384
248
  end
385
249
 
386
250
  cmd = ARGV.shift
387
251
 
388
252
  case cmd
389
253
  when 'help', '--help', '-h'
390
- puts global_help
391
- exit 0
254
+ print_help
392
255
  when 'version', '--version'
393
- puts VERSION
394
- exit 0
256
+ puts "RatProto #{RatProto::VERSION} πŸ€"
395
257
  when 'fetch'
396
- do_fetch(ARGV)
258
+ run_fetch(ARGV)
397
259
  when 'resolve'
398
- do_resolve(ARGV)
260
+ run_resolve(ARGV)
399
261
  when 'stream'
400
- do_stream(ARGV)
262
+ run_stream(ARGV)
401
263
  else
402
- abort_with_help("unknown command: #{cmd}")
264
+ abort_with_error "Error: unknown command: #{cmd}"
403
265
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RatProto
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.1"
5
5
  end
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.0.1
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuba Suder