skyfall 0.2.3 → 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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +46 -11
- data/example/block_tracker.rb +84 -0
- data/example/monitor_phrases.rb +53 -0
- data/example/print_all_posts.rb +34 -0
- data/example/push_notifications.rb +262 -0
- data/lib/skyfall/collection.rb +1 -0
- data/lib/skyfall/label.rb +63 -0
- data/lib/skyfall/messages/identity_message.rb +4 -0
- data/lib/skyfall/messages/labels_message.rb +23 -0
- data/lib/skyfall/messages/websocket_message.rb +4 -0
- data/lib/skyfall/operation.rb +12 -0
- data/lib/skyfall/stream.rb +61 -42
- data/lib/skyfall/version.rb +1 -1
- metadata +87 -5
- data/example/firehose.rb +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34e2166dc123a74140ce5cae67d3c000c1f88972d15cbe8a2550bde11ba4034d
|
|
4
|
+
data.tar.gz: 334f4b8b0ddce03b2258a0266e01e08cac9528924ac13380e91b66a0bb1217b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f56ab132941d80a577e5f3ba630b58d6f99548d0cde398fdb4f15e7c24d9557c3b0243d64a3940c18bf370afb219df3282c935300fc0359b6b0e00915b7002ca
|
|
7
|
+
data.tar.gz: 4bd7672a9450d8b1ec80ec679b69e6ba4242c4a6554865e296e7a347769795239b99a95539fe0b637407c2736b0716dfc8d1c3a9dea2d8d7a9654b47158262c9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## [0.3.0] - 2024-03-21
|
|
2
|
+
|
|
3
|
+
- added support for labeller firehose, served by labeller services at the `com.atproto.label.subscribeLabels` endpoint (aliased as `:subscribe_labels`)
|
|
4
|
+
- the `#labels` messages from the labeller firehose are parsed into a `LabelsMessage`, which includes a `labels` array of `Label` objects
|
|
5
|
+
- `Stream` callbacks can now also be assigned via setters, e.g. `stream.on_message = proc { ... }`
|
|
6
|
+
- added default error handler to `Stream` which logs the error to `$stdout` - set `stream.on_error = nil` to disable
|
|
7
|
+
- added Ruby stdlib dependencies explicitly to the gemspec - fixes a warning in Ruby 3.3 when requiring `base64`, which will be extracted as an optional gem in 3.4
|
|
8
|
+
|
|
9
|
+
## [0.2.5] - 2024-03-14
|
|
10
|
+
|
|
11
|
+
- added `:bsky_labeler` record type symbol & collection constant
|
|
12
|
+
|
|
13
|
+
## [0.2.4] - 2024-02-27
|
|
14
|
+
|
|
15
|
+
- added support for `#identity` message type
|
|
16
|
+
- added `Operation#did` as an alias of `#repo`
|
|
17
|
+
- added `Stream#reconnect` method which forces the websocket to reconnect
|
|
18
|
+
- added some validation for the `cursor` parameter in `Stream` initializer
|
|
19
|
+
- the `server` parameter in `Stream` initializer can be a full URL with scheme, which lets you connect to e.g. `ws://localhost` (since by default, `wss://` is used)
|
|
20
|
+
- tweaked `#inspect` output of `Stream` and `Operation`
|
|
21
|
+
|
|
1
22
|
## [0.2.3] - 2023-09-28
|
|
2
23
|
|
|
3
24
|
- fixed encoding of image CIDs again (they should be wrapped in a `$link` object)
|
data/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Start a connection to the firehose by creating a `Skyfall::Stream` object, passi
|
|
|
22
22
|
```rb
|
|
23
23
|
require 'skyfall'
|
|
24
24
|
|
|
25
|
-
sky = Skyfall::Stream.new('bsky.
|
|
25
|
+
sky = Skyfall::Stream.new('bsky.network', :subscribe_repos)
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Add event listeners to handle incoming messages and get notified of errors:
|
|
@@ -44,27 +44,53 @@ sky.connect
|
|
|
44
44
|
|
|
45
45
|
### Processing messages
|
|
46
46
|
|
|
47
|
-
Each message passed to `on_message` is an instance of
|
|
47
|
+
Each message passed to `on_message` is an instance of a subclass of `WebsocketMessage`, depending on the message type. The supported message types are:
|
|
48
|
+
|
|
49
|
+
- `CommitMessage` (`#commit`) - represents a change in a user's repo; most messages are of this type
|
|
50
|
+
- `HandleMessage` (`#handle`) - when a different handle is assigned to a user's DID
|
|
51
|
+
- `TombstoneMessage` (`#tombstone`) - when an account is deleted
|
|
52
|
+
- `InfoMessage` (`#info`) - a protocol error message, e.g. about an invalid cursor parameter
|
|
53
|
+
- `UnknownMessage` is used for other unrecognized message types
|
|
54
|
+
|
|
55
|
+
All message objects have the following properties:
|
|
56
|
+
|
|
57
|
+
- `type` (symbol) - the message type identifier, e.g. `:commit`
|
|
58
|
+
- `seq` (integer) - a sequential index of the message
|
|
59
|
+
- `repo` or `did` (string) - DID of the repository (user account)
|
|
60
|
+
- `time` (Time) - timestamp of the described action
|
|
61
|
+
|
|
62
|
+
All properties except `type` may be nil for some message types that aren't related to a specific user, like `#info`.
|
|
63
|
+
|
|
64
|
+
Commit messages additionally have:
|
|
48
65
|
|
|
49
|
-
- `type` (symbol) - usually `:commit`
|
|
50
|
-
- `seq` (sequential number)
|
|
51
|
-
- `time` (Time)
|
|
52
|
-
- `repo` (string) - DID of the repository (user account)
|
|
53
66
|
- `commit` - CID of the commit
|
|
54
67
|
- `prev` - CID of the previous commit in that repo
|
|
55
68
|
- `operations` - list of operations (usually one)
|
|
56
69
|
|
|
70
|
+
Handle messages additionally have:
|
|
71
|
+
|
|
72
|
+
- `handle` - the new handle assigned to the DID
|
|
73
|
+
|
|
74
|
+
Info messages additionally have:
|
|
75
|
+
|
|
76
|
+
- `name` - identifier of the message/error
|
|
77
|
+
- `message` - a human-readable description
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
### Commit operations
|
|
81
|
+
|
|
57
82
|
Operations are objects of type `Operation` and have such properties:
|
|
58
83
|
|
|
59
|
-
- `repo` (string) - DID of the repository (user account)
|
|
84
|
+
- `repo` or `did` (string) - DID of the repository (user account)
|
|
60
85
|
- `collection` (string) - name of the relevant collection in the repository, e.g. `app.bsky.feed.post` for posts
|
|
86
|
+
- `type` (symbol) - short name of the collection, e.g. `:bsky_post`
|
|
87
|
+
- `rkey` (string) - identifier of a record in a collection
|
|
61
88
|
- `path` (string) - the path part of the at:// URI - collection name + ID (rkey) of the item
|
|
89
|
+
- `uri` (string) - the complete at:// URI
|
|
62
90
|
- `action` (symbol) - `:create`, `:update` or `:delete`
|
|
63
|
-
- `uri` (string) - the at:// URI
|
|
64
|
-
- `type` (symbol) - short name of the collection, e.g. `:bsky_post`
|
|
65
91
|
- `cid` - CID of the operation/record (`nil` for delete operations)
|
|
66
92
|
|
|
67
|
-
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
|
|
93
|
+
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).
|
|
68
94
|
|
|
69
95
|
So for example, in order to filter only "create post" operations and print their details, you can do something like this:
|
|
70
96
|
|
|
@@ -82,7 +108,16 @@ sky.on_message do |m|
|
|
|
82
108
|
end
|
|
83
109
|
```
|
|
84
110
|
|
|
85
|
-
|
|
111
|
+
For more examples, see the [example](https://github.com/mackuba/skyfall/blob/master/example) folder or the [bluesky-feeds-rb](https://github.com/mackuba/bluesky-feeds-rb/blob/master/app/firehose_stream.rb) project, which implements a feed generator service.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
### Custom lexicons
|
|
115
|
+
|
|
116
|
+
A note on custom lexicons: the `Skyfall::Operation` objects have two properties that tell you the kind of record they're about: `#collection`, which is a string containing the official name of the collection/lexicon, e.g. `"app.bsky.feed.post"`; and `#type`, which is a symbol meant to save you some typing, e.g. `:bsky_post`.
|
|
117
|
+
|
|
118
|
+
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.
|
|
119
|
+
|
|
120
|
+
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.
|
|
86
121
|
|
|
87
122
|
|
|
88
123
|
## Credits
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Example: monitor the network for people blocking your account or adding you to mute lists.
|
|
4
|
+
|
|
5
|
+
# load skyfall from a local folder - you normally won't need this
|
|
6
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
7
|
+
|
|
8
|
+
require 'json'
|
|
9
|
+
require 'open-uri'
|
|
10
|
+
require 'skyfall'
|
|
11
|
+
|
|
12
|
+
$monitored_did = ARGV[0]
|
|
13
|
+
|
|
14
|
+
if $monitored_did.to_s.empty?
|
|
15
|
+
puts "Usage: #{$PROGRAM_NAME} <monitored_did>"
|
|
16
|
+
exit 1
|
|
17
|
+
elsif ARGV[0] !~ /^did:plc:[a-z0-9]{24}$/
|
|
18
|
+
puts "Not a valid DID: #{$monitored_did}"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sky = Skyfall::Stream.new('bsky.network', :subscribe_repos)
|
|
23
|
+
|
|
24
|
+
sky.on_connect { puts "Connected, monitoring #{$monitored_did}" }
|
|
25
|
+
sky.on_disconnect { puts "Disconnected" }
|
|
26
|
+
sky.on_reconnect { puts "Reconnecting..." }
|
|
27
|
+
sky.on_error { |e| puts "ERROR: #{e}" }
|
|
28
|
+
|
|
29
|
+
sky.on_message do |msg|
|
|
30
|
+
# we're only interested in repo commit messages
|
|
31
|
+
next if msg.type != :commit
|
|
32
|
+
|
|
33
|
+
msg.operations.each do |op|
|
|
34
|
+
next if op.action != :create
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
case op.type
|
|
38
|
+
when :bsky_block
|
|
39
|
+
process_block(msg, op)
|
|
40
|
+
when :bsky_listitem
|
|
41
|
+
process_list_item(msg, op)
|
|
42
|
+
end
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
puts "Error: #{e}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def process_block(msg, op)
|
|
50
|
+
if op.raw_record['subject'] == $monitored_did
|
|
51
|
+
owner_handle = get_user_handle(op.repo)
|
|
52
|
+
puts "@#{owner_handle} has blocked you! (#{msg.time.getlocal})"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def process_list_item(msg, op)
|
|
57
|
+
if op.raw_record['subject'] == $monitored_did
|
|
58
|
+
owner_handle = get_user_handle(op.repo)
|
|
59
|
+
|
|
60
|
+
list_uri = op.raw_record['list']
|
|
61
|
+
list_name = get_list_name(list_uri)
|
|
62
|
+
|
|
63
|
+
puts "@#{owner_handle} has added you to list \"#{list_name}\" (#{msg.time.getlocal})"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_user_handle(did)
|
|
68
|
+
url = "https://plc.directory/#{did}"
|
|
69
|
+
json = JSON.parse(URI.open(url).read)
|
|
70
|
+
json['alsoKnownAs'][0].gsub('at://', '')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def get_list_name(list_uri)
|
|
74
|
+
repo, type, rkey = list_uri.gsub('at://', '').split('/')
|
|
75
|
+
url = "https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=#{repo}&collection=#{type}&rkey=#{rkey}"
|
|
76
|
+
|
|
77
|
+
json = JSON.parse(URI.open(url).read)
|
|
78
|
+
json['value']['name']
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# close the connection cleanly on Ctrl+C
|
|
82
|
+
trap("SIGINT") { sky.disconnect }
|
|
83
|
+
|
|
84
|
+
sky.connect
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Example: monitor new posts for mentions of one or more words or phrases (e.g. anyone mentioning your name or the name
|
|
4
|
+
# of your company, project etc.).
|
|
5
|
+
|
|
6
|
+
# load skyfall from a local folder - you normally won't need this
|
|
7
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
8
|
+
|
|
9
|
+
require 'json'
|
|
10
|
+
require 'open-uri'
|
|
11
|
+
require 'skyfall'
|
|
12
|
+
|
|
13
|
+
terms = ARGV.map(&:downcase)
|
|
14
|
+
|
|
15
|
+
if terms.empty?
|
|
16
|
+
puts "Usage: #{$PROGRAM_NAME} <word_or_phrase> [<word_or_phrase>...]"
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sky = Skyfall::Stream.new('bsky.network', :subscribe_repos)
|
|
21
|
+
|
|
22
|
+
sky.on_message do |msg|
|
|
23
|
+
# we're only interested in repo commit messages
|
|
24
|
+
next if msg.type != :commit
|
|
25
|
+
|
|
26
|
+
msg.operations.each do |op|
|
|
27
|
+
# ignore any operations other than "create post"
|
|
28
|
+
next unless op.action == :create && op.type == :bsky_post
|
|
29
|
+
|
|
30
|
+
text = op.raw_record['text'].to_s.downcase
|
|
31
|
+
|
|
32
|
+
if terms.any? { |x| text.include?(x) }
|
|
33
|
+
owner_handle = get_user_handle(op.repo)
|
|
34
|
+
puts "\n#{msg.time.getlocal} @#{owner_handle}: #{op.raw_record['text']}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def get_user_handle(did)
|
|
40
|
+
url = "https://plc.directory/#{did}"
|
|
41
|
+
json = JSON.parse(URI.open(url).read)
|
|
42
|
+
json['alsoKnownAs'][0].gsub('at://', '')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sky.on_connect { puts "Connected" }
|
|
46
|
+
sky.on_disconnect { puts "Disconnected" }
|
|
47
|
+
sky.on_reconnect { puts "Reconnecting..." }
|
|
48
|
+
sky.on_error { |e| puts "ERROR: #{e}" }
|
|
49
|
+
|
|
50
|
+
# close the connection cleanly on Ctrl+C
|
|
51
|
+
trap("SIGINT") { sky.disconnect }
|
|
52
|
+
|
|
53
|
+
sky.connect
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Example: print the date and text of every new post made on the network as they appear.
|
|
4
|
+
|
|
5
|
+
# load skyfall from a local folder - you normally won't need this
|
|
6
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
7
|
+
|
|
8
|
+
require 'skyfall'
|
|
9
|
+
|
|
10
|
+
sky = Skyfall::Stream.new('bsky.network', :subscribe_repos)
|
|
11
|
+
|
|
12
|
+
sky.on_message do |msg|
|
|
13
|
+
# we're only interested in repo commit messages
|
|
14
|
+
next if msg.type != :commit
|
|
15
|
+
|
|
16
|
+
msg.operations.each do |op|
|
|
17
|
+
# ignore any operations other than "create post"
|
|
18
|
+
next unless op.action == :create && op.type == :bsky_post
|
|
19
|
+
|
|
20
|
+
puts "#{op.repo} • #{msg.time.getlocal}"
|
|
21
|
+
puts op.raw_record['text']
|
|
22
|
+
puts
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sky.on_connect { puts "Connected" }
|
|
27
|
+
sky.on_disconnect { puts "Disconnected" }
|
|
28
|
+
sky.on_reconnect { puts "Reconnecting..." }
|
|
29
|
+
sky.on_error { |e| puts "ERROR: #{e}" }
|
|
30
|
+
|
|
31
|
+
# close the connection cleanly on Ctrl+C
|
|
32
|
+
trap("SIGINT") { sky.disconnect }
|
|
33
|
+
|
|
34
|
+
sky.connect
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Example: send push notifications to a client app about interactions with a given account.
|
|
4
|
+
|
|
5
|
+
# load skyfall from a local folder - you normally won't need this
|
|
6
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
7
|
+
|
|
8
|
+
require 'json'
|
|
9
|
+
require 'open-uri'
|
|
10
|
+
require 'skyfall'
|
|
11
|
+
|
|
12
|
+
monitored_did = ARGV[0]
|
|
13
|
+
|
|
14
|
+
if monitored_did.to_s.empty?
|
|
15
|
+
puts "Usage: #{$PROGRAM_NAME} <monitored_did>"
|
|
16
|
+
exit 1
|
|
17
|
+
elsif monitored_did !~ /^did:plc:[a-z0-9]{24}$/
|
|
18
|
+
puts "Not a valid DID: #{monitored_did}"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class InvalidURIException < StandardError
|
|
23
|
+
def initialize(uri)
|
|
24
|
+
super("Invalid AT URI: #{uri}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class AtURI
|
|
29
|
+
attr_reader :did, :collection, :rkey
|
|
30
|
+
|
|
31
|
+
def initialize(uri)
|
|
32
|
+
if uri =~ /\Aat:\/\/(did:[\w]+:[\w\.\-]+)\/([\w\.]+)\/([\w\-]+)\z/
|
|
33
|
+
@did = $1
|
|
34
|
+
@collection = $2
|
|
35
|
+
@rkey = $3
|
|
36
|
+
else
|
|
37
|
+
raise InvalidURIException, uri
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class NotificationEngine
|
|
43
|
+
def initialize(user_did)
|
|
44
|
+
@user_did = user_did
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def connect
|
|
48
|
+
@sky = Skyfall::Stream.new('bsky.network', :subscribe_repos)
|
|
49
|
+
|
|
50
|
+
@sky.on_connect { puts "Connected, monitoring #{@user_did}" }
|
|
51
|
+
@sky.on_disconnect { puts "Disconnected" }
|
|
52
|
+
@sky.on_reconnect { puts "Reconnecting..." }
|
|
53
|
+
@sky.on_error { |e| puts "ERROR: #{e}" }
|
|
54
|
+
|
|
55
|
+
@sky.on_message do |msg|
|
|
56
|
+
process_message(msg)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@sky.connect
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def disconnect
|
|
63
|
+
@sky.disconnect
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def process_message(msg)
|
|
67
|
+
# we're only interested in repo commit messages
|
|
68
|
+
return if msg.type != :commit
|
|
69
|
+
|
|
70
|
+
# ignore user's own actions
|
|
71
|
+
return if msg.repo == @user_did
|
|
72
|
+
|
|
73
|
+
msg.operations.each do |op|
|
|
74
|
+
next if op.action != :create
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
case op.type
|
|
78
|
+
when :bsky_post
|
|
79
|
+
process_post(msg, op)
|
|
80
|
+
when :bsky_like
|
|
81
|
+
process_like(msg, op)
|
|
82
|
+
when :bsky_repost
|
|
83
|
+
process_repost(msg, op)
|
|
84
|
+
when :bsky_follow
|
|
85
|
+
process_follow(msg, op)
|
|
86
|
+
end
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
puts "Error: #{e}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# posts
|
|
95
|
+
|
|
96
|
+
def process_post(msg, op)
|
|
97
|
+
data = op.raw_record
|
|
98
|
+
|
|
99
|
+
if reply = data['reply']
|
|
100
|
+
# check for replies (direct only)
|
|
101
|
+
if reply['parent'] && reply['parent']['uri']
|
|
102
|
+
parent_uri = AtURI.new(reply['parent']['uri'])
|
|
103
|
+
|
|
104
|
+
if parent_uri.did == @user_did
|
|
105
|
+
send_reply_notification(msg, op)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if embed = data['embed']
|
|
111
|
+
# check for quotes
|
|
112
|
+
if embed['record'] && embed['record']['uri']
|
|
113
|
+
quoted_uri = AtURI.new(embed['record']['uri'])
|
|
114
|
+
|
|
115
|
+
if quoted_uri.did == @user_did
|
|
116
|
+
send_quote_notification(msg, op)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# second type of quote (recordWithMedia)
|
|
121
|
+
if embed['record'] && embed['record']['record'] && embed['record']['record']['uri']
|
|
122
|
+
quoted_uri = AtURI.new(embed['record']['record']['uri'])
|
|
123
|
+
|
|
124
|
+
if quoted_uri.did == @user_did
|
|
125
|
+
send_quote_notification(msg, op)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if facets = data['facets']
|
|
131
|
+
# check for mentions
|
|
132
|
+
if facets.any? { |f| f['features'] && f['features'].any? { |x| x['did'] == @user_did }}
|
|
133
|
+
send_mention_notification(msg, op)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def send_reply_notification(msg, op)
|
|
139
|
+
handle = get_user_handle(msg.repo)
|
|
140
|
+
|
|
141
|
+
send_push("@#{handle} replied:", op.raw_record)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def send_quote_notification(msg, op)
|
|
145
|
+
handle = get_user_handle(msg.repo)
|
|
146
|
+
|
|
147
|
+
send_push("@#{handle} quoted you:", op.raw_record)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def send_mention_notification(msg, op)
|
|
151
|
+
handle = get_user_handle(msg.repo)
|
|
152
|
+
|
|
153
|
+
send_push("@#{handle} mentioned you:", op.raw_record)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# likes
|
|
158
|
+
|
|
159
|
+
def process_like(msg, op)
|
|
160
|
+
data = op.raw_record
|
|
161
|
+
|
|
162
|
+
if data['subject'] && data['subject']['uri']
|
|
163
|
+
liked_uri = AtURI.new(data['subject']['uri'])
|
|
164
|
+
|
|
165
|
+
if liked_uri.did == @user_did
|
|
166
|
+
case liked_uri.collection
|
|
167
|
+
when 'app.bsky.feed.post'
|
|
168
|
+
send_post_like_notification(msg, liked_uri)
|
|
169
|
+
when 'app.bsky.feed.generator'
|
|
170
|
+
send_feed_like_notification(msg, liked_uri)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def send_post_like_notification(msg, uri)
|
|
177
|
+
handle = get_user_handle(msg.repo)
|
|
178
|
+
post = get_record(uri)
|
|
179
|
+
|
|
180
|
+
send_push("@#{handle} liked your post", post)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def send_feed_like_notification(msg, uri)
|
|
184
|
+
handle = get_user_handle(msg.repo)
|
|
185
|
+
feed = get_record(uri)
|
|
186
|
+
|
|
187
|
+
send_push("@#{handle} liked your feed", feed)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# reposts
|
|
192
|
+
|
|
193
|
+
def process_repost(msg, op)
|
|
194
|
+
data = op.raw_record
|
|
195
|
+
|
|
196
|
+
if data['subject'] && data['subject']['uri']
|
|
197
|
+
reposted_uri = AtURI.new(data['subject']['uri'])
|
|
198
|
+
|
|
199
|
+
if reposted_uri.did == @user_did && reposted_uri.collection == 'app.bsky.feed.post'
|
|
200
|
+
send_repost_notification(msg, reposted_uri)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def send_repost_notification(msg, uri)
|
|
206
|
+
handle = get_user_handle(msg.repo)
|
|
207
|
+
post = get_record(uri)
|
|
208
|
+
|
|
209
|
+
send_push("@#{handle} reposted your post", post)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# follows
|
|
214
|
+
|
|
215
|
+
def process_follow(msg, op)
|
|
216
|
+
if op.raw_record['subject'] == @user_did
|
|
217
|
+
send_follow_notification(msg)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def send_follow_notification(msg)
|
|
222
|
+
handle = get_user_handle(msg.repo)
|
|
223
|
+
|
|
224
|
+
send_push("@#{handle} followed you", msg.repo)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
#
|
|
229
|
+
# Note: in this example, we're calling the Bluesky AppView to get details about the person interacting with the user
|
|
230
|
+
# and the post/feed that was liked/reposted etc. In a real app, you might run into rate limits if you do that,
|
|
231
|
+
# because these requests will all be sent from the server's IP.
|
|
232
|
+
#
|
|
233
|
+
# So you might need to take a different route and send just the info that you have here in the push notification data
|
|
234
|
+
# (the AT URI / DID) and fetch the details on the client side, e.g. in a Notification Service Extension on iOS.
|
|
235
|
+
#
|
|
236
|
+
|
|
237
|
+
def get_user_handle(did)
|
|
238
|
+
url = "https://api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=#{did}"
|
|
239
|
+
json = JSON.parse(URI.open(url).read)
|
|
240
|
+
json['handle']
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def get_record(uri)
|
|
244
|
+
url = "https://api.bsky.app/xrpc/com.atproto.repo.getRecord?" +
|
|
245
|
+
"repo=#{uri.did}&collection=#{uri.collection}&rkey=#{uri.rkey}"
|
|
246
|
+
json = JSON.parse(URI.open(url).read)
|
|
247
|
+
json['value']
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def send_push(message, data = nil)
|
|
251
|
+
# send the message to APNS/FCM here
|
|
252
|
+
puts
|
|
253
|
+
puts "[#{Time.now}] #{message} #{data&.inspect}"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
engine = NotificationEngine.new(monitored_did)
|
|
258
|
+
|
|
259
|
+
# close the connection cleanly on Ctrl+C
|
|
260
|
+
trap("SIGINT") { engine.disconnect }
|
|
261
|
+
|
|
262
|
+
engine.connect
|
data/lib/skyfall/collection.rb
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require_relative 'errors'
|
|
2
|
+
require 'time'
|
|
3
|
+
|
|
4
|
+
module Skyfall
|
|
5
|
+
class Label
|
|
6
|
+
attr_reader :data
|
|
7
|
+
|
|
8
|
+
def initialize(data)
|
|
9
|
+
@data = data
|
|
10
|
+
|
|
11
|
+
raise DecodeError.new("Missing version: #{data}") unless data.has_key?('ver')
|
|
12
|
+
raise DecodeError.new("Invalid version: #{ver}") unless ver.is_a?(Integer) && ver >= 1
|
|
13
|
+
raise UnsupportedError.new("Unsupported version: #{ver}") unless ver == 1
|
|
14
|
+
|
|
15
|
+
raise DecodeError.new("Missing source: #{data}") unless data.has_key?('src')
|
|
16
|
+
raise DecodeError.new("Invalid source: #{src}") unless src.is_a?(String) && src.start_with?('did:')
|
|
17
|
+
|
|
18
|
+
raise DecodeError.new("Missing uri: #{data}") unless data.has_key?('uri')
|
|
19
|
+
raise DecodeError.new("Invalid uri: #{uri}") unless uri.is_a?(String)
|
|
20
|
+
raise DecodeError.new("Invalid uri: #{uri}") unless uri.start_with?('at://') || uri.start_with?('did:')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def version
|
|
24
|
+
@data['ver']
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def authority
|
|
28
|
+
@data['src']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def subject
|
|
32
|
+
@data['uri']
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cid
|
|
36
|
+
@cid ||= @data['cid'] && CID.from_json(@data['cid'])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def value
|
|
40
|
+
@data['val']
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def negation?
|
|
44
|
+
!!@data['neg']
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def created_at
|
|
48
|
+
@created_at ||= Time.parse(@data['cts'])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def expires_at
|
|
52
|
+
@expires_at ||= @data['exp'] && Time.parse(@data['exp'])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
alias ver version
|
|
56
|
+
alias src authority
|
|
57
|
+
alias uri subject
|
|
58
|
+
alias val value
|
|
59
|
+
alias neg negation?
|
|
60
|
+
alias cts created_at
|
|
61
|
+
alias exp expires_at
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require_relative 'websocket_message'
|
|
2
|
+
require_relative '../label'
|
|
3
|
+
|
|
4
|
+
module Skyfall
|
|
5
|
+
class LabelsMessage
|
|
6
|
+
using Skyfall::Extensions
|
|
7
|
+
|
|
8
|
+
attr_reader :type_object, :data_object
|
|
9
|
+
attr_reader :type, :seq
|
|
10
|
+
|
|
11
|
+
def initialize(type_object, data_object)
|
|
12
|
+
@type_object = type_object
|
|
13
|
+
@data_object = data_object
|
|
14
|
+
|
|
15
|
+
@type = @type_object['t'][1..-1].to_sym
|
|
16
|
+
@seq = @data_object['seq']
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def labels
|
|
20
|
+
@labels ||= @data_object['labels'].map { |x| Label.new(x) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -10,7 +10,9 @@ module Skyfall
|
|
|
10
10
|
|
|
11
11
|
require_relative 'commit_message'
|
|
12
12
|
require_relative 'handle_message'
|
|
13
|
+
require_relative 'identity_message'
|
|
13
14
|
require_relative 'info_message'
|
|
15
|
+
require_relative 'labels_message'
|
|
14
16
|
require_relative 'tombstone_message'
|
|
15
17
|
require_relative 'unknown_message'
|
|
16
18
|
|
|
@@ -25,7 +27,9 @@ module Skyfall
|
|
|
25
27
|
message_class = case type_object['t']
|
|
26
28
|
when '#commit' then CommitMessage
|
|
27
29
|
when '#handle' then HandleMessage
|
|
30
|
+
when '#identity' then IdentityMessage
|
|
28
31
|
when '#info' then InfoMessage
|
|
32
|
+
when '#labels' then LabelsMessage
|
|
29
33
|
when '#tombstone' then TombstoneMessage
|
|
30
34
|
else UnknownMessage
|
|
31
35
|
end
|
data/lib/skyfall/operation.rb
CHANGED
|
@@ -11,6 +11,8 @@ module Skyfall
|
|
|
11
11
|
@message.repo
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
alias did repo
|
|
15
|
+
|
|
14
16
|
def path
|
|
15
17
|
@json['path']
|
|
16
18
|
end
|
|
@@ -44,6 +46,7 @@ module Skyfall
|
|
|
44
46
|
when Collection::BSKY_BLOCK then :bsky_block
|
|
45
47
|
when Collection::BSKY_FEED then :bsky_feed
|
|
46
48
|
when Collection::BSKY_FOLLOW then :bsky_follow
|
|
49
|
+
when Collection::BSKY_LABELER then :bsky_labeler
|
|
47
50
|
when Collection::BSKY_LIKE then :bsky_like
|
|
48
51
|
when Collection::BSKY_LIST then :bsky_list
|
|
49
52
|
when Collection::BSKY_LISTBLOCK then :bsky_listblock
|
|
@@ -55,5 +58,14 @@ module Skyfall
|
|
|
55
58
|
else :unknown
|
|
56
59
|
end
|
|
57
60
|
end
|
|
61
|
+
|
|
62
|
+
def inspectable_variables
|
|
63
|
+
instance_variables - [:@message]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def inspect
|
|
67
|
+
vars = inspectable_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
|
|
68
|
+
"#<#{self.class}:0x#{object_id} #{vars}>"
|
|
69
|
+
end
|
|
58
70
|
end
|
|
59
71
|
end
|
data/lib/skyfall/stream.rb
CHANGED
|
@@ -7,26 +7,28 @@ require 'uri'
|
|
|
7
7
|
module Skyfall
|
|
8
8
|
class Stream
|
|
9
9
|
SUBSCRIBE_REPOS = "com.atproto.sync.subscribeRepos"
|
|
10
|
+
SUBSCRIBE_LABELS = "com.atproto.label.subscribeLabels"
|
|
10
11
|
|
|
11
12
|
NAMED_ENDPOINTS = {
|
|
12
|
-
:subscribe_repos => SUBSCRIBE_REPOS
|
|
13
|
+
:subscribe_repos => SUBSCRIBE_REPOS,
|
|
14
|
+
:subscribe_labels => SUBSCRIBE_LABELS
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
EVENTS = %w(message raw_message connecting connect disconnect reconnect error)
|
|
18
|
+
|
|
15
19
|
MAX_RECONNECT_INTERVAL = 300
|
|
16
20
|
|
|
17
21
|
attr_accessor :heartbeat_timeout, :heartbeat_interval, :cursor, :auto_reconnect
|
|
18
22
|
|
|
19
23
|
def initialize(server, endpoint, cursor = nil)
|
|
20
24
|
@endpoint = check_endpoint(endpoint)
|
|
21
|
-
@
|
|
22
|
-
@cursor = cursor
|
|
25
|
+
@root_url = build_root_url(server)
|
|
26
|
+
@cursor = check_cursor(cursor)
|
|
23
27
|
@handlers = {}
|
|
24
|
-
@heartbeat_mutex = Mutex.new
|
|
25
|
-
@heartbeat_interval = 5
|
|
26
|
-
@heartbeat_timeout = 30
|
|
27
|
-
@last_update = nil
|
|
28
28
|
@auto_reconnect = true
|
|
29
29
|
@connection_attempts = 0
|
|
30
|
+
|
|
31
|
+
@handlers[:error] = proc { |e| puts "ERROR: #{e}" }
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def connect
|
|
@@ -37,6 +39,9 @@ module Skyfall
|
|
|
37
39
|
@handlers[:connecting]&.call(url)
|
|
38
40
|
@engines_on = true
|
|
39
41
|
|
|
42
|
+
@reconnect_timer&.cancel
|
|
43
|
+
@reconnect_timer = nil
|
|
44
|
+
|
|
40
45
|
EM.run do
|
|
41
46
|
EventMachine.error_handler do |e|
|
|
42
47
|
@handlers[:error]&.call(e)
|
|
@@ -49,6 +54,7 @@ module Skyfall
|
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
@ws.on(:message) do |msg|
|
|
57
|
+
@reconnecting = false
|
|
52
58
|
@connection_attempts = 0
|
|
53
59
|
|
|
54
60
|
data = msg.data.pack('C*')
|
|
@@ -70,10 +76,12 @@ module Skyfall
|
|
|
70
76
|
@ws.on(:close) do |e|
|
|
71
77
|
@ws = nil
|
|
72
78
|
|
|
73
|
-
if @auto_reconnect && @engines_on
|
|
74
|
-
|
|
79
|
+
if @reconnecting || @auto_reconnect && @engines_on
|
|
80
|
+
@handlers[:reconnect]&.call
|
|
81
|
+
|
|
82
|
+
@reconnect_timer&.cancel
|
|
83
|
+
@reconnect_timer = EM::Timer.new(reconnect_delay) do
|
|
75
84
|
@connection_attempts += 1
|
|
76
|
-
@handlers[:reconnect]&.call
|
|
77
85
|
connect
|
|
78
86
|
end
|
|
79
87
|
else
|
|
@@ -85,41 +93,40 @@ module Skyfall
|
|
|
85
93
|
end
|
|
86
94
|
end
|
|
87
95
|
|
|
96
|
+
def reconnect
|
|
97
|
+
@reconnecting = true
|
|
98
|
+
@connection_attempts = 0
|
|
99
|
+
|
|
100
|
+
@ws ? @ws.close : connect
|
|
101
|
+
end
|
|
102
|
+
|
|
88
103
|
def disconnect
|
|
89
104
|
return unless EM.reactor_running?
|
|
90
105
|
|
|
106
|
+
@reconnecting = false
|
|
91
107
|
@engines_on = false
|
|
92
108
|
EM.stop_event_loop
|
|
93
109
|
end
|
|
94
110
|
|
|
95
111
|
alias close disconnect
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def on_raw_message(&block)
|
|
102
|
-
@handlers[:raw_message] = block
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def on_connecting(&block)
|
|
106
|
-
@handlers[:connecting] = block
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def on_connect(&block)
|
|
110
|
-
@handlers[:connect] = block
|
|
111
|
-
end
|
|
113
|
+
EVENTS.each do |event|
|
|
114
|
+
define_method "on_#{event}" do |&block|
|
|
115
|
+
@handlers[event.to_sym] = block
|
|
116
|
+
end
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
define_method "on_#{event}=" do |block|
|
|
119
|
+
@handlers[event.to_sym] = block
|
|
120
|
+
end
|
|
115
121
|
end
|
|
116
122
|
|
|
117
|
-
def
|
|
118
|
-
|
|
123
|
+
def inspectable_variables
|
|
124
|
+
instance_variables - [:@handlers, :@ws]
|
|
119
125
|
end
|
|
120
126
|
|
|
121
|
-
def
|
|
122
|
-
|
|
127
|
+
def inspect
|
|
128
|
+
vars = inspectable_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
|
|
129
|
+
"#<#{self.class}:0x#{object_id} #{vars}>"
|
|
123
130
|
end
|
|
124
131
|
|
|
125
132
|
|
|
@@ -134,32 +141,44 @@ module Skyfall
|
|
|
134
141
|
end
|
|
135
142
|
|
|
136
143
|
def build_websocket_url
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
@root_url + "/xrpc/" + @endpoint + (@cursor ? "?cursor=#{@cursor}" : "")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def check_cursor(cursor)
|
|
148
|
+
if cursor.nil?
|
|
149
|
+
nil
|
|
150
|
+
elsif cursor.is_a?(Integer) || cursor.is_a?(String) && cursor =~ /^[0-9]+$/
|
|
151
|
+
cursor.to_i
|
|
152
|
+
else
|
|
153
|
+
raise ArgumentError, "Invalid cursor: #{cursor.inspect} - cursor must be an integer number"
|
|
154
|
+
end
|
|
140
155
|
end
|
|
141
156
|
|
|
142
157
|
def check_endpoint(endpoint)
|
|
143
158
|
if endpoint.is_a?(String)
|
|
144
|
-
raise ArgumentError("Invalid endpoint name: #{endpoint}") if endpoint.strip
|
|
159
|
+
raise ArgumentError.new("Invalid endpoint name: #{endpoint}") if endpoint.strip == '' || !endpoint.include?('.')
|
|
145
160
|
elsif endpoint.is_a?(Symbol)
|
|
146
|
-
raise ArgumentError("Unknown endpoint: #{endpoint}") if NAMED_ENDPOINTS[endpoint].nil?
|
|
161
|
+
raise ArgumentError.new("Unknown endpoint: #{endpoint}") if NAMED_ENDPOINTS[endpoint].nil?
|
|
147
162
|
endpoint = NAMED_ENDPOINTS[endpoint]
|
|
148
163
|
else
|
|
149
|
-
raise ArgumentError
|
|
164
|
+
raise ArgumentError, "Endpoint should be a string or a symbol"
|
|
150
165
|
end
|
|
151
166
|
|
|
152
167
|
endpoint
|
|
153
168
|
end
|
|
154
169
|
|
|
155
|
-
def
|
|
170
|
+
def build_root_url(server)
|
|
156
171
|
if server.is_a?(String)
|
|
157
|
-
|
|
172
|
+
if server.start_with?('ws://') || server.start_with?('wss://')
|
|
173
|
+
server
|
|
174
|
+
elsif server.strip.empty? || server.include?('/')
|
|
175
|
+
raise ArgumentError, "Server parameter should be a hostname or a ws:// or wss:// URL"
|
|
176
|
+
else
|
|
177
|
+
"wss://#{server}"
|
|
178
|
+
end
|
|
158
179
|
else
|
|
159
|
-
raise ArgumentError
|
|
180
|
+
raise ArgumentError, "Server parameter should be a string"
|
|
160
181
|
end
|
|
161
|
-
|
|
162
|
-
server
|
|
163
182
|
end
|
|
164
183
|
end
|
|
165
184
|
end
|
data/lib/skyfall/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: skyfall
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kuba Suder
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2024-03-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base32
|
|
@@ -50,20 +50,96 @@ dependencies:
|
|
|
50
50
|
- - ">="
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
52
|
version: 0.5.9.6
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: eventmachine
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '1.2'
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: 1.2.7
|
|
63
|
+
type: :runtime
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - "~>"
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '1.2'
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 1.2.7
|
|
53
73
|
- !ruby/object:Gem::Dependency
|
|
54
74
|
name: faye-websocket
|
|
55
75
|
requirement: !ruby/object:Gem::Requirement
|
|
56
76
|
requirements:
|
|
57
77
|
- - "~>"
|
|
58
78
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: 0.11
|
|
79
|
+
version: '0.11'
|
|
80
|
+
type: :runtime
|
|
81
|
+
prerelease: false
|
|
82
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - "~>"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '0.11'
|
|
87
|
+
- !ruby/object:Gem::Dependency
|
|
88
|
+
name: base64
|
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - "~>"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0.1'
|
|
94
|
+
type: :runtime
|
|
95
|
+
prerelease: false
|
|
96
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0.1'
|
|
101
|
+
- !ruby/object:Gem::Dependency
|
|
102
|
+
name: stringio
|
|
103
|
+
requirement: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - "~>"
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '3.0'
|
|
108
|
+
type: :runtime
|
|
109
|
+
prerelease: false
|
|
110
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - "~>"
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '3.0'
|
|
115
|
+
- !ruby/object:Gem::Dependency
|
|
116
|
+
name: time
|
|
117
|
+
requirement: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - "~>"
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '0.3'
|
|
122
|
+
type: :runtime
|
|
123
|
+
prerelease: false
|
|
124
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - "~>"
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '0.3'
|
|
129
|
+
- !ruby/object:Gem::Dependency
|
|
130
|
+
name: uri
|
|
131
|
+
requirement: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - "~>"
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '0.13'
|
|
60
136
|
type: :runtime
|
|
61
137
|
prerelease: false
|
|
62
138
|
version_requirements: !ruby/object:Gem::Requirement
|
|
63
139
|
requirements:
|
|
64
140
|
- - "~>"
|
|
65
141
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: 0.
|
|
142
|
+
version: '0.13'
|
|
67
143
|
description: "\n Skyfall is a Ruby library for connecting to the \"firehose\" of
|
|
68
144
|
the Bluesky social network, i.e. a websocket which\n streams all new posts and
|
|
69
145
|
everything else happening on the Bluesky network in real time. The code connects
|
|
@@ -79,16 +155,22 @@ files:
|
|
|
79
155
|
- CHANGELOG.md
|
|
80
156
|
- LICENSE.txt
|
|
81
157
|
- README.md
|
|
82
|
-
- example/
|
|
158
|
+
- example/block_tracker.rb
|
|
159
|
+
- example/monitor_phrases.rb
|
|
160
|
+
- example/print_all_posts.rb
|
|
161
|
+
- example/push_notifications.rb
|
|
83
162
|
- lib/skyfall.rb
|
|
84
163
|
- lib/skyfall/car_archive.rb
|
|
85
164
|
- lib/skyfall/cid.rb
|
|
86
165
|
- lib/skyfall/collection.rb
|
|
87
166
|
- lib/skyfall/errors.rb
|
|
88
167
|
- lib/skyfall/extensions.rb
|
|
168
|
+
- lib/skyfall/label.rb
|
|
89
169
|
- lib/skyfall/messages/commit_message.rb
|
|
90
170
|
- lib/skyfall/messages/handle_message.rb
|
|
171
|
+
- lib/skyfall/messages/identity_message.rb
|
|
91
172
|
- lib/skyfall/messages/info_message.rb
|
|
173
|
+
- lib/skyfall/messages/labels_message.rb
|
|
92
174
|
- lib/skyfall/messages/tombstone_message.rb
|
|
93
175
|
- lib/skyfall/messages/unknown_message.rb
|
|
94
176
|
- lib/skyfall/messages/websocket_message.rb
|
data/example/firehose.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
lib = File.expand_path('../../lib', __FILE__)
|
|
4
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
-
|
|
6
|
-
require 'skyfall'
|
|
7
|
-
|
|
8
|
-
sky = Skyfall::Stream.new('bsky.social', :subscribe_repos)
|
|
9
|
-
|
|
10
|
-
sky.on_message do |m|
|
|
11
|
-
next if m.type != :commit
|
|
12
|
-
|
|
13
|
-
m.operations.each do |op|
|
|
14
|
-
next unless op.action == :create && op.type == :bsky_post
|
|
15
|
-
|
|
16
|
-
puts "#{op.repo}:"
|
|
17
|
-
puts op.raw_record['text']
|
|
18
|
-
puts
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
sky.on_connect { puts "Connected" }
|
|
23
|
-
sky.on_disconnect { puts "Disconnected" }
|
|
24
|
-
sky.on_reconnect { puts "Reconnecting..." }
|
|
25
|
-
sky.on_error { |e| puts "ERROR: #{e}" }
|
|
26
|
-
|
|
27
|
-
sky.connect
|