skyfall 0.7.0 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -1
- data/README.md +40 -31
- data/lib/skyfall/firehose/commit_message.rb +9 -8
- data/lib/skyfall/firehose/message.rb +2 -2
- data/lib/skyfall/firehose/operation.rb +3 -2
- data/lib/skyfall/firehose/sync_message.rb +3 -3
- data/lib/skyfall/jetstream/operation.rb +3 -2
- data/lib/skyfall/label.rb +3 -2
- data/lib/skyfall/version.rb +1 -1
- metadata +20 -62
- data/lib/skyfall/car_archive.rb +0 -158
- data/lib/skyfall/cid.rb +0 -45
- data/lib/skyfall/extensions.rb +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c52f2f3c63530b1e032666c7bfb71ba0b0279bfe0128c7432b8e267f1e2f9e95
|
|
4
|
+
data.tar.gz: 1c7e2e5cac7ec4f421be5c291a311dede53221150901a44418f01f7a6cdeae9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1eb9556cbc363f6a9c2ae42d27dc9a4471a24f8e08f0b1b8046c131fe320b63de73498d839367f0fd89c3841b27877c53bc0a937b9eeed41ea486f05b55a38c3
|
|
7
|
+
data.tar.gz: 9b61caf5cc63834efd8185622c435a9102b4a310f890a988fd029e9e691e80acf7527e0afc5e5fecde967c4f2de538559168d07a72a2af4dd9d5ef06cc16ab50
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
## [0.7.1] - 2026-08-05
|
|
2
|
+
|
|
3
|
+
- extracted `CID` and `CarArchive` to a new gem "oxygene"
|
|
4
|
+
- added `CommitMessage#cid` as alias of `#commit`
|
|
5
|
+
|
|
1
6
|
## [0.7.0] - 2026-02-13
|
|
2
7
|
|
|
3
|
-
The main change in this version is that inline YARD documentation has been added. This was also a good opportunity to review some APIs and tweak some things in order to get Skyfall a bit closer to 1.0.
|
|
8
|
+
The main change in this version is that inline [YARD documentation](https://rubydoc.info/gems/skyfall/) has been added. This was also a good opportunity to review some APIs and tweak some things in order to get Skyfall a bit closer to 1.0.
|
|
4
9
|
|
|
5
10
|
New APIs:
|
|
6
11
|
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Skyfall
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/skyfall) [](https://rubydoc.info/gems/skyfall)
|
|
4
|
+
|
|
3
5
|
A Ruby gem for streaming data from the Bluesky/ATProto firehose 🦋
|
|
4
6
|
|
|
5
7
|
> [!NOTE]
|
|
@@ -15,7 +17,7 @@ Since version 0.5, Skyfall also supports connecting to [Jetstream](https://githu
|
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
18
|
-
To use Skyfall, you need a reasonably new version of Ruby – it should run on Ruby 2.6 and above, although it's recommended to use a version that's still getting maintainance updates, i.e. currently 3.
|
|
20
|
+
To use Skyfall, you need a reasonably new version of Ruby – it should run on Ruby 2.6 and above, although it's recommended to use a version that's still getting maintainance updates, i.e. currently 3.3+. A compatible version should be preinstalled on macOS Big Sur and above and on many Linux systems. Otherwise, you can install one using tools such as [RVM](https://rvm.io), [asdf](https://asdf-vm.com), [ruby-install](https://github.com/postmodern/ruby-install) or [ruby-build](https://github.com/rbenv/ruby-build), or `rpm` or `apt-get` on Linux (see more installation options on [ruby-lang.org](https://www.ruby-lang.org/en/downloads/)).
|
|
19
21
|
|
|
20
22
|
To install the gem, run the command:
|
|
21
23
|
|
|
@@ -38,7 +40,7 @@ require 'skyfall'
|
|
|
38
40
|
sky = Skyfall::Firehose.new('bsky.network', :subscribe_repos)
|
|
39
41
|
```
|
|
40
42
|
|
|
41
|
-
The server name can be just a hostname, or a full URL with a `ws:` or `wss:` scheme, which is useful if you want to use a non-encrypted websocket connection, e.g. `"ws://localhost:8000"`. The endpoint can be either a full NSID string like `"com.atproto.sync.subscribeRepos"`, or one of the defined symbol shortcuts
|
|
43
|
+
The server name can be just a hostname, or a full URL with a `ws:` or `wss:` scheme, which is useful if you want to use a non-encrypted websocket connection, e.g. `"ws://localhost:8000"`. The endpoint can be either a full NSID string like `"com.atproto.sync.subscribeRepos"`, or one of the defined symbol shortcuts – you will almost always want to pass `:subscribe_repos` here.
|
|
42
44
|
|
|
43
45
|
Next, set up event listeners to handle incoming messages and get notified of errors. Here are all the available listeners (you will need at least either `on_message` or `on_raw_message`):
|
|
44
46
|
|
|
@@ -60,7 +62,7 @@ sky.on_timeout { puts "Connection stalled, triggering a reconnect..." }
|
|
|
60
62
|
sky.on_error { |e| puts "ERROR: #{e}" }
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
You can also call these as setters accepting a `Proc`
|
|
65
|
+
You can also call these as setters accepting a `Proc` – e.g. to disable default error handling, you can do:
|
|
64
66
|
|
|
65
67
|
```rb
|
|
66
68
|
sky.on_error = nil
|
|
@@ -94,7 +96,7 @@ sky.connect
|
|
|
94
96
|
|
|
95
97
|
### Cursors
|
|
96
98
|
|
|
97
|
-
ATProto websocket endpoints implement a "*cursor*" feature to help you make sure that you don't miss anything if your connection is down for a bit (because of a network issue, server restart, deploy etc.). Each message includes a `seq` field, which is the sequence number of the event. You can keep track of the last seq you've seen, and when you reconnect, you pass that number as a cursor parameter
|
|
99
|
+
ATProto websocket endpoints implement a "*cursor*" feature to help you make sure that you don't miss anything if your connection is down for a bit (because of a network issue, server restart, deploy etc.). Each message includes a `seq` field, which is the sequence number of the event. You can keep track of the last seq you've seen, and when you reconnect, you pass that number as a cursor parameter – the server will then "replay" all events you might have missed since that last one. (The `bsky.network` Relay firehose currently has a buffer of about 72 hours, though that's not something required by specification.)
|
|
98
100
|
|
|
99
101
|
To use a cursor when connecting to the firehose, pass it as the third parameter to `Skyfall::Firehose`. You should then regularly save the `seq` of the last event to some permanent storage, and then load it from there when reconnecting.
|
|
100
102
|
|
|
@@ -127,57 +129,57 @@ end
|
|
|
127
129
|
|
|
128
130
|
Each message passed to `on_message` is an instance of a subclass of either `Skyfall::Firehose::Message` or `Skyfall::Jetstream::Message`, depending on the selected source. The supported message types are:
|
|
129
131
|
|
|
130
|
-
- `CommitMessage` (`#commit`)
|
|
131
|
-
- `IdentityMessage` (`#identity`)
|
|
132
|
-
- `AccountMessage` (`#account`)
|
|
133
|
-
- `SyncMessage` (`#sync`)
|
|
134
|
-
- `LabelsMessage` (`#labels`)
|
|
135
|
-
- `InfoMessage` (`#info`)
|
|
132
|
+
- `CommitMessage` (`#commit`) – represents a change in a user's repo; most messages are of this type
|
|
133
|
+
- `IdentityMessage` (`#identity`) – notifies about a change in user's DID document, e.g. a handle change or a migration to a new PDS
|
|
134
|
+
- `AccountMessage` (`#account`) – notifies about a change of an account's status (de/activation, suspension, deletion)
|
|
135
|
+
- `SyncMessage` (`#sync`) – updates repository state, can be used to trigger account resynchronization
|
|
136
|
+
- `LabelsMessage` (`#labels`) – only used in `subscribe_labels` endpoint
|
|
137
|
+
- `InfoMessage` (`#info`) – a protocol error message, e.g. about an invalid cursor parameter
|
|
136
138
|
- `UnknownMessage` is used for other unrecognized message types
|
|
137
139
|
|
|
138
140
|
`Skyfall::Firehose::Message` and `Skyfall::Jetstream::Message` variants of message classes should have more or less the same interface, except when a given field is not included in one of the formats.
|
|
139
141
|
|
|
140
142
|
All message objects have the following shared properties:
|
|
141
143
|
|
|
142
|
-
- `type` (symbol)
|
|
143
|
-
- `seq` (integer)
|
|
144
|
-
- `repo` or `did` (string)
|
|
145
|
-
- `time` (Time)
|
|
144
|
+
- `type` (symbol) – the message type identifier, e.g. `:commit`
|
|
145
|
+
- `seq` (integer) – a sequential index of the message; Jetstream messages instead have a `time_us` value, which is a Unix timestamp in microseconds (also aliased as `seq` for compatibility)
|
|
146
|
+
- `repo` or `did` (string) – DID of the repository (user account)
|
|
147
|
+
- `time` (Time) – timestamp of the described action
|
|
146
148
|
|
|
147
149
|
All properties except `type` may be nil for some message types that aren't related to a specific user, like `#info`.
|
|
148
150
|
|
|
149
151
|
Commit messages additionally have:
|
|
150
152
|
|
|
151
|
-
- `commit`
|
|
152
|
-
- `operations`
|
|
153
|
+
- `commit` – CID of the commit
|
|
154
|
+
- `operations` – list of operations (usually one)
|
|
153
155
|
|
|
154
156
|
Handle and Identity messages additionally have:
|
|
155
157
|
|
|
156
|
-
- `handle`
|
|
158
|
+
- `handle` – the new handle assigned to the DID
|
|
157
159
|
|
|
158
160
|
Account messages additionally have:
|
|
159
161
|
|
|
160
|
-
- `active?`
|
|
161
|
-
- `status`
|
|
162
|
+
- `active?` – whether the account is active, or inactive for any reason
|
|
163
|
+
- `status` – if not active, shows the status of the account (`:deactivated`, `:deleted`, `:takendown`)
|
|
162
164
|
|
|
163
165
|
Info messages additionally have:
|
|
164
166
|
|
|
165
|
-
- `name`
|
|
166
|
-
- `message`
|
|
167
|
+
- `name` – identifier of the message/error
|
|
168
|
+
- `message` – a human-readable description
|
|
167
169
|
|
|
168
170
|
|
|
169
171
|
### Commit operations
|
|
170
172
|
|
|
171
173
|
Operations are objects of type `Skyfall::Firehose::Operation` or `Skyfall::Jetstream::Operation` and have such properties:
|
|
172
174
|
|
|
173
|
-
- `repo` or `did` (string)
|
|
174
|
-
- `collection` (string)
|
|
175
|
-
- `type` (symbol)
|
|
176
|
-
- `rkey` (string)
|
|
177
|
-
- `path` (string)
|
|
178
|
-
- `uri` (string)
|
|
179
|
-
- `action` (symbol)
|
|
180
|
-
- `cid` (CID)
|
|
175
|
+
- `repo` or `did` (string) – DID of the repository (user account)
|
|
176
|
+
- `collection` (string) – name of the relevant collection in the repository, e.g. `app.bsky.feed.post` for posts
|
|
177
|
+
- `type` (symbol) – short name of the collection, e.g. `:bsky_post`
|
|
178
|
+
- `rkey` (string) – identifier of a record in a collection
|
|
179
|
+
- `path` (string) – the path part of the at:// URI – collection name + ID (rkey) of the item
|
|
180
|
+
- `uri` (string) – the complete at:// URI
|
|
181
|
+
- `action` (symbol) – `:create`, `:update` or `:delete`
|
|
182
|
+
- `cid` (CID) – CID of the operation/record (`nil` for delete operations)
|
|
181
183
|
|
|
182
184
|
Create and update operations will also have an attached record (JSON object) with details of the post, like etc. The record data is currently available as a Ruby hash via `raw_record` property (custom types will be added in future).
|
|
183
185
|
|
|
@@ -206,7 +208,7 @@ Note that the `Operation` objects have two properties that tell you the kind of
|
|
|
206
208
|
|
|
207
209
|
When Skyfall receives a message about a record type that's not on the list, whether in the `app.bsky` namespace or not, the operation `type` will be `:unknown`, while the `collection` will be the original string. So if an app like e.g. "Skygram" appears with a `zz.skygram.*` namespace that lets you share photos on ATProto, the operations will have a type `:unknown` and collection names like `zz.skygram.feed.photo`, and you can check the `collection` field for record types known to you and process them in some appropriate way, even if Skyfall doesn't recognize the record type.
|
|
208
210
|
|
|
209
|
-
Do not however check if such operations have a `type` equal to `:unknown` first
|
|
211
|
+
Do not however check if such operations have a `type` equal to `:unknown` first – just ignore the type and only check the `collection` string. The reason is that some next version of Skyfall might start recognizing those records and add a new `type` value for them like e.g. `:skygram_photo`, and then they won't match your condition anymore.
|
|
210
212
|
|
|
211
213
|
|
|
212
214
|
## Reconnection logic
|
|
@@ -260,7 +262,7 @@ sky.on_message do |msg|
|
|
|
260
262
|
end
|
|
261
263
|
```
|
|
262
264
|
|
|
263
|
-
See [ATProto label docs](https://atproto.com/specs/label) for info on what fields are included with each label
|
|
265
|
+
See [ATProto label docs](https://atproto.com/specs/label) for info on what fields are included with each label – `Skyfall::Label` includes properties with these original names, and also more friendly aliases for each (e.g. `value` instead of `val`).
|
|
264
266
|
|
|
265
267
|
|
|
266
268
|
## Other configuration
|
|
@@ -302,6 +304,13 @@ See [Jetstream docs](https://github.com/bluesky-social/jetstream?tab=readme-ov-f
|
|
|
302
304
|
> The `compress` and `requireHello` options (and zstd compression) are not available at the moment. Also the "subscriber sourced messages" aren't implemented yet.
|
|
303
305
|
|
|
304
306
|
|
|
307
|
+
## Other resources
|
|
308
|
+
|
|
309
|
+
- [YARD API documentation](https://rubydoc.info/gems/skyfall) at rubydoc.info
|
|
310
|
+
- [ruby.sdk.blue](https://ruby.sdk.blue)
|
|
311
|
+
- [Example scripts](https://ruby.sdk.blue/examples/)
|
|
312
|
+
- [bluesky-feeds-rb](https://tangled.org/mackuba.eu/bluesky-feeds-rb) – feed generator template project
|
|
313
|
+
|
|
305
314
|
## Credits
|
|
306
315
|
|
|
307
316
|
Copyright © 2026 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)).
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../car_archive'
|
|
4
|
-
require_relative '../cid'
|
|
5
3
|
require_relative '../firehose'
|
|
6
4
|
require_relative 'message'
|
|
7
5
|
require_relative 'operation'
|
|
6
|
+
require 'oxygene'
|
|
8
7
|
|
|
9
8
|
module Skyfall
|
|
10
9
|
|
|
@@ -39,19 +38,21 @@ module Skyfall
|
|
|
39
38
|
@data_object['since']
|
|
40
39
|
end
|
|
41
40
|
|
|
42
|
-
# @return [CID, nil] CID (Content Identifier) of data of the previous commit in the repo
|
|
41
|
+
# @return [Oxygene::CID, nil] CID (Content Identifier) of data of the previous commit in the repo
|
|
43
42
|
def prev_data
|
|
44
|
-
@prev_data ||= CID.from_cbor_tag(@data_object['prevData'])
|
|
43
|
+
@prev_data ||= Oxygene::CID.from_cbor_tag(@data_object['prevData'])
|
|
45
44
|
end
|
|
46
45
|
|
|
47
|
-
# @return [CID] CID (Content Identifier) of the commit
|
|
46
|
+
# @return [Oxygene::CID] CID (Content Identifier) of the commit
|
|
48
47
|
def commit
|
|
49
|
-
@commit ||= CID.from_cbor_tag(@data_object['commit'])
|
|
48
|
+
@commit ||= Oxygene::CID.from_cbor_tag(@data_object['commit'])
|
|
50
49
|
end
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
alias cid commit
|
|
52
|
+
|
|
53
|
+
# @return [Oxygene::CARArchive] commit data in the form of a parsed CAR archive
|
|
53
54
|
def blocks
|
|
54
|
-
@blocks ||=
|
|
55
|
+
@blocks ||= Oxygene::CARArchive.new(@data_object['blocks'])
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
# @return [Array<Firehose::Operation>] record operations (usually one) included in the commit
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../errors'
|
|
4
|
-
require_relative '../extensions'
|
|
5
4
|
require_relative '../firehose'
|
|
6
5
|
|
|
7
6
|
require 'cbor'
|
|
7
|
+
require 'oxygene'
|
|
8
8
|
require 'time'
|
|
9
9
|
|
|
10
10
|
module Skyfall
|
|
@@ -24,7 +24,7 @@ module Skyfall
|
|
|
24
24
|
# the {Skyfall::Stream#on_raw_message} event handler.
|
|
25
25
|
|
|
26
26
|
class Firehose::Message
|
|
27
|
-
using
|
|
27
|
+
using Oxygene::Extensions
|
|
28
28
|
|
|
29
29
|
# Type of the message (e.g. `:commit`, `:identity` etc.)
|
|
30
30
|
# @return [Symbol]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../collection'
|
|
4
4
|
require_relative '../firehose'
|
|
5
|
+
require 'oxygene'
|
|
5
6
|
|
|
6
7
|
module Skyfall
|
|
7
8
|
|
|
@@ -73,9 +74,9 @@ module Skyfall
|
|
|
73
74
|
"at://#{repo}/#{@json['path']}"
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
# @return [CID, nil] CID (Content Identifier) of the record (nil for delete operations)
|
|
77
|
+
# @return [Oxygene::CID, nil] CID (Content Identifier) of the record (nil for delete operations)
|
|
77
78
|
def cid
|
|
78
|
-
@cid ||= @json['cid'] && CID.from_cbor_tag(@json['cid'])
|
|
79
|
+
@cid ||= @json['cid'] && Oxygene::CID.from_cbor_tag(@json['cid'])
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
# @return [Hash, nil] record data as a plain Ruby Hash (nil for delete operations)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../car_archive'
|
|
4
3
|
require_relative '../firehose'
|
|
5
4
|
require_relative 'message'
|
|
5
|
+
require 'oxygene'
|
|
6
6
|
|
|
7
7
|
module Skyfall
|
|
8
8
|
|
|
@@ -32,9 +32,9 @@ module Skyfall
|
|
|
32
32
|
@rev ||= @data_object['rev']
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# @return [
|
|
35
|
+
# @return [Oxygene::CARArchive] commit data in the form of a parsed CAR archive
|
|
36
36
|
def blocks
|
|
37
|
-
@blocks ||=
|
|
37
|
+
@blocks ||= Oxygene::CARArchive.new(@data_object['blocks'])
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../collection'
|
|
4
4
|
require_relative '../jetstream'
|
|
5
|
+
require 'oxygene'
|
|
5
6
|
|
|
6
7
|
module Skyfall
|
|
7
8
|
|
|
@@ -73,9 +74,9 @@ module Skyfall
|
|
|
73
74
|
"at://#{repo}/#{collection}/#{rkey}"
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
# @return [CID, nil] CID (Content Identifier) of the record (nil for delete operations)
|
|
77
|
+
# @return [Oxygene::CID, nil] CID (Content Identifier) of the record (nil for delete operations)
|
|
77
78
|
def cid
|
|
78
|
-
@cid ||= @json['cid'] && CID.from_json(@json['cid'])
|
|
79
|
+
@cid ||= @json['cid'] && Oxygene::CID.from_json(@json['cid'])
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
# @return [Hash, nil] record data as a plain Ruby Hash (nil for delete operations)
|
data/lib/skyfall/label.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'errors'
|
|
4
|
+
require 'oxygene'
|
|
4
5
|
require 'time'
|
|
5
6
|
|
|
6
7
|
module Skyfall
|
|
@@ -60,9 +61,9 @@ module Skyfall
|
|
|
60
61
|
@data['uri']
|
|
61
62
|
end
|
|
62
63
|
|
|
63
|
-
# @return [CID, nil] CID of the specific version of the subject that this label applies to
|
|
64
|
+
# @return [Oxygene::CID, nil] CID of the specific version of the subject that this label applies to
|
|
64
65
|
def cid
|
|
65
|
-
@cid ||= @data['cid'] && CID.from_json(@data['cid'])
|
|
66
|
+
@cid ||= @data['cid'] && Oxygene::CID.from_json(@data['cid'])
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
# @return [String] label value
|
data/lib/skyfall/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: skyfall
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kuba Suder
|
|
@@ -10,99 +10,60 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: eventmachine
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '1.2'
|
|
19
19
|
- - ">="
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version:
|
|
21
|
+
version: 1.2.7
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
25
|
requirements:
|
|
26
26
|
- - "~>"
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: '
|
|
28
|
+
version: '1.2'
|
|
29
29
|
- - ">="
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version:
|
|
32
|
-
- !ruby/object:Gem::Dependency
|
|
33
|
-
name: base64
|
|
34
|
-
requirement: !ruby/object:Gem::Requirement
|
|
35
|
-
requirements:
|
|
36
|
-
- - "~>"
|
|
37
|
-
- !ruby/object:Gem::Version
|
|
38
|
-
version: '0.1'
|
|
39
|
-
type: :runtime
|
|
40
|
-
prerelease: false
|
|
41
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
-
requirements:
|
|
43
|
-
- - "~>"
|
|
44
|
-
- !ruby/object:Gem::Version
|
|
45
|
-
version: '0.1'
|
|
31
|
+
version: 1.2.7
|
|
46
32
|
- !ruby/object:Gem::Dependency
|
|
47
|
-
name:
|
|
33
|
+
name: faye-websocket
|
|
48
34
|
requirement: !ruby/object:Gem::Requirement
|
|
49
35
|
requirements:
|
|
50
36
|
- - "~>"
|
|
51
37
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: '0.
|
|
53
|
-
- - ">="
|
|
54
|
-
- !ruby/object:Gem::Version
|
|
55
|
-
version: 0.5.9.6
|
|
38
|
+
version: '0.12'
|
|
56
39
|
type: :runtime
|
|
57
40
|
prerelease: false
|
|
58
41
|
version_requirements: !ruby/object:Gem::Requirement
|
|
59
42
|
requirements:
|
|
60
43
|
- - "~>"
|
|
61
44
|
- !ruby/object:Gem::Version
|
|
62
|
-
version: '0.
|
|
63
|
-
- - ">="
|
|
64
|
-
- !ruby/object:Gem::Version
|
|
65
|
-
version: 0.5.9.6
|
|
45
|
+
version: '0.12'
|
|
66
46
|
- !ruby/object:Gem::Dependency
|
|
67
|
-
name:
|
|
47
|
+
name: oxygene
|
|
68
48
|
requirement: !ruby/object:Gem::Requirement
|
|
69
49
|
requirements:
|
|
70
|
-
- - "~>"
|
|
71
|
-
- !ruby/object:Gem::Version
|
|
72
|
-
version: '1.2'
|
|
73
50
|
- - ">="
|
|
74
51
|
- !ruby/object:Gem::Version
|
|
75
|
-
version:
|
|
52
|
+
version: 0.0.1
|
|
76
53
|
type: :runtime
|
|
77
54
|
prerelease: false
|
|
78
55
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
56
|
requirements:
|
|
80
|
-
- - "~>"
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: '1.2'
|
|
83
57
|
- - ">="
|
|
84
58
|
- !ruby/object:Gem::Version
|
|
85
|
-
version:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
type: :runtime
|
|
94
|
-
prerelease: false
|
|
95
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
96
|
-
requirements:
|
|
97
|
-
- - "~>"
|
|
98
|
-
- !ruby/object:Gem::Version
|
|
99
|
-
version: '0.12'
|
|
100
|
-
description: "\n Skyfall is a Ruby library for connecting to the \"firehose\" of
|
|
101
|
-
the Bluesky social network, i.e. a websocket which\n streams all new posts and
|
|
102
|
-
everything else happening on the Bluesky network in real time. The code connects
|
|
103
|
-
to the\n websocket endpoint, decodes the messages which are encoded in some binary
|
|
104
|
-
formats, and returns the data as Ruby\n objects, which you can filter and save
|
|
105
|
-
to some kind of database (e.g. in order to create a custom feed).\n "
|
|
59
|
+
version: 0.0.1
|
|
60
|
+
description: "\n Skyfall is a Ruby gem for connecting to the \"firehose\" of the
|
|
61
|
+
AT Protocol network that Bluesky is built on,\n i.e. a websocket which streams
|
|
62
|
+
all new posts and everything else happening on the network in real time. The code\n
|
|
63
|
+
\ connects to the websocket endpoint, decodes the messages which are encoded in
|
|
64
|
+
some binary formats, and returns\n the data as Ruby objects and hashes, which
|
|
65
|
+
you can filter and save to some kind of database (e.g. in order\n to create a
|
|
66
|
+
custom feed).\n "
|
|
106
67
|
email:
|
|
107
68
|
- jakub.suder@gmail.com
|
|
108
69
|
executables: []
|
|
@@ -113,12 +74,9 @@ files:
|
|
|
113
74
|
- LICENSE.txt
|
|
114
75
|
- README.md
|
|
115
76
|
- lib/skyfall.rb
|
|
116
|
-
- lib/skyfall/car_archive.rb
|
|
117
|
-
- lib/skyfall/cid.rb
|
|
118
77
|
- lib/skyfall/collection.rb
|
|
119
78
|
- lib/skyfall/errors.rb
|
|
120
79
|
- lib/skyfall/events.rb
|
|
121
|
-
- lib/skyfall/extensions.rb
|
|
122
80
|
- lib/skyfall/firehose.rb
|
|
123
81
|
- lib/skyfall/firehose/account_message.rb
|
|
124
82
|
- lib/skyfall/firehose/commit_message.rb
|
|
@@ -161,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
161
119
|
- !ruby/object:Gem::Version
|
|
162
120
|
version: '0'
|
|
163
121
|
requirements: []
|
|
164
|
-
rubygems_version:
|
|
122
|
+
rubygems_version: 3.6.9
|
|
165
123
|
specification_version: 4
|
|
166
124
|
summary: A Ruby gem for streaming data from the Bluesky/ATProto firehose
|
|
167
125
|
test_files: []
|
data/lib/skyfall/car_archive.rb
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'cid'
|
|
4
|
-
require_relative 'errors'
|
|
5
|
-
require_relative 'extensions'
|
|
6
|
-
|
|
7
|
-
require 'base64'
|
|
8
|
-
require 'cbor'
|
|
9
|
-
require 'stringio'
|
|
10
|
-
|
|
11
|
-
# CAR v1: https://ipld.io/specs/transport/car/carv1/
|
|
12
|
-
# multicodec codes: https://github.com/multiformats/multicodec/blob/master/table.csv
|
|
13
|
-
|
|
14
|
-
module Skyfall
|
|
15
|
-
class CarSection
|
|
16
|
-
attr_reader :cid
|
|
17
|
-
|
|
18
|
-
def initialize(cid, body_data)
|
|
19
|
-
@cid = cid
|
|
20
|
-
@body_data = body_data
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def body
|
|
24
|
-
@body ||= CarArchive.convert_data(CBOR.decode(@body_data))
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
class CarArchive
|
|
29
|
-
using Skyfall::Extensions
|
|
30
|
-
|
|
31
|
-
attr_reader :roots, :sections
|
|
32
|
-
|
|
33
|
-
def initialize(data)
|
|
34
|
-
@sections = []
|
|
35
|
-
@buffer = StringIO.new(data)
|
|
36
|
-
|
|
37
|
-
read_header(@buffer)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def section_with_cid(cid)
|
|
41
|
-
if section = @sections.detect { |s| s.cid == cid }
|
|
42
|
-
return section.body
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
if @buffer
|
|
46
|
-
while !@buffer.eof?
|
|
47
|
-
section = read_section(@buffer)
|
|
48
|
-
return section.body if section.cid == cid
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
@buffer = nil
|
|
53
|
-
nil
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def sections
|
|
57
|
-
if @buffer
|
|
58
|
-
read_section(@buffer) while !@buffer.eof?
|
|
59
|
-
@buffer = nil
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
@sections
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def self.convert_data(object)
|
|
66
|
-
if object.is_a?(Hash)
|
|
67
|
-
object.each do |k, v|
|
|
68
|
-
if v.is_a?(Hash) || v.is_a?(Array)
|
|
69
|
-
convert_data(v)
|
|
70
|
-
elsif v.is_a?(CBOR::Tagged)
|
|
71
|
-
object[k] = make_cid_link(v)
|
|
72
|
-
elsif v.is_a?(String) && v.encoding == Encoding::ASCII_8BIT
|
|
73
|
-
object[k] = make_bytes(v)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
elsif object.is_a?(Array)
|
|
77
|
-
object.each_with_index do |v, i|
|
|
78
|
-
if v.is_a?(Hash) || v.is_a?(Array)
|
|
79
|
-
convert_data(v)
|
|
80
|
-
elsif v.is_a?(CBOR::Tagged)
|
|
81
|
-
object[i] = make_cid_link(v)
|
|
82
|
-
elsif v.is_a?(String) && v.encoding == Encoding::ASCII_8BIT
|
|
83
|
-
object[i] = make_bytes(v)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
else
|
|
87
|
-
raise DecodeError, "Unexpected value type in record: #{object}"
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def self.make_cid_link(cid)
|
|
92
|
-
{ '$link' => CID.from_cbor_tag(cid) }
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def self.make_bytes(data)
|
|
96
|
-
{ '$bytes' => Base64.encode64(data).chomp.gsub(/=+$/, '') }
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def inspect
|
|
100
|
-
vars = instance_variables.map { |v|
|
|
101
|
-
if v == :@sections && @buffer
|
|
102
|
-
"#{v}=[...]"
|
|
103
|
-
else
|
|
104
|
-
"#{v}=#{instance_variable_get(v).inspect}"
|
|
105
|
-
end
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
"#<#{self.class}:0x#{object_id} #{vars.join(", ")}>"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
def read_header(buffer)
|
|
114
|
-
len = buffer.read_varint
|
|
115
|
-
|
|
116
|
-
header_data = buffer.read(len)
|
|
117
|
-
raise DecodeError.new("Header too short: #{header_data}") unless header_data.length == len
|
|
118
|
-
|
|
119
|
-
header = CBOR.decode(header_data)
|
|
120
|
-
raise UnsupportedError.new("Unexpected CAR version: #{header['version']}") unless header['version'] == 1
|
|
121
|
-
@roots = header['roots'].map { |x| CID.from_cbor_tag(x) }
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def read_section(buffer)
|
|
125
|
-
len = buffer.read_varint
|
|
126
|
-
|
|
127
|
-
section_data = buffer.read(len)
|
|
128
|
-
raise DecodeError.new("Section too short: #{section_data}") unless section_data.length == len
|
|
129
|
-
|
|
130
|
-
sbuffer = StringIO.new(section_data)
|
|
131
|
-
|
|
132
|
-
version = sbuffer.read_varint
|
|
133
|
-
raise UnsupportedError.new("Unexpected CID version: #{version}") unless version == 1
|
|
134
|
-
|
|
135
|
-
codec = sbuffer.read_varint
|
|
136
|
-
raise UnsupportedError.new("Unexpected CID codec: #{codec}") unless codec == 0x71 # dag-cbor
|
|
137
|
-
|
|
138
|
-
hash = sbuffer.read_varint
|
|
139
|
-
raise UnsupportedError.new("Unexpected CID hash: #{hash}") unless hash == 0x12 # sha2-256
|
|
140
|
-
|
|
141
|
-
clen = sbuffer.read_varint
|
|
142
|
-
raise UnsupportedError.new("Unexpected CID length: #{clen}") unless clen == 32
|
|
143
|
-
|
|
144
|
-
prefix = section_data[0...sbuffer.pos]
|
|
145
|
-
|
|
146
|
-
cid_data = sbuffer.read(clen)
|
|
147
|
-
raise DecodeError.new("CID too short: #{cid_data}") unless cid_data.length == clen
|
|
148
|
-
|
|
149
|
-
cid = CID.new(prefix + cid_data)
|
|
150
|
-
|
|
151
|
-
body_data = sbuffer.read
|
|
152
|
-
new_section = CarSection.new(cid, body_data)
|
|
153
|
-
|
|
154
|
-
@sections << new_section
|
|
155
|
-
new_section
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
data/lib/skyfall/cid.rb
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'errors'
|
|
4
|
-
|
|
5
|
-
require 'base32'
|
|
6
|
-
|
|
7
|
-
# CIDs in DAG-CBOR: https://ipld.io/specs/codecs/dag-cbor/spec/
|
|
8
|
-
# CIDs in JSON: https://ipld.io/specs/codecs/dag-json/spec/
|
|
9
|
-
# multibase: https://github.com/multiformats/multibase
|
|
10
|
-
|
|
11
|
-
module Skyfall
|
|
12
|
-
class CID
|
|
13
|
-
attr_reader :data
|
|
14
|
-
|
|
15
|
-
def self.from_cbor_tag(tag)
|
|
16
|
-
data = tag.value
|
|
17
|
-
raise DecodeError.new("Unexpected first byte of CID: #{data[0]}") unless data[0] == "\x00"
|
|
18
|
-
CID.new(data[1..-1])
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def self.from_json(string)
|
|
22
|
-
raise DecodeError.new("Unexpected CID length") unless string.length == 59
|
|
23
|
-
raise DecodeError.new("Unexpected CID prefix") unless string[0] == 'b'
|
|
24
|
-
|
|
25
|
-
data = Base32.decode(string[1..-1].upcase)
|
|
26
|
-
CID.new(data)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def initialize(data)
|
|
30
|
-
@data = data
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def to_s
|
|
34
|
-
'b' + Base32.encode(@data).downcase.gsub(/=+$/, '')
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def inspect
|
|
38
|
-
"CID(\"#{to_s}\")"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def ==(other)
|
|
42
|
-
other.is_a?(CID) && @data == other.data
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
data/lib/skyfall/extensions.rb
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'cbor'
|
|
4
|
-
require 'stringio'
|
|
5
|
-
|
|
6
|
-
module Skyfall
|
|
7
|
-
|
|
8
|
-
# @private
|
|
9
|
-
module Extensions
|
|
10
|
-
|
|
11
|
-
refine StringIO do
|
|
12
|
-
# https://en.wikipedia.org/wiki/LEB128
|
|
13
|
-
def read_varint
|
|
14
|
-
shift = 1
|
|
15
|
-
value = 0
|
|
16
|
-
|
|
17
|
-
loop do
|
|
18
|
-
byte = self.readbyte
|
|
19
|
-
value += byte % 128 * shift
|
|
20
|
-
break if byte < 128
|
|
21
|
-
shift *= 128
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
value
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
refine CBOR.singleton_class do
|
|
29
|
-
def decode_sequence(data)
|
|
30
|
-
unpacker = CBOR::Unpacker.new(StringIO.new(data))
|
|
31
|
-
unpacker.each.to_a
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
end
|
|
36
|
-
end
|