skyfall 0.2.3 → 0.2.5
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 +13 -0
- data/README.md +46 -11
- data/example/block_tracker.rb +84 -0
- data/example/monitor_phrases.rb +53 -0
- data/example/print_all_posts.rb +34 -0
- data/example/push_notifications.rb +262 -0
- data/lib/skyfall/collection.rb +1 -0
- data/lib/skyfall/messages/identity_message.rb +4 -0
- data/lib/skyfall/messages/websocket_message.rb +2 -0
- data/lib/skyfall/operation.rb +12 -0
- data/lib/skyfall/stream.rb +51 -20
- data/lib/skyfall/version.rb +1 -1
- metadata +7 -3
- data/example/firehose.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 965eb9da488c1af0a4ea4573de3677572f6acdfab64497ce65bf0f3ba11ac015
|
4
|
+
data.tar.gz: 0fa130b4da079e253de127813e245a5851eaa9c83651e955099b070a65a7396e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c0a2f258e2a96d0fb6e9f1206df3dadcad9543bf5898fdc3702e74e097c3f5758b7a6c33d8b3dbfaa437b4718122e2a4c199b70d2e9c12ea5686645cbcd414b7
|
7
|
+
data.tar.gz: 417f34ff64333390397a91e6cbc9364b858b015a000759a6beccfed3bf81b448b8261c4fdfeed7ae3ebadaa995f380837b6cc543aceec83342884e8ff8e48b11
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
## [0.2.5] - 2024-03-14
|
2
|
+
|
3
|
+
- added `:bsky_labeler` record type symbol & collection constant
|
4
|
+
|
5
|
+
## [0.2.4] - 2024-02-27
|
6
|
+
|
7
|
+
- added support for `#identity` message type
|
8
|
+
- added `Operation#did` as an alias of `#repo`
|
9
|
+
- added `Stream#reconnect` method which forces the websocket to reconnect
|
10
|
+
- added some validation for the `cursor` parameter in `Stream` initializer
|
11
|
+
- the `server` parameter in `Stream` initializer can be a full URL with scheme, which lets you connect to e.g. `ws://localhost` (since by default, `wss://` is used)
|
12
|
+
- tweaked `#inspect` output of `Stream` and `Operation`
|
13
|
+
|
1
14
|
## [0.2.3] - 2023-09-28
|
2
15
|
|
3
16
|
- fixed encoding of image CIDs again (they should be wrapped in a `$link` object)
|
data/README.md
CHANGED
@@ -22,7 +22,7 @@ Start a connection to the firehose by creating a `Skyfall::Stream` object, passi
|
|
22
22
|
```rb
|
23
23
|
require 'skyfall'
|
24
24
|
|
25
|
-
sky = Skyfall::Stream.new('bsky.
|
25
|
+
sky = Skyfall::Stream.new('bsky.network', :subscribe_repos)
|
26
26
|
```
|
27
27
|
|
28
28
|
Add event listeners to handle incoming messages and get notified of errors:
|
@@ -44,27 +44,53 @@ sky.connect
|
|
44
44
|
|
45
45
|
### Processing messages
|
46
46
|
|
47
|
-
Each message passed to `on_message` is an instance of
|
47
|
+
Each message passed to `on_message` is an instance of a subclass of `WebsocketMessage`, depending on the message type. The supported message types are:
|
48
|
+
|
49
|
+
- `CommitMessage` (`#commit`) - represents a change in a user's repo; most messages are of this type
|
50
|
+
- `HandleMessage` (`#handle`) - when a different handle is assigned to a user's DID
|
51
|
+
- `TombstoneMessage` (`#tombstone`) - when an account is deleted
|
52
|
+
- `InfoMessage` (`#info`) - a protocol error message, e.g. about an invalid cursor parameter
|
53
|
+
- `UnknownMessage` is used for other unrecognized message types
|
54
|
+
|
55
|
+
All message objects have the following properties:
|
56
|
+
|
57
|
+
- `type` (symbol) - the message type identifier, e.g. `:commit`
|
58
|
+
- `seq` (integer) - a sequential index of the message
|
59
|
+
- `repo` or `did` (string) - DID of the repository (user account)
|
60
|
+
- `time` (Time) - timestamp of the described action
|
61
|
+
|
62
|
+
All properties except `type` may be nil for some message types that aren't related to a specific user, like `#info`.
|
63
|
+
|
64
|
+
Commit messages additionally have:
|
48
65
|
|
49
|
-
- `type` (symbol) - usually `:commit`
|
50
|
-
- `seq` (sequential number)
|
51
|
-
- `time` (Time)
|
52
|
-
- `repo` (string) - DID of the repository (user account)
|
53
66
|
- `commit` - CID of the commit
|
54
67
|
- `prev` - CID of the previous commit in that repo
|
55
68
|
- `operations` - list of operations (usually one)
|
56
69
|
|
70
|
+
Handle messages additionally have:
|
71
|
+
|
72
|
+
- `handle` - the new handle assigned to the DID
|
73
|
+
|
74
|
+
Info messages additionally have:
|
75
|
+
|
76
|
+
- `name` - identifier of the message/error
|
77
|
+
- `message` - a human-readable description
|
78
|
+
|
79
|
+
|
80
|
+
### Commit operations
|
81
|
+
|
57
82
|
Operations are objects of type `Operation` and have such properties:
|
58
83
|
|
59
|
-
- `repo` (string) - DID of the repository (user account)
|
84
|
+
- `repo` or `did` (string) - DID of the repository (user account)
|
60
85
|
- `collection` (string) - name of the relevant collection in the repository, e.g. `app.bsky.feed.post` for posts
|
86
|
+
- `type` (symbol) - short name of the collection, e.g. `:bsky_post`
|
87
|
+
- `rkey` (string) - identifier of a record in a collection
|
61
88
|
- `path` (string) - the path part of the at:// URI - collection name + ID (rkey) of the item
|
89
|
+
- `uri` (string) - the complete at:// URI
|
62
90
|
- `action` (symbol) - `:create`, `:update` or `:delete`
|
63
|
-
- `uri` (string) - the at:// URI
|
64
|
-
- `type` (symbol) - short name of the collection, e.g. `:bsky_post`
|
65
91
|
- `cid` - CID of the operation/record (`nil` for delete operations)
|
66
92
|
|
67
|
-
Create and update operations will also have an attached record (JSON object) with details of the post, like etc. The record data is currently available as a Ruby hash via `raw_record` property (custom types will be added in
|
93
|
+
Create and update operations will also have an attached record (JSON object) with details of the post, like etc. The record data is currently available as a Ruby hash via `raw_record` property (custom types will be added in future).
|
68
94
|
|
69
95
|
So for example, in order to filter only "create post" operations and print their details, you can do something like this:
|
70
96
|
|
@@ -82,7 +108,16 @@ sky.on_message do |m|
|
|
82
108
|
end
|
83
109
|
```
|
84
110
|
|
85
|
-
|
111
|
+
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.
|
112
|
+
|
113
|
+
|
114
|
+
### Custom lexicons
|
115
|
+
|
116
|
+
A note on custom lexicons: the `Skyfall::Operation` objects have two properties that tell you the kind of record they're about: `#collection`, which is a string containing the official name of the collection/lexicon, e.g. `"app.bsky.feed.post"`; and `#type`, which is a symbol meant to save you some typing, e.g. `:bsky_post`.
|
117
|
+
|
118
|
+
When Skyfall receives a message about a record type that's not on the list, whether in the `app.bsky` namespace or not, the operation `type` will be `:unknown`, while the `collection` will be the original string. So if an app like e.g. "Skygram" appears with a `zz.skygram.*` namespace that lets you share photos on ATProto, the operations will have a type `:unknown` and collection names like `zz.skygram.feed.photo`, and you can check the `collection` field for record types known to you and process them in some appropriate way, even if Skyfall doesn't recognize the record type.
|
119
|
+
|
120
|
+
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.
|
86
121
|
|
87
122
|
|
88
123
|
## Credits
|
@@ -0,0 +1,84 @@
|
|
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::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
|
@@ -0,0 +1,53 @@
|
|
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.).
|
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
|
+
sky = Skyfall::Stream.new('bsky.network', :subscribe_repos)
|
21
|
+
|
22
|
+
sky.on_message do |msg|
|
23
|
+
# we're only interested in repo commit messages
|
24
|
+
next if msg.type != :commit
|
25
|
+
|
26
|
+
msg.operations.each do |op|
|
27
|
+
# ignore any operations other than "create post"
|
28
|
+
next unless op.action == :create && op.type == :bsky_post
|
29
|
+
|
30
|
+
text = op.raw_record['text'].to_s.downcase
|
31
|
+
|
32
|
+
if terms.any? { |x| text.include?(x) }
|
33
|
+
owner_handle = get_user_handle(op.repo)
|
34
|
+
puts "\n#{msg.time.getlocal} @#{owner_handle}: #{op.raw_record['text']}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_user_handle(did)
|
40
|
+
url = "https://plc.directory/#{did}"
|
41
|
+
json = JSON.parse(URI.open(url).read)
|
42
|
+
json['alsoKnownAs'][0].gsub('at://', '')
|
43
|
+
end
|
44
|
+
|
45
|
+
sky.on_connect { puts "Connected" }
|
46
|
+
sky.on_disconnect { puts "Disconnected" }
|
47
|
+
sky.on_reconnect { puts "Reconnecting..." }
|
48
|
+
sky.on_error { |e| puts "ERROR: #{e}" }
|
49
|
+
|
50
|
+
# close the connection cleanly on Ctrl+C
|
51
|
+
trap("SIGINT") { sky.disconnect }
|
52
|
+
|
53
|
+
sky.connect
|
@@ -0,0 +1,34 @@
|
|
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::Stream.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
|
@@ -0,0 +1,262 @@
|
|
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::Stream.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
|
data/lib/skyfall/collection.rb
CHANGED
@@ -10,6 +10,7 @@ module Skyfall
|
|
10
10
|
|
11
11
|
require_relative 'commit_message'
|
12
12
|
require_relative 'handle_message'
|
13
|
+
require_relative 'identity_message'
|
13
14
|
require_relative 'info_message'
|
14
15
|
require_relative 'tombstone_message'
|
15
16
|
require_relative 'unknown_message'
|
@@ -25,6 +26,7 @@ module Skyfall
|
|
25
26
|
message_class = case type_object['t']
|
26
27
|
when '#commit' then CommitMessage
|
27
28
|
when '#handle' then HandleMessage
|
29
|
+
when '#identity' then IdentityMessage
|
28
30
|
when '#info' then InfoMessage
|
29
31
|
when '#tombstone' then TombstoneMessage
|
30
32
|
else UnknownMessage
|
data/lib/skyfall/operation.rb
CHANGED
@@ -11,6 +11,8 @@ module Skyfall
|
|
11
11
|
@message.repo
|
12
12
|
end
|
13
13
|
|
14
|
+
alias did repo
|
15
|
+
|
14
16
|
def path
|
15
17
|
@json['path']
|
16
18
|
end
|
@@ -44,6 +46,7 @@ module Skyfall
|
|
44
46
|
when Collection::BSKY_BLOCK then :bsky_block
|
45
47
|
when Collection::BSKY_FEED then :bsky_feed
|
46
48
|
when Collection::BSKY_FOLLOW then :bsky_follow
|
49
|
+
when Collection::BSKY_LABELER then :bsky_labeler
|
47
50
|
when Collection::BSKY_LIKE then :bsky_like
|
48
51
|
when Collection::BSKY_LIST then :bsky_list
|
49
52
|
when Collection::BSKY_LISTBLOCK then :bsky_listblock
|
@@ -55,5 +58,14 @@ module Skyfall
|
|
55
58
|
else :unknown
|
56
59
|
end
|
57
60
|
end
|
61
|
+
|
62
|
+
def inspectable_variables
|
63
|
+
instance_variables - [:@message]
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
vars = inspectable_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
|
68
|
+
"#<#{self.class}:0x#{object_id} #{vars}>"
|
69
|
+
end
|
58
70
|
end
|
59
71
|
end
|
data/lib/skyfall/stream.rb
CHANGED
@@ -18,13 +18,9 @@ module Skyfall
|
|
18
18
|
|
19
19
|
def initialize(server, endpoint, cursor = nil)
|
20
20
|
@endpoint = check_endpoint(endpoint)
|
21
|
-
@
|
22
|
-
@cursor = cursor
|
21
|
+
@root_url = build_root_url(server)
|
22
|
+
@cursor = check_cursor(cursor)
|
23
23
|
@handlers = {}
|
24
|
-
@heartbeat_mutex = Mutex.new
|
25
|
-
@heartbeat_interval = 5
|
26
|
-
@heartbeat_timeout = 30
|
27
|
-
@last_update = nil
|
28
24
|
@auto_reconnect = true
|
29
25
|
@connection_attempts = 0
|
30
26
|
end
|
@@ -37,6 +33,9 @@ module Skyfall
|
|
37
33
|
@handlers[:connecting]&.call(url)
|
38
34
|
@engines_on = true
|
39
35
|
|
36
|
+
@reconnect_timer&.cancel
|
37
|
+
@reconnect_timer = nil
|
38
|
+
|
40
39
|
EM.run do
|
41
40
|
EventMachine.error_handler do |e|
|
42
41
|
@handlers[:error]&.call(e)
|
@@ -49,6 +48,7 @@ module Skyfall
|
|
49
48
|
end
|
50
49
|
|
51
50
|
@ws.on(:message) do |msg|
|
51
|
+
@reconnecting = false
|
52
52
|
@connection_attempts = 0
|
53
53
|
|
54
54
|
data = msg.data.pack('C*')
|
@@ -70,10 +70,12 @@ module Skyfall
|
|
70
70
|
@ws.on(:close) do |e|
|
71
71
|
@ws = nil
|
72
72
|
|
73
|
-
if @auto_reconnect && @engines_on
|
74
|
-
|
73
|
+
if @reconnecting || @auto_reconnect && @engines_on
|
74
|
+
@handlers[:reconnect]&.call
|
75
|
+
|
76
|
+
@reconnect_timer&.cancel
|
77
|
+
@reconnect_timer = EM::Timer.new(reconnect_delay) do
|
75
78
|
@connection_attempts += 1
|
76
|
-
@handlers[:reconnect]&.call
|
77
79
|
connect
|
78
80
|
end
|
79
81
|
else
|
@@ -85,9 +87,17 @@ module Skyfall
|
|
85
87
|
end
|
86
88
|
end
|
87
89
|
|
90
|
+
def reconnect
|
91
|
+
@reconnecting = true
|
92
|
+
@connection_attempts = 0
|
93
|
+
|
94
|
+
@ws ? @ws.close : connect
|
95
|
+
end
|
96
|
+
|
88
97
|
def disconnect
|
89
98
|
return unless EM.reactor_running?
|
90
99
|
|
100
|
+
@reconnecting = false
|
91
101
|
@engines_on = false
|
92
102
|
EM.stop_event_loop
|
93
103
|
end
|
@@ -122,6 +132,15 @@ module Skyfall
|
|
122
132
|
@handlers[:reconnect] = block
|
123
133
|
end
|
124
134
|
|
135
|
+
def inspectable_variables
|
136
|
+
instance_variables - [:@handlers, :@ws]
|
137
|
+
end
|
138
|
+
|
139
|
+
def inspect
|
140
|
+
vars = inspectable_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
|
141
|
+
"#<#{self.class}:0x#{object_id} #{vars}>"
|
142
|
+
end
|
143
|
+
|
125
144
|
|
126
145
|
private
|
127
146
|
|
@@ -134,32 +153,44 @@ module Skyfall
|
|
134
153
|
end
|
135
154
|
|
136
155
|
def build_websocket_url
|
137
|
-
|
138
|
-
|
139
|
-
|
156
|
+
@root_url + "/xrpc/" + @endpoint + (@cursor ? "?cursor=#{@cursor}" : "")
|
157
|
+
end
|
158
|
+
|
159
|
+
def check_cursor(cursor)
|
160
|
+
if cursor.nil?
|
161
|
+
nil
|
162
|
+
elsif cursor.is_a?(Integer) || cursor.is_a?(String) && cursor =~ /^[0-9]+$/
|
163
|
+
cursor.to_i
|
164
|
+
else
|
165
|
+
raise ArgumentError, "Invalid cursor: #{cursor.inspect} - cursor must be an integer number"
|
166
|
+
end
|
140
167
|
end
|
141
168
|
|
142
169
|
def check_endpoint(endpoint)
|
143
170
|
if endpoint.is_a?(String)
|
144
|
-
raise ArgumentError("Invalid endpoint name: #{endpoint}") if endpoint.strip
|
171
|
+
raise ArgumentError.new("Invalid endpoint name: #{endpoint}") if endpoint.strip == '' || !endpoint.include?('.')
|
145
172
|
elsif endpoint.is_a?(Symbol)
|
146
|
-
raise ArgumentError("Unknown endpoint: #{endpoint}") if NAMED_ENDPOINTS[endpoint].nil?
|
173
|
+
raise ArgumentError.new("Unknown endpoint: #{endpoint}") if NAMED_ENDPOINTS[endpoint].nil?
|
147
174
|
endpoint = NAMED_ENDPOINTS[endpoint]
|
148
175
|
else
|
149
|
-
raise ArgumentError
|
176
|
+
raise ArgumentError, "Endpoint should be a string or a symbol"
|
150
177
|
end
|
151
178
|
|
152
179
|
endpoint
|
153
180
|
end
|
154
181
|
|
155
|
-
def
|
182
|
+
def build_root_url(server)
|
156
183
|
if server.is_a?(String)
|
157
|
-
|
184
|
+
if server.start_with?('ws://') || server.start_with?('wss://')
|
185
|
+
server
|
186
|
+
elsif server.strip.empty? || server.include?('/')
|
187
|
+
raise ArgumentError, "Server parameter should be a hostname or a ws:// or wss:// URL"
|
188
|
+
else
|
189
|
+
"wss://#{server}"
|
190
|
+
end
|
158
191
|
else
|
159
|
-
raise ArgumentError
|
192
|
+
raise ArgumentError, "Server parameter should be a string"
|
160
193
|
end
|
161
|
-
|
162
|
-
server
|
163
194
|
end
|
164
195
|
end
|
165
196
|
end
|
data/lib/skyfall/version.rb
CHANGED
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.2.
|
4
|
+
version: 0.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kuba Suder
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base32
|
@@ -79,7 +79,10 @@ files:
|
|
79
79
|
- CHANGELOG.md
|
80
80
|
- LICENSE.txt
|
81
81
|
- README.md
|
82
|
-
- example/
|
82
|
+
- example/block_tracker.rb
|
83
|
+
- example/monitor_phrases.rb
|
84
|
+
- example/print_all_posts.rb
|
85
|
+
- example/push_notifications.rb
|
83
86
|
- lib/skyfall.rb
|
84
87
|
- lib/skyfall/car_archive.rb
|
85
88
|
- lib/skyfall/cid.rb
|
@@ -88,6 +91,7 @@ files:
|
|
88
91
|
- lib/skyfall/extensions.rb
|
89
92
|
- lib/skyfall/messages/commit_message.rb
|
90
93
|
- lib/skyfall/messages/handle_message.rb
|
94
|
+
- lib/skyfall/messages/identity_message.rb
|
91
95
|
- lib/skyfall/messages/info_message.rb
|
92
96
|
- lib/skyfall/messages/tombstone_message.rb
|
93
97
|
- lib/skyfall/messages/unknown_message.rb
|
data/example/firehose.rb
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
lib = File.expand_path('../../lib', __FILE__)
|
4
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
|
6
|
-
require 'skyfall'
|
7
|
-
|
8
|
-
sky = Skyfall::Stream.new('bsky.social', :subscribe_repos)
|
9
|
-
|
10
|
-
sky.on_message do |m|
|
11
|
-
next if m.type != :commit
|
12
|
-
|
13
|
-
m.operations.each do |op|
|
14
|
-
next unless op.action == :create && op.type == :bsky_post
|
15
|
-
|
16
|
-
puts "#{op.repo}:"
|
17
|
-
puts op.raw_record['text']
|
18
|
-
puts
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
sky.on_connect { puts "Connected" }
|
23
|
-
sky.on_disconnect { puts "Disconnected" }
|
24
|
-
sky.on_reconnect { puts "Reconnecting..." }
|
25
|
-
sky.on_error { |e| puts "ERROR: #{e}" }
|
26
|
-
|
27
|
-
sky.connect
|