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.
@@ -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
- require_relative 'account_message'
13
- require_relative 'commit_message'
14
- require_relative 'handle_message'
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
- # :nodoc: - consider this as semi-private API
26
- attr_reader :type_object, :data_object
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.parse(@data_object['time'])
129
+ @time ||= @data_object['time'] && Time.iso8601(@data_object['time'])
67
130
  end
68
131
 
69
- def inspectable_variables
70
- instance_variables - [:@type_object, :@data_object, :@blocks]
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 UnsupportedError.new("Unexpected CBOR object: #{type}") unless type['op'] == 1
97
- raise DecodeError.new("Missing data: #{type} #{objects.inspect}") unless type['op'] && type['t']
98
- raise DecodeError.new("Invalid message type: #{type['t']}") unless type['t'].start_with?('#')
99
- raise DecodeError.new("Invalid object type: #{data}") unless data.is_a?(Hash)
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
- def inspectable_variables
50
- instance_variables - [:@message]
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
@@ -1,6 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../firehose'
4
+ require_relative 'message'
2
5
 
3
6
  module Skyfall
7
+
8
+ #
9
+ # Firehose message of an unrecognized type.
10
+ #
11
+
4
12
  class Firehose::UnknownMessage < Firehose::Message
5
13
  end
6
14
  end
@@ -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
- def initialize(server, endpoint, cursor = nil)
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
- @operations ||= [Jetstream::Operation.new(self, json['commit'])]
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