skyfall 0.6.1 → 0.7.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 +53 -10
- data/README.md +1 -4
- data/lib/skyfall/car_archive.rb +42 -6
- data/lib/skyfall/cid.rb +2 -0
- data/lib/skyfall/collection.rb +20 -0
- data/lib/skyfall/errors.rb +50 -5
- data/lib/skyfall/events.rb +19 -0
- data/lib/skyfall/extensions.rb +4 -0
- data/lib/skyfall/firehose/account_message.rb +32 -4
- data/lib/skyfall/firehose/commit_message.rb +45 -5
- data/lib/skyfall/firehose/identity_message.rb +30 -2
- data/lib/skyfall/firehose/info_message.rb +35 -1
- data/lib/skyfall/firehose/labels_message.rb +28 -10
- data/lib/skyfall/firehose/message.rb +133 -24
- data/lib/skyfall/firehose/operation.rb +57 -5
- data/lib/skyfall/firehose/sync_message.rb +31 -0
- data/lib/skyfall/firehose/unknown_message.rb +8 -0
- data/lib/skyfall/firehose.rb +103 -5
- data/lib/skyfall/jetstream/account_message.rb +21 -1
- data/lib/skyfall/jetstream/commit_message.rb +35 -2
- data/lib/skyfall/jetstream/identity_message.rb +22 -1
- data/lib/skyfall/jetstream/message.rb +94 -7
- data/lib/skyfall/jetstream/operation.rb +56 -4
- data/lib/skyfall/jetstream/unknown_message.rb +8 -0
- data/lib/skyfall/jetstream.rb +94 -7
- data/lib/skyfall/label.rb +33 -0
- data/lib/skyfall/stream.rb +264 -47
- data/lib/skyfall/version.rb +1 -1
- metadata +2 -3
- data/lib/skyfall/firehose/handle_message.rb +0 -14
- data/lib/skyfall/firehose/tombstone_message.rb +0 -11
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative '../errors'
|
|
2
4
|
require_relative '../extensions'
|
|
3
5
|
require_relative '../firehose'
|
|
@@ -6,45 +8,88 @@ require 'cbor'
|
|
|
6
8
|
require 'time'
|
|
7
9
|
|
|
8
10
|
module Skyfall
|
|
11
|
+
|
|
12
|
+
# @abstract
|
|
13
|
+
# Abstract base class representing a CBOR firehose message.
|
|
14
|
+
#
|
|
15
|
+
# Actual messages are returned as instances of one of the subclasses of this class,
|
|
16
|
+
# depending on the type of message, most commonly as {Skyfall::Firehose::CommitMessage}.
|
|
17
|
+
#
|
|
18
|
+
# The {new} method is overridden here so that it can be called with a binary data message
|
|
19
|
+
# from the websocket, and it parses the type from the appropriate frame and builds an
|
|
20
|
+
# instance of a matching subclass.
|
|
21
|
+
#
|
|
22
|
+
# You normally don't need to call this class directly, unless you're building a custom
|
|
23
|
+
# subclass of {Skyfall::Stream}, or reading raw data packets from the websocket through
|
|
24
|
+
# the {Skyfall::Stream#on_raw_message} event handler.
|
|
25
|
+
|
|
9
26
|
class Firehose::Message
|
|
10
27
|
using Skyfall::Extensions
|
|
11
28
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
require_relative 'identity_message'
|
|
16
|
-
require_relative 'info_message'
|
|
17
|
-
require_relative 'labels_message'
|
|
18
|
-
require_relative 'sync_message'
|
|
19
|
-
require_relative 'tombstone_message'
|
|
20
|
-
require_relative 'unknown_message'
|
|
21
|
-
|
|
22
|
-
attr_reader :type, :did, :seq
|
|
23
|
-
alias repo did
|
|
29
|
+
# Type of the message (e.g. `:commit`, `:identity` etc.)
|
|
30
|
+
# @return [Symbol]
|
|
31
|
+
attr_reader :type
|
|
24
32
|
|
|
25
|
-
#
|
|
26
|
-
|
|
33
|
+
# DID of the account (repo) that the event is sent by.
|
|
34
|
+
# @return [String, nil]
|
|
35
|
+
attr_reader :did
|
|
27
36
|
|
|
37
|
+
# Sequential number of the message, to be used as a cursor when reconnecting.
|
|
38
|
+
# @return [Integer, nil]
|
|
39
|
+
attr_reader :seq
|
|
40
|
+
|
|
41
|
+
alias repo did
|
|
42
|
+
alias kind type
|
|
43
|
+
|
|
44
|
+
# First of the two CBOR objects forming the message payload, which mostly just includes the type field.
|
|
45
|
+
# @api private
|
|
46
|
+
# @return [Hash]
|
|
47
|
+
attr_reader :type_object
|
|
48
|
+
|
|
49
|
+
# Second of the two CBOR objects forming the message payload, which contains the rest of the data.
|
|
50
|
+
# @api private
|
|
51
|
+
# @return [Hash]
|
|
52
|
+
attr_reader :data_object
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Parses the CBOR objects from the binary data and returns an instance of an appropriate subclass.
|
|
56
|
+
#
|
|
57
|
+
# {Skyfall::Firehose::UnknownMessage} is returned if the message type is not recognized.
|
|
58
|
+
#
|
|
59
|
+
# @param data [String] binary payload of a firehose websocket message
|
|
60
|
+
# @return [Skyfall::Firehose::Message]
|
|
61
|
+
# @raise [Skyfall::DecodeError] if the structure of the message is invalid
|
|
62
|
+
# @raise [Skyfall::UnsupportedError] if the message has an unknown future version
|
|
63
|
+
# @raise [Skyfall::SubscriptionError] if the data contains an error message from the server
|
|
64
|
+
#
|
|
28
65
|
def self.new(data)
|
|
29
66
|
type_object, data_object = decode_cbor_objects(data)
|
|
30
67
|
|
|
31
68
|
message_class = case type_object['t']
|
|
32
69
|
when '#account' then Firehose::AccountMessage
|
|
33
70
|
when '#commit' then Firehose::CommitMessage
|
|
34
|
-
when '#handle' then Firehose::HandleMessage
|
|
35
71
|
when '#identity' then Firehose::IdentityMessage
|
|
36
72
|
when '#info' then Firehose::InfoMessage
|
|
37
73
|
when '#labels' then Firehose::LabelsMessage
|
|
38
74
|
when '#sync' then Firehose::SyncMessage
|
|
39
|
-
when '#tombstone' then Firehose::TombstoneMessage
|
|
40
75
|
else Firehose::UnknownMessage
|
|
41
76
|
end
|
|
42
77
|
|
|
78
|
+
if self != Firehose::Message && self != message_class
|
|
79
|
+
expected_type = self.name.split('::').last.gsub(/Message$/, '').downcase
|
|
80
|
+
raise DecodeError, "Expected ##{expected_type} message, got #{type_object['t']}"
|
|
81
|
+
end
|
|
82
|
+
|
|
43
83
|
message = message_class.allocate
|
|
44
84
|
message.send(:initialize, type_object, data_object)
|
|
45
85
|
message
|
|
46
86
|
end
|
|
47
87
|
|
|
88
|
+
#
|
|
89
|
+
# @private
|
|
90
|
+
# @param type_object [Hash] first decoded CBOR frame with metadata
|
|
91
|
+
# @param data_object [Hash] second decoded CBOR frame with payload
|
|
92
|
+
#
|
|
48
93
|
def initialize(type_object, data_object)
|
|
49
94
|
@type_object = type_object
|
|
50
95
|
@data_object = data_object
|
|
@@ -54,29 +99,79 @@ module Skyfall
|
|
|
54
99
|
@seq = @data_object['seq']
|
|
55
100
|
end
|
|
56
101
|
|
|
102
|
+
#
|
|
103
|
+
# List of operations on records included in the message. Only `#commit` messages include
|
|
104
|
+
# operations, but for convenience the method is declared here and returns an empty array
|
|
105
|
+
# in other messages.
|
|
106
|
+
# @return [Array<Firehose::Operation>]
|
|
107
|
+
#
|
|
57
108
|
def operations
|
|
58
109
|
[]
|
|
59
110
|
end
|
|
60
111
|
|
|
112
|
+
#
|
|
113
|
+
# @return [Boolean] true if the message is {Firehose::UnknownMessage} (of unrecognized type)
|
|
114
|
+
#
|
|
61
115
|
def unknown?
|
|
62
116
|
self.is_a?(Firehose::UnknownMessage)
|
|
63
117
|
end
|
|
64
118
|
|
|
119
|
+
#
|
|
120
|
+
# Timestamp decoded from the message.
|
|
121
|
+
#
|
|
122
|
+
# Note: this represents the time when the message was emitted from the original PDS, which
|
|
123
|
+
# might differ a lot from the `created_at` time saved in the record data, e.g. if user's local
|
|
124
|
+
# time is set incorrectly, or if an archive of existing posts was imported from another platform.
|
|
125
|
+
#
|
|
126
|
+
# @return [Time, nil]
|
|
127
|
+
#
|
|
65
128
|
def time
|
|
66
|
-
@time ||= @data_object['time'] && Time.
|
|
129
|
+
@time ||= @data_object['time'] && Time.iso8601(@data_object['time'])
|
|
67
130
|
end
|
|
68
131
|
|
|
69
|
-
|
|
70
|
-
|
|
132
|
+
# Much faster version for Ruby 3.2+
|
|
133
|
+
|
|
134
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
|
|
135
|
+
def time
|
|
136
|
+
@time ||= @data_object['time'] && Time.new(@data_object['time'])
|
|
137
|
+
end
|
|
71
138
|
end
|
|
72
139
|
|
|
140
|
+
# Returns a string with a representation of the object for debugging purposes.
|
|
141
|
+
# @return [String]
|
|
73
142
|
def inspect
|
|
74
143
|
vars = inspectable_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
|
|
75
144
|
"#<#{self.class}:0x#{object_id} #{vars}>"
|
|
76
145
|
end
|
|
77
146
|
|
|
147
|
+
|
|
148
|
+
protected
|
|
149
|
+
|
|
150
|
+
# @return [Array<Symbol>] list of instance variables to be printed in the {#inspect} output
|
|
151
|
+
def inspectable_variables
|
|
152
|
+
instance_variables - [:@type_object, :@data_object, :@blocks]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
|
|
78
156
|
private
|
|
79
157
|
|
|
158
|
+
# Note: this method is written this way as an optimization
|
|
159
|
+
def check_if_not_nil(a, b = nil, c = nil, d = nil, e = nil, f = nil, g = nil)
|
|
160
|
+
ok = @data_object.has_key?(a)
|
|
161
|
+
ok &&= @data_object.has_key?(b) if b
|
|
162
|
+
ok &&= @data_object.has_key?(c) if c
|
|
163
|
+
ok &&= @data_object.has_key?(d) if d
|
|
164
|
+
ok &&= @data_object.has_key?(e) if e
|
|
165
|
+
ok &&= @data_object.has_key?(f) if f
|
|
166
|
+
ok &&= @data_object.has_key?(g) if g
|
|
167
|
+
|
|
168
|
+
if !ok
|
|
169
|
+
expected_fields = [a, b, c, d, e, f, g].compact
|
|
170
|
+
missing_fields = expected_fields.select { |x| @data_object[x].nil? }
|
|
171
|
+
raise DecodeError.new("Missing event details (#{missing_fields.map(&:to_s).join(', ')})")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
80
175
|
def self.decode_cbor_objects(data)
|
|
81
176
|
objects = CBOR.decode_sequence(data)
|
|
82
177
|
|
|
@@ -92,13 +187,27 @@ module Skyfall
|
|
|
92
187
|
raise SubscriptionError.new(data['error'], data['message'])
|
|
93
188
|
end
|
|
94
189
|
|
|
95
|
-
raise DecodeError.new("Invalid object type: #{type}") unless type.is_a?(Hash)
|
|
96
|
-
raise
|
|
97
|
-
raise DecodeError.new("
|
|
98
|
-
raise DecodeError.new("Invalid
|
|
99
|
-
raise DecodeError.new("Invalid
|
|
190
|
+
raise DecodeError.new("Invalid object type: #{type.inspect}") unless type.is_a?(Hash)
|
|
191
|
+
raise DecodeError.new("Missing data: #{type.inspect}") unless type['op'] && type['t']
|
|
192
|
+
raise DecodeError.new("Invalid object type: #{type['op'].inspect}") unless type['op'].is_a?(Integer)
|
|
193
|
+
raise DecodeError.new("Invalid object type: #{type['t'].inspect}") unless type['t'].is_a?(String)
|
|
194
|
+
raise DecodeError.new("Invalid message type: #{type['t'].inspect}") unless type['t'].start_with?('#')
|
|
195
|
+
raise UnsupportedError.new("Unsupported version: #{type['op']}") unless type['op'] == 1
|
|
196
|
+
raise DecodeError.new("Invalid object type: #{data.inspect}") unless data.is_a?(Hash)
|
|
100
197
|
|
|
101
198
|
[type, data]
|
|
102
199
|
end
|
|
200
|
+
|
|
201
|
+
private_class_method :decode_cbor_objects
|
|
103
202
|
end
|
|
104
203
|
end
|
|
204
|
+
|
|
205
|
+
# need to be at the end because of a circular dependency
|
|
206
|
+
|
|
207
|
+
require_relative 'account_message'
|
|
208
|
+
require_relative 'commit_message'
|
|
209
|
+
require_relative 'identity_message'
|
|
210
|
+
require_relative 'info_message'
|
|
211
|
+
require_relative 'labels_message'
|
|
212
|
+
require_relative 'sync_message'
|
|
213
|
+
require_relative 'unknown_message'
|
|
@@ -1,58 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative '../collection'
|
|
2
4
|
require_relative '../firehose'
|
|
3
5
|
|
|
4
6
|
module Skyfall
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# A single record operation from a firehose commit event. An operation is a new record being
|
|
10
|
+
# created, or an existing record modified or deleted. It includes the URI and other details of
|
|
11
|
+
# the record in question, type of the action taken, and record data for "created" and "update"
|
|
12
|
+
# actions.
|
|
13
|
+
#
|
|
14
|
+
# Note: when a record is deleted, the previous record data is *not* included in the commit, only
|
|
15
|
+
# its URI. This means that if you're tracking records which are referencing other records, e.g.
|
|
16
|
+
# follow, block, or like records, you need to store information about this referencing record
|
|
17
|
+
# including an URI or rkey, because in case of a delete, you will not get information about which
|
|
18
|
+
# post was unliked or which account was unfollowed, only which like/follow record was deleted.
|
|
19
|
+
#
|
|
20
|
+
# At the moment, Skyfall doesn't parse the record data into any rich models specific for a given
|
|
21
|
+
# record type with a convenient API, but simply returns them as `Hash` objects (see {#raw_record}).
|
|
22
|
+
# In the future, a separate `#record` method might be added which returns a parsed record model.
|
|
23
|
+
#
|
|
24
|
+
|
|
5
25
|
class Firehose::Operation
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# @param message [Skyfall::Firehose::Message] commit message the operation is included in
|
|
29
|
+
# @param json [Hash] operation data
|
|
30
|
+
#
|
|
6
31
|
def initialize(message, json)
|
|
7
32
|
@message = message
|
|
8
33
|
@json = json
|
|
9
34
|
end
|
|
10
35
|
|
|
36
|
+
# @return [String] DID of the account/repository in which the operation happened
|
|
11
37
|
def repo
|
|
12
38
|
@message.repo
|
|
13
39
|
end
|
|
14
40
|
|
|
15
41
|
alias did repo
|
|
16
42
|
|
|
43
|
+
# @return [String] path part of the record URI (collection + rkey)
|
|
44
|
+
# @deprecated Use {#collection} + {#rkey}
|
|
17
45
|
def path
|
|
46
|
+
@@path_warning_printed ||= false
|
|
47
|
+
|
|
48
|
+
unless @@path_warning_printed
|
|
49
|
+
$stderr.puts "Warning: Skyfall::Firehose::Operation#path is deprecated - use #collection + #rkey"
|
|
50
|
+
@@path_warning_printed = true
|
|
51
|
+
end
|
|
52
|
+
|
|
18
53
|
@json['path']
|
|
19
54
|
end
|
|
20
55
|
|
|
56
|
+
# @return [Symbol] type of the operation (`:create`, `:update` or `:delete`)
|
|
21
57
|
def action
|
|
22
58
|
@json['action'].to_sym
|
|
23
59
|
end
|
|
24
60
|
|
|
61
|
+
# @return [String] record collection NSID
|
|
25
62
|
def collection
|
|
26
63
|
@json['path'].split('/')[0]
|
|
27
64
|
end
|
|
28
65
|
|
|
66
|
+
# @return [String] record rkey
|
|
29
67
|
def rkey
|
|
30
68
|
@json['path'].split('/')[1]
|
|
31
69
|
end
|
|
32
70
|
|
|
71
|
+
# @return [String] full AT URI of the record
|
|
33
72
|
def uri
|
|
34
|
-
"at://#{repo}/#{path}"
|
|
73
|
+
"at://#{repo}/#{@json['path']}"
|
|
35
74
|
end
|
|
36
75
|
|
|
76
|
+
# @return [CID, nil] CID (Content Identifier) of the record (nil for delete operations)
|
|
37
77
|
def cid
|
|
38
78
|
@cid ||= @json['cid'] && CID.from_cbor_tag(@json['cid'])
|
|
39
79
|
end
|
|
40
80
|
|
|
81
|
+
# @return [Hash, nil] record data as a plain Ruby Hash (nil for delete operations)
|
|
41
82
|
def raw_record
|
|
42
83
|
@raw_record ||= @message.raw_record_for_operation(self)
|
|
43
84
|
end
|
|
44
85
|
|
|
86
|
+
# Symbol short code of the collection, like `:bsky_post`. If the collection NSID is not
|
|
87
|
+
# recognized, the type is `:unknown`. The full NSID is always available through the
|
|
88
|
+
# `#collection` property.
|
|
89
|
+
#
|
|
90
|
+
# @return [Symbol]
|
|
91
|
+
# @see Skyfall::Collection
|
|
92
|
+
#
|
|
45
93
|
def type
|
|
46
94
|
Collection.short_code(collection)
|
|
47
95
|
end
|
|
48
96
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
end
|
|
52
|
-
|
|
97
|
+
# Returns a string with a representation of the object for debugging purposes.
|
|
98
|
+
# @return [String]
|
|
53
99
|
def inspect
|
|
54
100
|
vars = inspectable_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
|
|
55
101
|
"#<#{self.class}:0x#{object_id} #{vars}>"
|
|
56
102
|
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def inspectable_variables
|
|
107
|
+
instance_variables - [:@message]
|
|
108
|
+
end
|
|
57
109
|
end
|
|
58
110
|
end
|
|
@@ -1,9 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../car_archive'
|
|
1
4
|
require_relative '../firehose'
|
|
5
|
+
require_relative 'message'
|
|
2
6
|
|
|
3
7
|
module Skyfall
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Firehose message which declares the current state of the repository. The message is meant to
|
|
11
|
+
# trigger a resynchronization of the repository from a receiving consumer, if the consumer detects
|
|
12
|
+
# from the message rev that it must have missed some events from that repository.
|
|
13
|
+
#
|
|
14
|
+
# The sync message can be emitted by a PDS or relay to force a repair of a broken account state,
|
|
15
|
+
# or e.g. when an account is created, migrated or recovered from a CAR backup.
|
|
16
|
+
#
|
|
17
|
+
|
|
4
18
|
class Firehose::SyncMessage < Firehose::Message
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# @private
|
|
22
|
+
# @param type_object [Hash] first decoded CBOR frame with metadata
|
|
23
|
+
# @param data_object [Hash] second decoded CBOR frame with payload
|
|
24
|
+
# @raise [DecodeError] if the message doesn't include required data
|
|
25
|
+
#
|
|
26
|
+
def initialize(type_object, data_object)
|
|
27
|
+
super
|
|
28
|
+
check_if_not_nil 'seq', 'did', 'blocks', 'rev', 'time'
|
|
29
|
+
end
|
|
30
|
+
|
|
5
31
|
def rev
|
|
6
32
|
@rev ||= @data_object['rev']
|
|
7
33
|
end
|
|
34
|
+
|
|
35
|
+
# @return [Skyfall::CarArchive] commit data in the form of a parsed CAR archive
|
|
36
|
+
def blocks
|
|
37
|
+
@blocks ||= CarArchive.new(@data_object['blocks'])
|
|
38
|
+
end
|
|
8
39
|
end
|
|
9
40
|
end
|
data/lib/skyfall/firehose.rb
CHANGED
|
@@ -1,9 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'stream'
|
|
2
4
|
require 'uri'
|
|
3
5
|
|
|
4
6
|
module Skyfall
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# Client of a standard AT Protocol firehose websocket.
|
|
10
|
+
#
|
|
11
|
+
# This is the main Skyfall class to use to connect to a CBOR-based firehose
|
|
12
|
+
# websocket endpoint like `subscribeRepos` (on a PDS or a relay).
|
|
13
|
+
#
|
|
14
|
+
# To connect to the firehose, you need to:
|
|
15
|
+
#
|
|
16
|
+
# * create an instance of {Firehose}, passing it the hostname/URL of the server,
|
|
17
|
+
# name of the endpoint (normally `:subscribe_repos`) and optionally a cursor
|
|
18
|
+
# * set up callbacks to be run when connecting, disconnecting, when a message
|
|
19
|
+
# is received etc. (you need to set at least a message handler)
|
|
20
|
+
# * call {#connect} to start the connection
|
|
21
|
+
# * handle the received messages (instances of a {Skyfall::Firehose::Message}
|
|
22
|
+
# subclass)
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# client = Skyfall::Firehose.new('bsky.network', :subscribe_repos, last_cursor)
|
|
26
|
+
# # or: client = Skyfall::Firehose.new('bsky.network', last_cursor)
|
|
27
|
+
#
|
|
28
|
+
# client.on_message do |msg|
|
|
29
|
+
# next unless msg.type == :commit
|
|
30
|
+
#
|
|
31
|
+
# msg.operations.each do |op|
|
|
32
|
+
# if op.type == :bsky_post && op.action == :create
|
|
33
|
+
# puts "[#{msg.time}] #{msg.repo}: #{op.raw_record['text']}"
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# client.connect
|
|
39
|
+
#
|
|
40
|
+
# # You might also want to set some or all of these lifecycle callback handlers:
|
|
41
|
+
#
|
|
42
|
+
# client.on_connecting { |url| puts "Connecting to #{url}..." }
|
|
43
|
+
# client.on_connect { puts "Connected" }
|
|
44
|
+
# client.on_disconnect { puts "Disconnected" }
|
|
45
|
+
# client.on_reconnect { puts "Connection lost, trying to reconnect..." }
|
|
46
|
+
# client.on_timeout { puts "Connection stalled, triggering a reconnect..." }
|
|
47
|
+
# client.on_error { |e| puts "ERROR: #{e}" }
|
|
48
|
+
#
|
|
49
|
+
# @note Most of the methods of this class that you might want to use are defined in {Skyfall::Stream}.
|
|
50
|
+
#
|
|
51
|
+
|
|
5
52
|
class Firehose < Stream
|
|
53
|
+
|
|
54
|
+
# the main firehose endpoint on a PDS or relay
|
|
6
55
|
SUBSCRIBE_REPOS = "com.atproto.sync.subscribeRepos"
|
|
56
|
+
|
|
57
|
+
# only used with moderation services (labellers)
|
|
7
58
|
SUBSCRIBE_LABELS = "com.atproto.label.subscribeLabels"
|
|
8
59
|
|
|
9
60
|
NAMED_ENDPOINTS = {
|
|
@@ -11,17 +62,68 @@ module Skyfall
|
|
|
11
62
|
:subscribe_labels => SUBSCRIBE_LABELS
|
|
12
63
|
}
|
|
13
64
|
|
|
65
|
+
# Current cursor (seq of the last seen message)
|
|
66
|
+
# @return [Integer, nil]
|
|
14
67
|
attr_accessor :cursor
|
|
15
68
|
|
|
16
|
-
|
|
69
|
+
#
|
|
70
|
+
# @overload initialize(server, endpoint, cursor = nil)
|
|
71
|
+
# Returns a new instance of a firehose client connecting to a given endpoint.
|
|
72
|
+
#
|
|
73
|
+
# @param server [String]
|
|
74
|
+
# Address of the server to connect to.
|
|
75
|
+
# Expects a string with either just a hostname, or a ws:// or wss:// URL with no path.
|
|
76
|
+
# @param endpoint [Symbol, String]
|
|
77
|
+
# XRPC method name.
|
|
78
|
+
# Pass either a full NSID, or a symbol shorthand from {NAMED_ENDPOINTS}
|
|
79
|
+
# @param cursor [Integer, String, nil]
|
|
80
|
+
# sequence number from which to resume
|
|
81
|
+
# @raise [ArgumentError] if any of the parameters is invalid
|
|
82
|
+
#
|
|
83
|
+
# @overload initialize(server, cursor = nil)
|
|
84
|
+
# Returns a new instance of a firehose client connecting to `subscribeRepos`.
|
|
85
|
+
#
|
|
86
|
+
# @param server [String]
|
|
87
|
+
# Address of the server to connect to.
|
|
88
|
+
# Expects a string with either just a hostname, or a ws:// or wss:// URL with no path.
|
|
89
|
+
# @param cursor [Integer, String, nil]
|
|
90
|
+
# sequence number from which to resume
|
|
91
|
+
# @raise [ArgumentError] if any of the parameters is invalid
|
|
92
|
+
#
|
|
93
|
+
|
|
94
|
+
def initialize(server, endpoint = nil, cursor = nil)
|
|
17
95
|
require_relative 'firehose/message'
|
|
18
96
|
super(server)
|
|
19
97
|
|
|
98
|
+
if cursor.nil? && (endpoint.nil? || endpoint.to_s =~ /\A\d+\z/)
|
|
99
|
+
cursor = endpoint
|
|
100
|
+
endpoint = :subscribe_repos
|
|
101
|
+
end
|
|
102
|
+
|
|
20
103
|
@endpoint = check_endpoint(endpoint)
|
|
21
104
|
@cursor = check_cursor(cursor)
|
|
22
105
|
@root_url = ensure_empty_path(@root_url)
|
|
23
106
|
end
|
|
24
107
|
|
|
108
|
+
|
|
109
|
+
protected
|
|
110
|
+
|
|
111
|
+
# Returns the full URL of the websocket endpoint to connect to.
|
|
112
|
+
# @return [String]
|
|
113
|
+
|
|
114
|
+
def build_websocket_url
|
|
115
|
+
@root_url + "/xrpc/" + @endpoint + (@cursor ? "?cursor=#{@cursor}" : "")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Processes a single message received from the websocket. Passes the received data to the
|
|
119
|
+
# {#on_raw_message} handler, builds a {Skyfall::Firehose::Message} object, and passes it to
|
|
120
|
+
# the {#on_message} handler (if defined). Also updates the {#cursor} to this message's sequence
|
|
121
|
+
# number (note: this is skipped if {#on_message} is not set).
|
|
122
|
+
#
|
|
123
|
+
# @param msg
|
|
124
|
+
# {https://rubydoc.info/gems/faye-websocket/Faye/WebSocket/API/MessageEvent Faye::WebSocket::API::MessageEvent}
|
|
125
|
+
# @return [nil]
|
|
126
|
+
|
|
25
127
|
def handle_message(msg)
|
|
26
128
|
data = msg.data
|
|
27
129
|
@handlers[:raw_message]&.call(data)
|
|
@@ -38,10 +140,6 @@ module Skyfall
|
|
|
38
140
|
|
|
39
141
|
private
|
|
40
142
|
|
|
41
|
-
def build_websocket_url
|
|
42
|
-
@root_url + "/xrpc/" + @endpoint + (@cursor ? "?cursor=#{@cursor}" : "")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
143
|
def check_cursor(cursor)
|
|
46
144
|
if cursor.nil?
|
|
47
145
|
nil
|
|
@@ -1,17 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative '../errors'
|
|
2
4
|
require_relative '../jetstream'
|
|
5
|
+
require_relative 'message'
|
|
3
6
|
|
|
4
7
|
module Skyfall
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Jetstream message sent when the status of an account changes. This can be:
|
|
11
|
+
#
|
|
12
|
+
# - an account being created, sending its initial state (should be active)
|
|
13
|
+
# - an account being deactivated or suspended
|
|
14
|
+
# - an account being restored back to an active state from deactivation/suspension
|
|
15
|
+
# - an account being deleted (the status returning `:deleted`)
|
|
16
|
+
#
|
|
17
|
+
|
|
5
18
|
class Jetstream::AccountMessage < Jetstream::Message
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# @param json [Hash] message JSON decoded from the websocket message
|
|
22
|
+
# @raise [DecodeError] if the message doesn't include required data
|
|
23
|
+
#
|
|
6
24
|
def initialize(json)
|
|
7
|
-
raise DecodeError.new("Missing event details") if json['account'].nil?
|
|
25
|
+
raise DecodeError.new("Missing event details (account)") if json['account'].nil? || json['account']['active'].nil?
|
|
8
26
|
super
|
|
9
27
|
end
|
|
10
28
|
|
|
29
|
+
# @return [Boolean] true if the account is active, false if it's deactivated/suspended etc.
|
|
11
30
|
def active?
|
|
12
31
|
@json['account']['active']
|
|
13
32
|
end
|
|
14
33
|
|
|
34
|
+
# @return [Symbol, nil] for inactive accounts, specifies the exact state; nil for active accounts
|
|
15
35
|
def status
|
|
16
36
|
@json['account']['status']&.to_sym
|
|
17
37
|
end
|
|
@@ -1,16 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative '../errors'
|
|
2
4
|
require_relative '../jetstream'
|
|
5
|
+
require_relative 'message'
|
|
3
6
|
require_relative 'operation'
|
|
4
7
|
|
|
5
8
|
module Skyfall
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# Jetstream message which includes a single operation on a record in the repo (a record was
|
|
12
|
+
# created, updated or deleted). Most of the messages received from Jetstream are of this type,
|
|
13
|
+
# and this is the type you will usually be most interested in.
|
|
14
|
+
#
|
|
15
|
+
|
|
6
16
|
class Jetstream::CommitMessage < Jetstream::Message
|
|
17
|
+
|
|
18
|
+
#
|
|
19
|
+
# @param json [Hash] message JSON decoded from the websocket message
|
|
20
|
+
# @raise [DecodeError] if the message doesn't include required data
|
|
21
|
+
#
|
|
7
22
|
def initialize(json)
|
|
8
|
-
raise DecodeError.new("Missing event details") if json['commit'].nil?
|
|
23
|
+
raise DecodeError.new("Missing event details (commit)") if json['commit'].nil?
|
|
24
|
+
|
|
25
|
+
%w(collection rkey operation).each { |f| raise DecodeError.new("Missing event details (#{f})") if json['commit'][f].nil? }
|
|
26
|
+
|
|
9
27
|
super
|
|
10
28
|
end
|
|
11
29
|
|
|
30
|
+
# Returns the record operation included in the commit.
|
|
31
|
+
# @return [Jetstream::Operation]
|
|
32
|
+
#
|
|
33
|
+
def operation
|
|
34
|
+
@operation ||= Jetstream::Operation.new(self, json['commit'])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
alias op operation
|
|
38
|
+
|
|
39
|
+
# Returns record operations included in the commit. Currently a `:commit` message from
|
|
40
|
+
# Jetstream always includes exactly one operation, but for compatibility with
|
|
41
|
+
# {Skyfall::Firehose}'s API it's also returned in an array here.
|
|
42
|
+
#
|
|
43
|
+
# @return [Array<Jetstream::Operation>]
|
|
44
|
+
#
|
|
12
45
|
def operations
|
|
13
|
-
|
|
46
|
+
[operation]
|
|
14
47
|
end
|
|
15
48
|
end
|
|
16
49
|
end
|
|
@@ -1,13 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative '../errors'
|
|
2
4
|
require_relative '../jetstream'
|
|
5
|
+
require_relative 'message'
|
|
3
6
|
|
|
4
7
|
module Skyfall
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Jetstream message sent when a new DID is created or when the details of someone's DID document
|
|
11
|
+
# are changed (usually either a handle change or a migration to a different PDS). The message
|
|
12
|
+
# should include currently assigned handle (though the field is not required).
|
|
13
|
+
#
|
|
14
|
+
# Note: the message is originally emitted from the account's PDS and is passed as is by relays,
|
|
15
|
+
# which means you can't fully trust that the handle is actually correctly assigned to the DID
|
|
16
|
+
# and verified by DNS or well-known. To confirm that, use `DID.resolve_handle` from
|
|
17
|
+
# [DIDKit](https://ruby.sdk.blue/didkit/).
|
|
18
|
+
#
|
|
19
|
+
|
|
5
20
|
class Jetstream::IdentityMessage < Jetstream::Message
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# @param json [Hash] message JSON decoded from the websocket message
|
|
24
|
+
# @raise [DecodeError] if the message doesn't include required data
|
|
25
|
+
#
|
|
6
26
|
def initialize(json)
|
|
7
|
-
raise DecodeError.new("Missing event details") if json['identity'].nil?
|
|
27
|
+
raise DecodeError.new("Missing event details (identity)") if json['identity'].nil?
|
|
8
28
|
super
|
|
9
29
|
end
|
|
10
30
|
|
|
31
|
+
# @return [String, nil] current handle assigned to the DID
|
|
11
32
|
def handle
|
|
12
33
|
@json['identity']['handle']
|
|
13
34
|
end
|