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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17dd2389545a6bd5c9c3483a89f95e40a62cf2014e263f69d3590cf651920865
4
- data.tar.gz: 9fe379e3d713c50e4a4700941660b529827e2329e200859ddea58b7113da1c3b
3
+ metadata.gz: 5d216f6eca172d8982c358d594511fa31212dd9dfdb949ed1149d16b23862bfa
4
+ data.tar.gz: c381c2abebdb22265568b0183b7d78525a6cbbbd01192b32faad4ed1dadbec02
5
5
  SHA512:
6
- metadata.gz: e9f7c603360a483241cbd6b2af8b49b315149b308c2064332b42d9fa7b27fd302c1b181c6fc7a60871f1dfead09cc3621ed2b9106db2680e0919f12119af881d
7
- data.tar.gz: e8c7da9dc57730ded46de52cc7e500d84129782bd534c2163205330143bb1a5ec0f24feb39a86e440c073972b1c4af3840ce688e9caf55177003d140267c6473
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
@@ -1,6 +1,6 @@
1
1
  The zlib License
2
2
 
3
- Copyright (c) 2023-2024 Jakub Suder
3
+ Copyright (c) 2026 Jakub Suder
4
4
 
5
5
  This software is provided 'as-is', without any express or implied
6
6
  warranty. In no event will the authors be held liable for any damages
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 gems collection: [skyfall](https://github.com/mackuba/skyfall) | [blue_factory](https://github.com/mackuba/blue_factory) | [minisky](https://github.com/mackuba/minisky) | [didkit](https://github.com/mackuba/didkit)
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
- From the command line:
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
- gem install skyfall
20
+ To install the gem, run the command:
21
21
 
22
- Or, add this to your `Gemfile`:
22
+ [sudo] gem install skyfall
23
23
 
24
- gem 'skyfall', '~> 0.5'
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 [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.
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 © 2024 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/mackuba.eu)).
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
 
@@ -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
- BSKY_CHAT_DECLARATION = "chat.bsky.actor.declaration"
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)
@@ -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
 
@@ -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.chomp('/')
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)
@@ -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)
@@ -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(service)
16
- @root_url = build_root_url(service)
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(service)
185
- if service.is_a?(String)
186
- if service.include?('/')
187
- uri = URI(service)
188
- if uri.scheme != 'ws' && uri.scheme != 'wss'
189
- raise ArgumentError, "Service parameter should be a hostname or a ws:// or wss:// URL"
190
- end
191
- uri.to_s
192
- else
193
- service = "wss://#{service}"
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
- raise ArgumentError, "Service parameter should be a string"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skyfall
4
- VERSION = "0.6.0"
4
+ VERSION = "0.6.1"
5
5
  end
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.0
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: 2025-06-25 00:00:00.000000000 Z
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://github.com/mackuba/skyfall
144
+ homepage: https://ruby.sdk.blue
149
145
  licenses:
150
146
  - Zlib
151
147
  metadata:
152
- bug_tracker_uri: https://github.com/mackuba/skyfall/issues
153
- changelog_uri: https://github.com/mackuba/skyfall/blob/master/CHANGELOG.md
154
- source_code_uri: https://github.com/mackuba/skyfall
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: 3.6.2
165
+ rubygems_version: 4.0.3
170
166
  specification_version: 4
171
- summary: A Ruby gem for streaming data from the Bluesky/AtProto firehose
167
+ summary: A Ruby gem for streaming data from the Bluesky/ATProto firehose
172
168
  test_files: []
@@ -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
@@ -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