skyfall 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34e2166dc123a74140ce5cae67d3c000c1f88972d15cbe8a2550bde11ba4034d
4
- data.tar.gz: 334f4b8b0ddce03b2258a0266e01e08cac9528924ac13380e91b66a0bb1217b8
3
+ metadata.gz: db537537fcd4e38f184c6cdea3f51ba1b8554848d4c3cd1de20f63ca2d956fc2
4
+ data.tar.gz: 34c51f6c8c0152589562bf0a3b3c5246cd8a1e95664f1128c86bd17c6a313d59
5
5
  SHA512:
6
- metadata.gz: f56ab132941d80a577e5f3ba630b58d6f99548d0cde398fdb4f15e7c24d9557c3b0243d64a3940c18bf370afb219df3282c935300fc0359b6b0e00915b7002ca
7
- data.tar.gz: 4bd7672a9450d8b1ec80ec679b69e6ba4242c4a6554865e296e7a347769795239b99a95539fe0b637407c2736b0716dfc8d1c3a9dea2d8d7a9654b47158262c9
6
+ metadata.gz: 11477293a1bc0377ef5d9eaa68cc2d374f76ac4d942006572e8b9e1668cb8f54a582abdeda81b65c1d72f6452ba2cad1153c403027ba5d7efbfa093624713105
7
+ data.tar.gz: 8d86b1f71a4fd5fa7f01077fefe8d8e292dc6b5007b79ee4d1ed1343f887c4104c0480b297e7ce4be61db0cce65211a568b642411fa5c3e7f62c4c91a4fe0f7b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.4.0] - 2024-09-23
2
+
3
+ - (re)added a "hearbeat" feature (removed earlier in 0.2.0) to fix the occasional issue when the websocket stops receiving data, but doesn't disconnect (not enabled by default, turn it on by setting `check_heartbeat` to true)
4
+ - added a way to set the user agent sent when connecting using the `user_agent` field (default is `"Skyfall/#{version}"`)
5
+ - added `app.bsky.feed.postgate` record type
6
+
7
+ ## [0.3.1] - 2024-06-28
8
+
9
+ - added `app.bsky.graph.starterpack` and `chat.bsky.actor.declaration` record types
10
+ - added `#account` event type (`AccountMessage`)
11
+ - added `handle` field to `IdentityMessage`
12
+ - fixed param validation on `Stream` initialization
13
+ - reverted the change that added Ruby stdlib dependencies explicitly to the gemspec, since this causes more problems than it's worth - only `base64` is left there, since it's the one now required to be listed
14
+
1
15
  ## [0.3.0] - 2024-03-21
2
16
 
3
17
  - added support for labeller firehose, served by labeller services at the `com.atproto.label.subscribeLabels` endpoint (aliased as `:subscribe_labels`)
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The zlib License
2
2
 
3
- Copyright (c) 2023 Jakub Suder
3
+ Copyright (c) 2023-2024 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
@@ -1,6 +1,9 @@
1
1
  # Skyfall
2
2
 
3
- 🌤 A Ruby gem for streaming data from the Bluesky/AtProto firehose 🦋
3
+ A Ruby gem for streaming data from the Bluesky/AtProto firehose 🦋
4
+
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)
4
7
 
5
8
 
6
9
  ## What does it do
@@ -120,9 +123,36 @@ When Skyfall receives a message about a record type that's not on the list, whet
120
123
  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.
121
124
 
122
125
 
126
+ ## Configuration
127
+
128
+ ### User agent
129
+
130
+ `Skyfall::Stream` sends a user agent header when making a connection. This is set by default to `"Skyfall/0.x.y"`, but it's recommended that you override it using the `user_agent` field to something that identifies your app and its author – this will let the owner of the server you're connecting to know who to contact in case the client is causing some problems.
131
+
132
+ You can also append your user agent info to the default value like this:
133
+
134
+ ```rb
135
+ sky.user_agent = "NewsBot (@news.bot) #{sky.default_user_agent}"
136
+ ```
137
+
138
+ ### Heartbeat and reconnecting
139
+
140
+ Occasionally, especially during times of very heavy traffic, the websocket can get into a stuck state where it stops receiving any data, but doesn't disconnect and just hangs like this forever. To work around this, there is a "heartbeat" feature which starts a background timer, which periodically checks how much time has passed since the last received event, and if the time exceeds a set limit, it manually disconnects and reconnects the stream.
141
+
142
+ The option is not enabled by default, because there are some firehoses which will not be sending events often, possibly only once in a while – e.g. labellers and independent PDS firehoses – and in this case we don't want any heartbeat since it will be completely normal not to have any events for a long time. It's not really possible to detect easily if we're connecting to a full network relay or one of those, so in order to avoid false alarms, you need to enable this manually using the `check_heartbeat` property.
143
+
144
+ You can also change the `heartbeat_interval`, i.e. how often the timer is triggered (default: 10s), and the `heartbeat_timeout`, i.e. the amount of time passed without events when it reconnects (default: 5 min):
145
+
146
+ ```rb
147
+ sky.check_heartbeat = true
148
+ sky.heartbeat_interval = 5
149
+ sky.heartbeat_timeout = 120
150
+ ```
151
+
152
+
123
153
  ## Credits
124
154
 
125
- Copyright © 2023 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/mackuba.eu)).
155
+ Copyright © 2024 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/mackuba.eu)).
126
156
 
127
157
  The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
128
158
 
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Example: track when people follow and unfollow your 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 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
@@ -1,16 +1,20 @@
1
1
  module Skyfall
2
2
  module Collection
3
- BSKY_PROFILE = "app.bsky.actor.profile"
4
- BSKY_FEED = "app.bsky.feed.generator"
5
- BSKY_LIKE = "app.bsky.feed.like"
6
- BSKY_POST = "app.bsky.feed.post"
7
- BSKY_REPOST = "app.bsky.feed.repost"
8
- BSKY_THREADGATE = "app.bsky.feed.threadgate"
9
- BSKY_BLOCK = "app.bsky.graph.block"
10
- BSKY_FOLLOW = "app.bsky.graph.follow"
11
- BSKY_LIST = "app.bsky.graph.list"
12
- BSKY_LISTBLOCK = "app.bsky.graph.listblock"
13
- BSKY_LISTITEM = "app.bsky.graph.listitem"
14
- BSKY_LABELER = "app.bsky.labeler.service"
3
+ BSKY_PROFILE = "app.bsky.actor.profile"
4
+ BSKY_FEED = "app.bsky.feed.generator"
5
+ BSKY_LIKE = "app.bsky.feed.like"
6
+ BSKY_POST = "app.bsky.feed.post"
7
+ BSKY_POSTGATE = "app.bsky.feed.postgate"
8
+ BSKY_REPOST = "app.bsky.feed.repost"
9
+ BSKY_THREADGATE = "app.bsky.feed.threadgate"
10
+ BSKY_BLOCK = "app.bsky.graph.block"
11
+ BSKY_FOLLOW = "app.bsky.graph.follow"
12
+ BSKY_LIST = "app.bsky.graph.list"
13
+ BSKY_LISTBLOCK = "app.bsky.graph.listblock"
14
+ BSKY_LISTITEM = "app.bsky.graph.listitem"
15
+ BSKY_STARTERPACK = "app.bsky.graph.starterpack"
16
+ BSKY_LABELER = "app.bsky.labeler.service"
17
+
18
+ BSKY_CHAT_DECLARATION = "chat.bsky.actor.declaration"
15
19
  end
16
20
  end
@@ -0,0 +1,11 @@
1
+ module Skyfall
2
+ class AccountMessage < WebsocketMessage
3
+ def active?
4
+ @data_object['active']
5
+ end
6
+
7
+ def status
8
+ @data_object['status']&.to_sym
9
+ end
10
+ end
11
+ end
@@ -1,4 +1,9 @@
1
1
  module Skyfall
2
+
3
+ #
4
+ # Note: this event type is deprecated and will stop being emitted at some point.
5
+ # You should instead listen for 'identity' events (Skyfall::IdentityMessage).
6
+ #
2
7
  class HandleMessage < WebsocketMessage
3
8
  def handle
4
9
  @data_object['handle']
@@ -1,4 +1,7 @@
1
1
  module Skyfall
2
2
  class IdentityMessage < WebsocketMessage
3
+ def handle
4
+ @data_object['handle']
5
+ end
3
6
  end
4
7
  end
@@ -1,4 +1,9 @@
1
1
  module Skyfall
2
+
3
+ #
4
+ # Note: this event type is deprecated and will stop being emitted at some point.
5
+ # You should instead listen for 'account' events (Skyfall::AccountMessage).
6
+ #
2
7
  class TombstoneMessage < WebsocketMessage
3
8
  end
4
9
  end
@@ -8,6 +8,7 @@ module Skyfall
8
8
  class WebsocketMessage
9
9
  using Skyfall::Extensions
10
10
 
11
+ require_relative 'account_message'
11
12
  require_relative 'commit_message'
12
13
  require_relative 'handle_message'
13
14
  require_relative 'identity_message'
@@ -25,6 +26,7 @@ module Skyfall
25
26
  type_object, data_object = decode_cbor_objects(data)
26
27
 
27
28
  message_class = case type_object['t']
29
+ when '#account' then AccountMessage
28
30
  when '#commit' then CommitMessage
29
31
  when '#handle' then HandleMessage
30
32
  when '#identity' then IdentityMessage
@@ -43,18 +43,21 @@ module Skyfall
43
43
 
44
44
  def type
45
45
  case collection
46
- when Collection::BSKY_BLOCK then :bsky_block
47
- when Collection::BSKY_FEED then :bsky_feed
48
- when Collection::BSKY_FOLLOW then :bsky_follow
49
- when Collection::BSKY_LABELER then :bsky_labeler
50
- when Collection::BSKY_LIKE then :bsky_like
51
- when Collection::BSKY_LIST then :bsky_list
52
- when Collection::BSKY_LISTBLOCK then :bsky_listblock
53
- when Collection::BSKY_LISTITEM then :bsky_listitem
54
- when Collection::BSKY_POST then :bsky_post
55
- when Collection::BSKY_PROFILE then :bsky_profile
56
- when Collection::BSKY_REPOST then :bsky_repost
57
- when Collection::BSKY_THREADGATE then :bsky_threadgate
46
+ when Collection::BSKY_BLOCK then :bsky_block
47
+ when Collection::BSKY_FEED then :bsky_feed
48
+ when Collection::BSKY_FOLLOW then :bsky_follow
49
+ when Collection::BSKY_LABELER then :bsky_labeler
50
+ when Collection::BSKY_LIKE then :bsky_like
51
+ when Collection::BSKY_LIST then :bsky_list
52
+ when Collection::BSKY_LISTBLOCK then :bsky_listblock
53
+ when Collection::BSKY_LISTITEM then :bsky_listitem
54
+ when Collection::BSKY_POST then :bsky_post
55
+ when Collection::BSKY_POSTGATE then :bsky_postgate
56
+ when Collection::BSKY_PROFILE then :bsky_profile
57
+ when Collection::BSKY_REPOST then :bsky_repost
58
+ when Collection::BSKY_STARTERPACK then :bsky_starterpack
59
+ when Collection::BSKY_THREADGATE then :bsky_threadgate
60
+ when Collection::BSKY_CHAT_DECLARATION then :bsky_chat_declaration
58
61
  else :unknown
59
62
  end
60
63
  end
@@ -14,11 +14,12 @@ module Skyfall
14
14
  :subscribe_labels => SUBSCRIBE_LABELS
15
15
  }
16
16
 
17
- EVENTS = %w(message raw_message connecting connect disconnect reconnect error)
17
+ EVENTS = %w(message raw_message connecting connect disconnect reconnect error timeout)
18
18
 
19
19
  MAX_RECONNECT_INTERVAL = 300
20
20
 
21
- attr_accessor :heartbeat_timeout, :heartbeat_interval, :cursor, :auto_reconnect
21
+ attr_accessor :cursor, :auto_reconnect, :last_update, :user_agent
22
+ attr_accessor :heartbeat_timeout, :heartbeat_interval, :check_heartbeat
22
23
 
23
24
  def initialize(server, endpoint, cursor = nil)
24
25
  @endpoint = check_endpoint(endpoint)
@@ -26,7 +27,12 @@ module Skyfall
26
27
  @cursor = check_cursor(cursor)
27
28
  @handlers = {}
28
29
  @auto_reconnect = true
30
+ @check_heartbeat = false
29
31
  @connection_attempts = 0
32
+ @heartbeat_interval = 10
33
+ @heartbeat_timeout = 300
34
+ @last_update = nil
35
+ @user_agent = default_user_agent
30
36
 
31
37
  @handlers[:error] = proc { |e| puts "ERROR: #{e}" }
32
38
  end
@@ -47,15 +53,18 @@ module Skyfall
47
53
  @handlers[:error]&.call(e)
48
54
  end
49
55
 
50
- @ws = Faye::WebSocket::Client.new(url)
56
+ @ws = Faye::WebSocket::Client.new(url, nil, { headers: { 'User-Agent' => user_agent }})
51
57
 
52
58
  @ws.on(:open) do |e|
53
59
  @handlers[:connect]&.call
60
+ @last_update = Time.now
61
+ start_heartbeat_timer
54
62
  end
55
63
 
56
64
  @ws.on(:message) do |msg|
57
65
  @reconnecting = false
58
66
  @connection_attempts = 0
67
+ @last_update = Time.now
59
68
 
60
69
  data = msg.data.pack('C*')
61
70
  @handlers[:raw_message]&.call(data)
@@ -85,6 +94,7 @@ module Skyfall
85
94
  connect
86
95
  end
87
96
  else
97
+ stop_heartbeat_timer
88
98
  @engines_on = false
89
99
  @handlers[:disconnect]&.call
90
100
  EM.stop_event_loop unless @ws
@@ -110,6 +120,40 @@ module Skyfall
110
120
 
111
121
  alias close disconnect
112
122
 
123
+ def default_user_agent
124
+ "Skyfall/#{Skyfall::VERSION}"
125
+ end
126
+
127
+ def check_heartbeat=(value)
128
+ @check_heartbeat = value
129
+
130
+ if @check_heartbeat && @engines_on && @ws && !@heartbeat_timer
131
+ start_heartbeat_timer
132
+ elsif !@check_heartbeat && @heartbeat_timer
133
+ stop_heartbeat_timer
134
+ end
135
+ end
136
+
137
+ def start_heartbeat_timer
138
+ return if !@check_heartbeat || @heartbeat_interval.to_f <= 0 || @heartbeat_timeout.to_f <= 0
139
+ return if @heartbeat_timer
140
+
141
+ @heartbeat_timer = EM::PeriodicTimer.new(@heartbeat_interval) do
142
+ next if @ws.nil? || @heartbeat_timeout.to_f <= 0
143
+ time_passed = Time.now - @last_update
144
+
145
+ if time_passed > @heartbeat_timeout
146
+ @handlers[:timeout]&.call
147
+ reconnect
148
+ end
149
+ end
150
+ end
151
+
152
+ def stop_heartbeat_timer
153
+ @heartbeat_timer&.cancel
154
+ @heartbeat_timer = nil
155
+ end
156
+
113
157
  EVENTS.each do |event|
114
158
  define_method "on_#{event}" do |&block|
115
159
  @handlers[event.to_sym] = block
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skyfall
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.0
4
+ version: 0.4.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: 2024-03-21 00:00:00.000000000 Z
11
+ date: 2024-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base32
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.3.4
33
+ - !ruby/object:Gem::Dependency
34
+ name: base64
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.1'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: cbor
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -84,62 +98,6 @@ dependencies:
84
98
  - - "~>"
85
99
  - !ruby/object:Gem::Version
86
100
  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'
136
- type: :runtime
137
- prerelease: false
138
- version_requirements: !ruby/object:Gem::Requirement
139
- requirements:
140
- - - "~>"
141
- - !ruby/object:Gem::Version
142
- version: '0.13'
143
101
  description: "\n Skyfall is a Ruby library for connecting to the \"firehose\" of
144
102
  the Bluesky social network, i.e. a websocket which\n streams all new posts and
145
103
  everything else happening on the Bluesky network in real time. The code connects
@@ -156,6 +114,7 @@ files:
156
114
  - LICENSE.txt
157
115
  - README.md
158
116
  - example/block_tracker.rb
117
+ - example/follower_tracker.rb
159
118
  - example/monitor_phrases.rb
160
119
  - example/print_all_posts.rb
161
120
  - example/push_notifications.rb
@@ -166,6 +125,7 @@ files:
166
125
  - lib/skyfall/errors.rb
167
126
  - lib/skyfall/extensions.rb
168
127
  - lib/skyfall/label.rb
128
+ - lib/skyfall/messages/account_message.rb
169
129
  - lib/skyfall/messages/commit_message.rb
170
130
  - lib/skyfall/messages/handle_message.rb
171
131
  - lib/skyfall/messages/identity_message.rb