skyfall 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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