skyfall 0.6.0 → 0.6.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 +7 -0
- data/LICENSE.txt +1 -1
- data/README.md +9 -7
- data/lib/skyfall/collection.rb +3 -1
- data/lib/skyfall/errors.rb +9 -0
- data/lib/skyfall/firehose.rb +1 -5
- data/lib/skyfall/jetstream.rb +1 -6
- data/lib/skyfall/stream.rb +51 -17
- data/lib/skyfall/version.rb +1 -1
- metadata +8 -12
- data/example/block_tracker.rb +0 -84
- data/example/jet_monitor_phrases.rb +0 -54
- data/example/print_all_posts.rb +0 -34
- data/example/push_notifications.rb +0 -262
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d216f6eca172d8982c358d594511fa31212dd9dfdb949ed1149d16b23862bfa
|
|
4
|
+
data.tar.gz: c381c2abebdb22265568b0183b7d78525a6cbbbd01192b32faad4ed1dadbec02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d4c45ef3103036b5e13c5614615f20b61d9beaa7092af2058b8135cd4b460ede00c23b1d05cc5b57751a64a50e6286946d438ccd80c899023c9d92be6ac710e
|
|
7
|
+
data.tar.gz: d7bd5bb732b86db3c56b9389a25a5e6d80f57629ec27621c32330ad0155c379864388eb347381e8f5e38387c4b22ce019fd4879c0e33eb46852e1565c2aef844
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [0.6.1] - 2026-01-08
|
|
2
|
+
|
|
3
|
+
- added `:bsky_notif_declaration` shortcode for `app.bsky.notification.declaration` collection
|
|
4
|
+
- throw error when trying to run two streams in one process (see b4a1514f5da28983205765e55724b5c4abe6c5e4 for details)
|
|
5
|
+
- added protected `#send_data` and `#socket` methods in `Stream` for use in `Stream` subclasses (currently for the Tapfall gem)
|
|
6
|
+
- added a way to customize headers sent when connecting in `Stream` subclasses through the `#request_headers` method
|
|
7
|
+
|
|
1
8
|
## [0.6.0] - 2025-06-25
|
|
2
9
|
|
|
3
10
|
- significantly speeded up reading of events from the binary firehose (`Skyfall::Firehose`) - up to 4-5x faster than before
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A Ruby gem for streaming data from the Bluesky/ATProto firehose 🦋
|
|
4
4
|
|
|
5
5
|
> [!NOTE]
|
|
6
|
-
> ATProto Ruby
|
|
6
|
+
> Part of ATProto Ruby SDK: [ruby.sdk.blue](https://ruby.sdk.blue)
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
## What does it do
|
|
@@ -15,13 +15,15 @@ Since version 0.5, Skyfall also supports connecting to [Jetstream](https://githu
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
|
|
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.2+. 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
19
|
|
|
20
|
-
|
|
20
|
+
To install the gem, run the command:
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
[sudo] gem install skyfall
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Or add this to your app's `Gemfile`:
|
|
25
|
+
|
|
26
|
+
gem 'skyfall', '~> 0.6'
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
## Usage
|
|
@@ -198,7 +200,7 @@ sky.on_message do |m|
|
|
|
198
200
|
end
|
|
199
201
|
```
|
|
200
202
|
|
|
201
|
-
For more examples, see the [
|
|
203
|
+
For more examples, see the [examples page](https://ruby.sdk.blue/examples/) on [ruby.sdk.blue](https://ruby.sdk.blue), or the [bluesky-feeds-rb](https://tangled.org/mackuba.eu/bluesky-feeds-rb/blob/master/app/firehose_stream.rb) project, which implements a feed generator service.
|
|
202
204
|
|
|
203
205
|
|
|
204
206
|
### Note on custom lexicons
|
|
@@ -305,7 +307,7 @@ See [Jetstream docs](https://github.com/bluesky-social/jetstream?tab=readme-ov-f
|
|
|
305
307
|
|
|
306
308
|
## Credits
|
|
307
309
|
|
|
308
|
-
Copyright ©
|
|
310
|
+
Copyright © 2026 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)).
|
|
309
311
|
|
|
310
312
|
The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
|
|
311
313
|
|
data/lib/skyfall/collection.rb
CHANGED
|
@@ -17,7 +17,8 @@ module Skyfall
|
|
|
17
17
|
BSKY_VERIFICATION = "app.bsky.graph.verification"
|
|
18
18
|
BSKY_LABELER = "app.bsky.labeler.service"
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
BSKY_NOTIF_DECLARATION = "app.bsky.notification.declaration"
|
|
21
|
+
BSKY_CHAT_DECLARATION = "chat.bsky.actor.declaration"
|
|
21
22
|
|
|
22
23
|
SHORT_CODES = {
|
|
23
24
|
BSKY_ACTOR_STATUS => :bsky_actor_status,
|
|
@@ -37,6 +38,7 @@ module Skyfall
|
|
|
37
38
|
BSKY_THREADGATE => :bsky_threadgate,
|
|
38
39
|
BSKY_VERIFICATION => :bsky_verification,
|
|
39
40
|
BSKY_CHAT_DECLARATION => :bsky_chat_declaration,
|
|
41
|
+
BSKY_NOTIF_DECLARATION => :bsky_notif_declaration
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
def self.short_code(collection)
|
data/lib/skyfall/errors.rb
CHANGED
|
@@ -5,6 +5,15 @@ module Skyfall
|
|
|
5
5
|
class UnsupportedError < StandardError
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
class ReactorActiveError < StandardError
|
|
9
|
+
def initialize
|
|
10
|
+
super(
|
|
11
|
+
"An EventMachine reactor thread is already running, but it seems to have been launched by another Stream. " +
|
|
12
|
+
"Skyfall doesn't currently support running two different Stream instances in a single process."
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
8
17
|
class SubscriptionError < StandardError
|
|
9
18
|
attr_reader :error_type, :error_message
|
|
10
19
|
|
data/lib/skyfall/firehose.rb
CHANGED
|
@@ -19,11 +19,7 @@ module Skyfall
|
|
|
19
19
|
|
|
20
20
|
@endpoint = check_endpoint(endpoint)
|
|
21
21
|
@cursor = check_cursor(cursor)
|
|
22
|
-
@root_url = @root_url
|
|
23
|
-
|
|
24
|
-
if URI(@root_url).path != ''
|
|
25
|
-
raise ArgumentError, "Server parameter should not include any path"
|
|
26
|
-
end
|
|
22
|
+
@root_url = ensure_empty_path(@root_url)
|
|
27
23
|
end
|
|
28
24
|
|
|
29
25
|
def handle_message(msg)
|
data/lib/skyfall/jetstream.rb
CHANGED
|
@@ -12,14 +12,9 @@ module Skyfall
|
|
|
12
12
|
require_relative 'jetstream/message'
|
|
13
13
|
super(server)
|
|
14
14
|
|
|
15
|
-
@root_url = @root_url.chomp('/')
|
|
16
|
-
|
|
17
|
-
if URI(@root_url).path != ''
|
|
18
|
-
raise ArgumentError, "Server parameter should not include any path"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
15
|
@params = check_params(params)
|
|
22
16
|
@cursor = @params.delete(:cursor)
|
|
17
|
+
@root_url = ensure_empty_path(@root_url)
|
|
23
18
|
end
|
|
24
19
|
|
|
25
20
|
def handle_message(msg)
|
data/lib/skyfall/stream.rb
CHANGED
|
@@ -12,8 +12,8 @@ module Skyfall
|
|
|
12
12
|
attr_accessor :auto_reconnect, :last_update, :user_agent
|
|
13
13
|
attr_accessor :heartbeat_timeout, :heartbeat_interval, :check_heartbeat
|
|
14
14
|
|
|
15
|
-
def initialize(
|
|
16
|
-
@root_url = build_root_url(
|
|
15
|
+
def initialize(server)
|
|
16
|
+
@root_url = build_root_url(server)
|
|
17
17
|
|
|
18
18
|
@handlers = {}
|
|
19
19
|
@auto_reconnect = true
|
|
@@ -33,11 +33,14 @@ module Skyfall
|
|
|
33
33
|
url = build_websocket_url
|
|
34
34
|
|
|
35
35
|
@handlers[:connecting]&.call(url)
|
|
36
|
-
@engines_on = true
|
|
37
36
|
|
|
38
37
|
@reconnect_timer&.cancel
|
|
39
38
|
@reconnect_timer = nil
|
|
40
39
|
|
|
40
|
+
raise ReactorActiveError if existing_reactor?
|
|
41
|
+
|
|
42
|
+
@engines_on = true
|
|
43
|
+
|
|
41
44
|
EM.run do
|
|
42
45
|
EventMachine.error_handler do |e|
|
|
43
46
|
@handlers[:error]&.call(e)
|
|
@@ -163,8 +166,27 @@ module Skyfall
|
|
|
163
166
|
end
|
|
164
167
|
|
|
165
168
|
|
|
169
|
+
protected
|
|
170
|
+
|
|
171
|
+
def request_headers
|
|
172
|
+
{}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def socket
|
|
176
|
+
@ws
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def send_data(data)
|
|
180
|
+
@ws.send(data)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
|
|
166
184
|
private
|
|
167
185
|
|
|
186
|
+
def existing_reactor?
|
|
187
|
+
EM.reactor_running? && !@engines_on
|
|
188
|
+
end
|
|
189
|
+
|
|
168
190
|
def reconnect_delay
|
|
169
191
|
if @connection_attempts == 0
|
|
170
192
|
0
|
|
@@ -174,29 +196,41 @@ module Skyfall
|
|
|
174
196
|
end
|
|
175
197
|
|
|
176
198
|
def build_websocket_client(url)
|
|
177
|
-
Faye::WebSocket::Client.new(url, nil, { headers: { 'User-Agent' => user_agent }})
|
|
199
|
+
Faye::WebSocket::Client.new(url, nil, { headers: { 'User-Agent' => user_agent }.merge(request_headers) })
|
|
178
200
|
end
|
|
179
201
|
|
|
180
202
|
def build_websocket_url
|
|
181
203
|
@root_url
|
|
182
204
|
end
|
|
183
205
|
|
|
184
|
-
def build_root_url(
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
uri = URI(service) # raises if invalid
|
|
195
|
-
service
|
|
206
|
+
def build_root_url(server)
|
|
207
|
+
if !server.is_a?(String)
|
|
208
|
+
raise ArgumentError, "Server parameter should be a string"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
if server.include?('://')
|
|
212
|
+
uri = URI(server)
|
|
213
|
+
|
|
214
|
+
if uri.scheme != 'ws' && uri.scheme != 'wss'
|
|
215
|
+
raise ArgumentError, "Server parameter should be a hostname or a ws:// or wss:// URL"
|
|
196
216
|
end
|
|
217
|
+
|
|
218
|
+
uri.to_s
|
|
197
219
|
else
|
|
198
|
-
|
|
220
|
+
server = "wss://#{server}"
|
|
221
|
+
uri = URI(server) # raises if invalid
|
|
222
|
+
server
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def ensure_empty_path(url)
|
|
227
|
+
url = url.chomp('/')
|
|
228
|
+
|
|
229
|
+
if URI(url).path != ''
|
|
230
|
+
raise ArgumentError, "Server URL should only include a hostname, without any path"
|
|
199
231
|
end
|
|
232
|
+
|
|
233
|
+
url
|
|
200
234
|
end
|
|
201
235
|
end
|
|
202
236
|
end
|
data/lib/skyfall/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: skyfall
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kuba Suder
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base32
|
|
@@ -112,10 +112,6 @@ files:
|
|
|
112
112
|
- CHANGELOG.md
|
|
113
113
|
- LICENSE.txt
|
|
114
114
|
- README.md
|
|
115
|
-
- example/block_tracker.rb
|
|
116
|
-
- example/jet_monitor_phrases.rb
|
|
117
|
-
- example/print_all_posts.rb
|
|
118
|
-
- example/push_notifications.rb
|
|
119
115
|
- lib/skyfall.rb
|
|
120
116
|
- lib/skyfall/car_archive.rb
|
|
121
117
|
- lib/skyfall/cid.rb
|
|
@@ -145,13 +141,13 @@ files:
|
|
|
145
141
|
- lib/skyfall/stream.rb
|
|
146
142
|
- lib/skyfall/version.rb
|
|
147
143
|
- sig/skyfall.rbs
|
|
148
|
-
homepage: https://
|
|
144
|
+
homepage: https://ruby.sdk.blue
|
|
149
145
|
licenses:
|
|
150
146
|
- Zlib
|
|
151
147
|
metadata:
|
|
152
|
-
bug_tracker_uri: https://
|
|
153
|
-
changelog_uri: https://
|
|
154
|
-
source_code_uri: https://
|
|
148
|
+
bug_tracker_uri: https://tangled.org/mackuba.eu/skyfall/issues
|
|
149
|
+
changelog_uri: https://tangled.org/mackuba.eu/skyfall/blob/master/CHANGELOG.md
|
|
150
|
+
source_code_uri: https://tangled.org/mackuba.eu/skyfall
|
|
155
151
|
rdoc_options: []
|
|
156
152
|
require_paths:
|
|
157
153
|
- lib
|
|
@@ -166,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
166
162
|
- !ruby/object:Gem::Version
|
|
167
163
|
version: '0'
|
|
168
164
|
requirements: []
|
|
169
|
-
rubygems_version:
|
|
165
|
+
rubygems_version: 4.0.3
|
|
170
166
|
specification_version: 4
|
|
171
|
-
summary: A Ruby gem for streaming data from the Bluesky/
|
|
167
|
+
summary: A Ruby gem for streaming data from the Bluesky/ATProto firehose
|
|
172
168
|
test_files: []
|
data/example/block_tracker.rb
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
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::Firehose.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
|
|
@@ -1,54 +0,0 @@
|
|
|
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.). This example uses a Jetstream connection.
|
|
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
|
-
# tell Jetstream to send us only post records
|
|
21
|
-
sky = Skyfall::Jetstream.new('jetstream2.us-east.bsky.network', { wanted_collections: [:bsky_post] })
|
|
22
|
-
|
|
23
|
-
sky.on_message do |msg|
|
|
24
|
-
# we're only interested in repo commit messages
|
|
25
|
-
next if msg.type != :commit
|
|
26
|
-
|
|
27
|
-
msg.operations.each do |op|
|
|
28
|
-
# ignore any operations other than "create post"
|
|
29
|
-
next unless op.action == :create && op.type == :bsky_post
|
|
30
|
-
|
|
31
|
-
text = op.raw_record['text'].to_s.downcase
|
|
32
|
-
|
|
33
|
-
if terms.any? { |x| text.include?(x) }
|
|
34
|
-
owner_handle = get_user_handle(op.repo)
|
|
35
|
-
puts "\n#{msg.time.getlocal} @#{owner_handle}: #{op.raw_record['text']}"
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def get_user_handle(did)
|
|
41
|
-
url = "https://plc.directory/#{did}"
|
|
42
|
-
json = JSON.parse(URI.open(url).read)
|
|
43
|
-
json['alsoKnownAs'][0].gsub('at://', '')
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
sky.on_connect { puts "Connected" }
|
|
47
|
-
sky.on_disconnect { puts "Disconnected" }
|
|
48
|
-
sky.on_reconnect { puts "Reconnecting..." }
|
|
49
|
-
sky.on_error { |e| puts "ERROR: #{e}" }
|
|
50
|
-
|
|
51
|
-
# close the connection cleanly on Ctrl+C
|
|
52
|
-
trap("SIGINT") { sky.disconnect }
|
|
53
|
-
|
|
54
|
-
sky.connect
|
data/example/print_all_posts.rb
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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::Firehose.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
|
|
@@ -1,262 +0,0 @@
|
|
|
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::Firehose.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
|