skyfall 0.3.1 → 0.4.1

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: 1002b9dbaf48b9158f4b37d314e7f72d0990bf8290a5ad5d172af5aec9c48e38
4
- data.tar.gz: 2a7cf444f30a13135e0848cbf0ad6c1900d9210e0799117e0612d5c52a93b2a4
3
+ metadata.gz: 1fa2648926fdc913472ec8fca4d6bdf3bbac188dbb0d9c82816538ca0c446d23
4
+ data.tar.gz: f35348c4834fb9dc26544105074bdc4f9ba07c36d595e5c248651d47e5dc188e
5
5
  SHA512:
6
- metadata.gz: 600eaf8dd34c393b592a6490848d9e981c9ff0fa17fab629d2d640162eecda1248a49944e49d4bb679f732551e75ac209fd09bac85b7a087f81e8b13153c3446
7
- data.tar.gz: e8d2c63b039aa7918337b67b77e8d0b511aba81b98d21862cf75a8561d42e72e71214fe16b11f884082fdd8b646e917b840b0369911d07afb32aedc42badc38a
6
+ metadata.gz: df775bdf85f5fe73f5fc5f27868b4ad8755219e98dfae72a32e7a9bb66295b463f65a2ebd9a8c956d74f200f667a9f02c00310e665a506f69a950db03eec4ad6
7
+ data.tar.gz: 6dfe5acf800a9765d0ad6f3ce78e7a6aae445a2dde4028ef3af39ff81a5fc3947080ecfb0ceb0dcf439a2db3e6d65e17376b187455fa1eaf56889c87d0137be1
data/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
- ## [Unreleased]
1
+ ## [0.4.1] - 2024-10-04
2
2
 
3
- - added heartbeat timer
3
+ - performance fix - don't decode CAR sections which aren't needed, which is most of them; this cuts the amount of memory that GC has to free up by about one third, and should speed up processing by around ~10%
4
+
5
+ ## [0.4.0] - 2024-09-23
6
+
7
+ - (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)
8
+ - added a way to set the user agent sent when connecting using the `user_agent` field (default is `"Skyfall/#{version}"`)
9
+ - added `app.bsky.feed.postgate` record type
4
10
 
5
11
  ## [0.3.1] - 2024-06-28
6
12
 
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
@@ -123,9 +123,36 @@ When Skyfall receives a message about a record type that's not on the list, whet
123
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.
124
124
 
125
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
+
126
153
  ## Credits
127
154
 
128
- 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)).
129
156
 
130
157
  The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
131
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
@@ -11,11 +11,15 @@ require 'stringio'
11
11
 
12
12
  module Skyfall
13
13
  class CarSection
14
- attr_reader :cid, :body
14
+ attr_reader :cid
15
15
 
16
- def initialize(cid, body)
16
+ def initialize(cid, body_data)
17
17
  @cid = cid
18
- @body = body
18
+ @body_data = body_data
19
+ end
20
+
21
+ def body
22
+ @body ||= CarArchive.convert_data(CBOR.decode(@body_data))
19
23
  end
20
24
  end
21
25
 
@@ -37,9 +41,7 @@ module Skyfall
37
41
  section && section.body
38
42
  end
39
43
 
40
- private
41
-
42
- def convert_data(object)
44
+ def self.convert_data(object)
43
45
  if object.is_a?(Hash)
44
46
  object.each do |k, v|
45
47
  if v.is_a?(Hash) || v.is_a?(Array)
@@ -65,14 +67,16 @@ module Skyfall
65
67
  end
66
68
  end
67
69
 
68
- def make_cid_link(cid)
70
+ def self.make_cid_link(cid)
69
71
  { '$link' => CID.from_cbor_tag(cid) }
70
72
  end
71
73
 
72
- def make_bytes(data)
74
+ def self.make_bytes(data)
73
75
  { '$bytes' => Base64.encode64(data).chomp.gsub(/=+$/, '') }
74
76
  end
75
77
 
78
+ private
79
+
76
80
  def read_header(buffer)
77
81
  len = buffer.read_varint
78
82
 
@@ -112,10 +116,7 @@ module Skyfall
112
116
  cid = CID.new(prefix + cid_data)
113
117
 
114
118
  body_data = sbuffer.read
115
- body = CBOR.decode(body_data)
116
- convert_data(body)
117
-
118
- @sections << CarSection.new(cid, body)
119
+ @sections << CarSection.new(cid, body_data)
119
120
  end
120
121
  end
121
122
  end
@@ -4,6 +4,7 @@ module Skyfall
4
4
  BSKY_FEED = "app.bsky.feed.generator"
5
5
  BSKY_LIKE = "app.bsky.feed.like"
6
6
  BSKY_POST = "app.bsky.feed.post"
7
+ BSKY_POSTGATE = "app.bsky.feed.postgate"
7
8
  BSKY_REPOST = "app.bsky.feed.repost"
8
9
  BSKY_THREADGATE = "app.bsky.feed.threadgate"
9
10
  BSKY_BLOCK = "app.bsky.graph.block"
@@ -52,6 +52,7 @@ module Skyfall
52
52
  when Collection::BSKY_LISTBLOCK then :bsky_listblock
53
53
  when Collection::BSKY_LISTITEM then :bsky_listitem
54
54
  when Collection::BSKY_POST then :bsky_post
55
+ when Collection::BSKY_POSTGATE then :bsky_postgate
55
56
  when Collection::BSKY_PROFILE then :bsky_profile
56
57
  when Collection::BSKY_REPOST then :bsky_repost
57
58
  when Collection::BSKY_STARTERPACK then :bsky_starterpack
@@ -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.1"
4
+ VERSION = "0.4.1"
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.1
4
+ version: 0.4.1
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-06-28 00:00:00.000000000 Z
11
+ date: 2024-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base32
@@ -114,6 +114,7 @@ files:
114
114
  - LICENSE.txt
115
115
  - README.md
116
116
  - example/block_tracker.rb
117
+ - example/follower_tracker.rb
117
118
  - example/monitor_phrases.rb
118
119
  - example/print_all_posts.rb
119
120
  - example/push_notifications.rb