ratproto 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ae75289d3bfe8a1adb0a27e3f33111b194756524fc6b040bb7131cd16fe0b6a
4
- data.tar.gz: 0a424e54c22bb32ffcfa5ee69f078b12705d0c19c3413a16867b9f4fc331d1a5
3
+ metadata.gz: dd51fed594f086af2d560047baf82a7adfe1f29483421d8dd1c4e0ffef8e00f4
4
+ data.tar.gz: b91f3eaac638d75e04d3030fb9cf59f70e6d8589f223fe0c1a846837ffa1336e
5
5
  SHA512:
6
- metadata.gz: 9f1e9a749a2d46ba339a194f5331569b89d05480f419b84a208062b39306893bd2bc88139f144426330c234ff5cdc63303a728ce3d47228bd664e8e59d7ec600
7
- data.tar.gz: '08d7134fd2647848dfd1df605c05b8b2b03893d0be2b2ffd0d55da50d36fd6168679b292765283231693fbeba80883f26a5464d6098a9f49cdd9853a5ef1c770'
6
+ metadata.gz: 5cb31e38a0d44e863da64ccbd826258d48c8ae6451e63279e577213fe482ed4122c084a338264c8dcb118369a3fd8e571678a02bf070537368b89b5d4011b91e
7
+ data.tar.gz: 85cd4e59791b9f09a987dbe1ac2e8ebfe2d4d991b7033d851d7d26e8b56f21b19816fb6fb763c9272491378d98c2e3fe7e4acdb0a7925e17a15ac31eba8efa59
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.3.0] - 2026-07-30
2
+
3
+ * `stream`:
4
+ - added option `--print-cursor` (`-s`)
5
+ - print account status change messages
6
+ - added option `--[no-]account` (`-a`) to enable/disable account messages
7
+ - print the timestamp of the last message on stop
8
+ - allow passing handles with `@` to `-d` option
9
+ - status messages are printed to stderr
10
+ - suggest that `-j` might be missing on connection error
11
+ * added `stream-labels` command which works similarly to `stream`, but streams labels from a labeller
12
+ - pass labeller service as a hostname or a DID/handle of an account
13
+ - filter labels by type (value) or by pattern in the labelled URI
14
+
1
15
  ## [0.2.0] - 2026-07-28
2
16
 
3
17
  - converted the codebase from a single script in `exe/rat` to per-command classes using Clamp framework
data/lib/ratproto/cli.rb CHANGED
@@ -7,6 +7,7 @@ require_relative 'commands/base'
7
7
  require_relative 'commands/fetch'
8
8
  require_relative 'commands/resolve'
9
9
  require_relative 'commands/stream'
10
+ require_relative 'commands/stream_labels'
10
11
 
11
12
  Clamp.allow_options_after_parameters = true
12
13
 
@@ -50,6 +51,7 @@ module RatProto
50
51
 
51
52
  subcommand 'fetch', 'fetch a single record given its at:// URI', Commands::Fetch
52
53
  subcommand 'stream', 'stream events from a relay or PDS firehose', Commands::Stream
54
+ subcommand 'stream-labels', 'stream labels a labeller firehose', Commands::StreamLabels
53
55
  subcommand 'resolve', 'resolve a DID or @handle', Commands::Resolve
54
56
  subcommand 'help', 'show help', Commands::Help
55
57
  subcommand 'version', 'show version', Commands::Version
@@ -11,6 +11,10 @@ module RatProto
11
11
 
12
12
  private
13
13
 
14
+ def log(text = nil)
15
+ $stderr.puts text
16
+ end
17
+
14
18
  def parse_at_uri(value)
15
19
  signal_usage_error "not an at:// URI: #{value.inspect}" unless value.start_with?('at://')
16
20
 
@@ -33,7 +37,7 @@ module RatProto
33
37
  def map_argument_to_did(did)
34
38
  if did =~ DID_REGEXP
35
39
  did
36
- elsif did =~ HANDLE_REGEXP
40
+ elsif did.gsub(/\A@/, '') =~ HANDLE_REGEXP
37
41
  if resolved_did = DID.resolve_handle(did)
38
42
  resolved_did.to_s
39
43
  else
@@ -15,9 +15,11 @@ module RatProto
15
15
 
16
16
  option ['-j', '--jetstream'], :flag, 'use a Jetstream source'
17
17
 
18
- option ['-r', '--cursor'], 'CURSOR', 'start from a given cursor' do |value|
18
+ option(
19
+ ['-r', '--cursor'], 'CURSOR',
20
+ 'start from a given cursor (pass 0 to read from the beginning of the playback window)'
21
+ ) do |value|
19
22
  raise ArgumentError, 'must be a decimal integer' unless value =~ /\A\d+\z/
20
-
21
23
  value.to_i
22
24
  end
23
25
 
@@ -27,35 +29,48 @@ module RatProto
27
29
  option ['-c', '--collection'], 'NSID[,NSID...]', 'filter events by collection (comma-separated or repeated)',
28
30
  multivalued: true, attribute_name: :collection_values
29
31
 
32
+ option ['-a', '--[no-]account'], :flag, 'print account status events (default: enabled unless filtering by collection)'
33
+ option ['-s', '--print-cursor'], :flag, 'only print current cursor (seq)'
34
+
35
+
30
36
  def execute
31
37
  require 'skyfall'
32
38
 
33
- dids = comma_separated(did_values, '-d/--did')
34
- collections = comma_separated(collection_values, '-c/--collection')
39
+ @dids = comma_separated(did_values, '-d/--did')
40
+ @collections = comma_separated(collection_values, '-c/--collection')
41
+
42
+ @dids = (@dids.length > 0) ? @dids.map { |did| map_argument_to_did(did) } : nil
43
+ @collections = (@collections.length > 0) ? @collections.map { |collection| validate_nsid(collection) } : nil
35
44
 
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
45
+ @sky = build_stream
46
+ configure_interrupt_handler
38
47
 
39
- sky = build_stream(dids, collections)
40
- configure_callbacks(sky, dids, collections)
41
- configure_interrupt_handler(sky)
48
+ if print_cursor?
49
+ @sky.on_message do |msg|
50
+ print_cursor_and_stop(msg)
51
+ next
52
+ end
42
53
 
43
- sky.connect
54
+ @sky.connect
55
+ else
56
+ configure_callbacks
57
+ @sky.connect
44
58
 
45
- puts "Stopped stream at cursor #{sky.cursor}"
59
+ log "Stopped stream at cursor #{@sky.cursor}" + (@last_timestamp ? " (#{@last_timestamp})" : "")
60
+ end
46
61
  end
47
62
 
48
63
 
49
64
  private
50
65
 
51
- def build_stream(dids, collections)
66
+ def build_stream
52
67
  if jetstream?
53
68
  options = {}
54
69
  options[:cursor] = cursor if cursor
55
70
 
56
71
  # 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
72
+ options[:wanted_dids] = @dids if @dids
73
+ options[:wanted_collections] = @collections if @collections
59
74
 
60
75
  Skyfall::Jetstream.new(service, options)
61
76
  else
@@ -63,36 +78,84 @@ module RatProto
63
78
  end
64
79
  end
65
80
 
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}" }
81
+ def configure_callbacks
82
+ @sky.on_connecting { |url| log "Connecting to #{url}..." }
83
+ @sky.on_connect { log "Connected" }
84
+ @sky.on_disconnect { log "Disconnected" }
85
+ @sky.on_reconnect { log "Connection lost, trying to reconnect..." }
86
+ @sky.on_timeout { log "Connection stalled, triggering a reconnect..." }
87
+ @sky.on_error { |e| report_error(e) }
73
88
 
74
- sky.on_message do |msg|
75
- next unless msg.type == :commit
76
- next if dids && !dids.include?(msg.repo)
89
+ @sky.on_message { |msg| process_message(msg) }
90
+ end
77
91
 
78
- time = msg.time.getlocal.iso8601
92
+ def report_error(error)
93
+ log "ERROR: #{error.message}"
79
94
 
80
- msg.operations.each do |op|
81
- next if collections && !collections.include?(op.collection)
95
+ if !jetstream? && error.message.include?('Unexpected response code')
96
+ log "If this is a Jetstream server, retry with -j / --jetstream."
97
+ end
98
+ end
82
99
 
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
100
+ def process_message(msg)
101
+ case msg.type
102
+ when :commit
103
+ print_commit_message(msg)
104
+ when :account
105
+ print_account_message(msg) if show_account_messages?
86
106
  end
87
107
  end
88
108
 
89
- def configure_interrupt_handler(sky)
109
+ def print_commit_message(msg)
110
+ return if @dids && !@dids.include?(msg.repo)
111
+
112
+ time = msg.time.getlocal.iso8601
113
+ @last_timestamp = time
114
+
115
+ msg.operations.each do |op|
116
+ next if @collections && !@collections.include?(op.collection)
117
+
118
+ json = op.raw_record && JSON.generate(op.raw_record.except('$type'))
119
+ puts "[#{time}] (#{msg.seq}) #{msg.repo} #{op.action.inspect} #{op.collection} #{op.rkey} #{json}"
120
+ end
121
+ end
122
+
123
+ def print_account_message(msg)
124
+ return if @dids && !@dids.include?(msg.repo)
125
+
126
+ time = msg.time.getlocal.iso8601
127
+ @last_timestamp = time
128
+
129
+ status = (msg.active?) ? :active : msg.status
130
+
131
+ puts "[#{time}] (#{msg.seq}) #{msg.repo} #account (status = #{status.inspect})"
132
+ end
133
+
134
+ def show_account_messages?
135
+ if account? == nil
136
+ # enabled by default if -c is not used
137
+ @collections.nil?
138
+ else
139
+ # if enabled/disabled explicitly, follow user's choice
140
+ account?
141
+ end
142
+ end
143
+
144
+ def configure_interrupt_handler
90
145
  trap('SIGINT') do
91
- puts
92
- puts 'Disconnecting...'
93
- sky.disconnect
146
+ log
147
+ log 'Disconnecting...'
148
+ @sky.disconnect
94
149
  end
95
150
  end
151
+
152
+ def print_cursor_and_stop(msg)
153
+ return if @disconnecting
154
+
155
+ puts msg.seq
156
+ @sky.disconnect
157
+ @disconnecting = true
158
+ end
96
159
  end
97
160
  end
98
161
  end
@@ -0,0 +1,127 @@
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 StreamLabels < Base
12
+ banner 'Stream labels from a labeller firehose'
13
+
14
+ parameter 'SERVICE', 'labeller hostname, or a @handle/DID of the labeller account (handle must start with @)'
15
+
16
+ option ['-r', '--cursor'], 'CURSOR', 'start from a given cursor' do |value|
17
+ raise ArgumentError, 'must be a decimal integer' unless value =~ /\A\d+\z/
18
+ value.to_i
19
+ end
20
+
21
+ option ['-l', '--labels'], 'LABEL[,LABEL...]', 'filter events by label type (comma-separated or repeated)',
22
+ multivalued: true, attribute_name: :label_types
23
+
24
+ option ['-t', '--target'], 'PATTERN', "filter by labelled DID/URI (can be a pattern with '*' as a wildcard; repeatable)",
25
+ multivalued: true, attribute_name: :target_patterns
26
+
27
+ option ['-s', '--print-cursor'], :flag, 'only print current cursor (seq)'
28
+
29
+
30
+ def execute
31
+ require 'skyfall'
32
+
33
+ @labels = (label_types.length > 0) ? comma_separated(label_types, '-l/--labels') : nil
34
+ @targets = (target_patterns.length > 0) ? target_patterns.map { |pattern| target_regexp(pattern) } : nil
35
+ @service = determine_hostname(service)
36
+
37
+ @sky = Skyfall::Firehose.new(@service, :subscribe_labels, cursor)
38
+ configure_interrupt_handler
39
+
40
+ if print_cursor?
41
+ @sky.on_message do |msg|
42
+ print_cursor_and_stop(msg)
43
+ next
44
+ end
45
+
46
+ @sky.connect
47
+ else
48
+ configure_callbacks
49
+ @sky.connect
50
+
51
+ log "Stopped stream at cursor #{@sky.cursor}" + (@last_timestamp ? " (#{@last_timestamp})" : "")
52
+ end
53
+ end
54
+
55
+
56
+ private
57
+
58
+ def target_regexp(pattern)
59
+ source = Regexp.escape(pattern).gsub('\*', '.*')
60
+ Regexp.new("\\A#{source}\\z")
61
+ end
62
+
63
+ def determine_hostname(service)
64
+ if service =~ /\A(https?:\/\/)?[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)+(:[0-9]+)?\z/
65
+ return service
66
+ end
67
+
68
+ if service =~ DID_REGEXP
69
+ did = DID.new(service)
70
+ else
71
+ handle = service.gsub(/\A@/, '')
72
+
73
+ if handle =~ HANDLE_REGEXP
74
+ if resolved_did = DID.resolve_handle(handle)
75
+ did = resolved_did
76
+ else
77
+ signal_error "couldn't resolve handle @#{handle}"
78
+ end
79
+ else
80
+ signal_usage_error "#{service.inspect} is not a valid hostname, DID or handle"
81
+ end
82
+ end
83
+
84
+ did.document.labeller_host
85
+ end
86
+
87
+ def configure_callbacks
88
+ @sky.on_connecting { |url| log "Connecting to #{url}..." }
89
+ @sky.on_connect { log "Connected" }
90
+ @sky.on_disconnect { log "Disconnected" }
91
+ @sky.on_reconnect { log "Connection lost, trying to reconnect..." }
92
+ @sky.on_timeout { log "Connection stalled, triggering a reconnect..." }
93
+ @sky.on_error { |e| log "ERROR: #{e}" }
94
+
95
+ @sky.on_message do |msg|
96
+ next unless msg.type == :labels
97
+
98
+ msg.labels.each do |label|
99
+ next if @labels && !@labels.include?(label.value)
100
+ next if @targets && @targets.none? { |pattern| pattern.match?(label.subject) }
101
+
102
+ @last_timestamp = label.created_at
103
+
104
+ json = JSON.generate(label.data.except('cid', 'cts', 'sig', 'src', 'ver'))
105
+ puts "[#{label.created_at}] (#{msg.seq}) #{json}"
106
+ end
107
+ end
108
+ end
109
+
110
+ def configure_interrupt_handler
111
+ trap('SIGINT') do
112
+ log
113
+ log 'Disconnecting...'
114
+ @sky.disconnect
115
+ end
116
+ end
117
+
118
+ def print_cursor_and_stop(msg)
119
+ return if @disconnecting
120
+
121
+ puts msg.seq
122
+ @sky.disconnect
123
+ @disconnecting = true
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RatProto
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuba Suder
@@ -106,6 +106,7 @@ files:
106
106
  - lib/ratproto/commands/fetch.rb
107
107
  - lib/ratproto/commands/resolve.rb
108
108
  - lib/ratproto/commands/stream.rb
109
+ - lib/ratproto/commands/stream_labels.rb
109
110
  - lib/ratproto/version.rb
110
111
  - sig/ratproto.rbs
111
112
  homepage: https://ruby.sdk.blue