bskyrb 0.3 → 0.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/lib/bskyrb/codegen.rb +211 -0
- data/lib/bskyrb/firehose.rb +39 -0
- data/lib/bskyrb/generated_classes.rb +5430 -0
- data/lib/bskyrb/records.rb +231 -0
- data/lib/bskyrb/session.rb +116 -0
- data/lib/bskyrb/version.rb +5 -0
- data/lib/bskyrb.rb +5 -0
- metadata +12 -4
@@ -0,0 +1,231 @@
|
|
1
|
+
module Bskyrb
|
2
|
+
class RecordManager
|
3
|
+
include RequestUtils
|
4
|
+
attr_reader :session
|
5
|
+
|
6
|
+
def initialize(session)
|
7
|
+
@session = session
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_post_by_url(url, depth = 10)
|
11
|
+
# e.g. "https://staging.bsky.app/profile/naia.bsky.social/post/3jszsrnruws27"
|
12
|
+
# regex by chatgpt:
|
13
|
+
query = Bskyrb::AppBskyFeedGetpostthread::GetPostThread::Input.new.tap do |q|
|
14
|
+
q.uri = at_post_link(session.pds, url)
|
15
|
+
q.depth = depth
|
16
|
+
end
|
17
|
+
res = HTTParty.get(
|
18
|
+
get_post_thread_uri(session.pds, query),
|
19
|
+
headers: default_authenticated_headers(session)
|
20
|
+
)
|
21
|
+
Bskyrb::AppBskyFeedDefs::PostView.from_hash res["thread"]["post"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def upload_blob(blob_path, content_type)
|
25
|
+
# only images?
|
26
|
+
image_bytes = File.binread(blob_path)
|
27
|
+
HTTParty.post(
|
28
|
+
upload_blob_uri(session.pds),
|
29
|
+
body: image_bytes,
|
30
|
+
headers: default_authenticated_headers(session)
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_record(input)
|
35
|
+
unless input.is_a?(Hash) || input.class.name.include?("Input")
|
36
|
+
raise "`create_record` takes an Input class or a hash"
|
37
|
+
end
|
38
|
+
HTTParty.post(
|
39
|
+
create_record_uri(session.pds),
|
40
|
+
body: input.to_h.compact.to_json,
|
41
|
+
headers: default_authenticated_headers(session)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def detect_facets(json_hash) # TODO, DOES NOT WORK YET
|
46
|
+
# For some reason this always fails at finding text records and I have no idea why
|
47
|
+
# Detect domain names that have been @mentioned in the text
|
48
|
+
matches = json_hash["record"]["text"].scan(/@([^\s.]+\.[^\s]+)/)
|
49
|
+
|
50
|
+
# Create a facets array to hold the resolved handles
|
51
|
+
facets = []
|
52
|
+
|
53
|
+
# Loop through the matches and resolve the handles
|
54
|
+
matches.each do |match|
|
55
|
+
handle = match[0]
|
56
|
+
resolved_handle = resolve_handle(session.pds, handle)
|
57
|
+
byte_start = json_hash["record"]["text"].index("@" + handle)
|
58
|
+
byte_end = byte_start + handle.length
|
59
|
+
facet = {
|
60
|
+
"$type": "app.bsky.richtext.facet",
|
61
|
+
features: [
|
62
|
+
{
|
63
|
+
"$type": "app.bsky.richtext.facet#mention",
|
64
|
+
did: resolved_handle
|
65
|
+
}
|
66
|
+
],
|
67
|
+
index: {
|
68
|
+
byteStart: byte_start,
|
69
|
+
byteEnd: byte_end
|
70
|
+
}
|
71
|
+
}
|
72
|
+
facets.push(facet)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Append the facets to the JSON hash
|
76
|
+
json_hash["record"]["facets"] = facets
|
77
|
+
|
78
|
+
# Convert the JSON hash back to a string
|
79
|
+
JSON.generate(json_hash)
|
80
|
+
|
81
|
+
# "Doesn't work yet"
|
82
|
+
end
|
83
|
+
|
84
|
+
def create_post(text)
|
85
|
+
input = Bskyrb::ComAtprotoRepoCreaterecord::CreateRecord::Input.from_hash({
|
86
|
+
"collection" => "app.bsky.feed.post",
|
87
|
+
"$type" => "app.bsky.feed.post",
|
88
|
+
"repo" => session.did,
|
89
|
+
"record" => {
|
90
|
+
"$type" => "app.bsky.feed.post",
|
91
|
+
"createdAt" => DateTime.now.iso8601(3),
|
92
|
+
"text" => text
|
93
|
+
}
|
94
|
+
})
|
95
|
+
create_record(input)
|
96
|
+
end
|
97
|
+
|
98
|
+
def create_reply(replylink, text)
|
99
|
+
reply_to = get_post_by_url(replylink)
|
100
|
+
reply_json = {
|
101
|
+
root: {
|
102
|
+
uri: reply_to.uri,
|
103
|
+
cid: reply_to.cid,
|
104
|
+
},
|
105
|
+
parent: {
|
106
|
+
uri: reply_to.uri,
|
107
|
+
cid: reply_to.cid,
|
108
|
+
},
|
109
|
+
collection: "app.bsky.feed.post",
|
110
|
+
repo: session.did,
|
111
|
+
record: {
|
112
|
+
"$type": "app.bsky.feed.post",
|
113
|
+
createdAt: DateTime.now.iso8601(3),
|
114
|
+
text: text
|
115
|
+
}
|
116
|
+
},
|
117
|
+
reply_hash = JSON.parse(reply_json.to_json)
|
118
|
+
reply = Bskyrb::ComAtprotoRepoCreaterecord::CreateRecord::Input.from_hash(reply_hash)
|
119
|
+
create_record(reply)
|
120
|
+
end
|
121
|
+
|
122
|
+
def profile_action(username, type)
|
123
|
+
input = Bskyrb::ComAtprotoRepoCreaterecord::CreateRecord::Input.from_hash({
|
124
|
+
"collection" => type,
|
125
|
+
"repo" => session.did,
|
126
|
+
"record" => {
|
127
|
+
"subject" => resolve_handle(session.pds, username)["did"],
|
128
|
+
"createdAt" => DateTime.now.iso8601(3),
|
129
|
+
"$type" => type
|
130
|
+
}
|
131
|
+
})
|
132
|
+
create_record(input)
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
def post_action(post, action_type)
|
137
|
+
data = {
|
138
|
+
collection: action_type,
|
139
|
+
repo: session.did,
|
140
|
+
record: {
|
141
|
+
subject: {
|
142
|
+
uri: post.uri,
|
143
|
+
cid: post.cid,
|
144
|
+
},
|
145
|
+
createdAt: DateTime.now.iso8601(3),
|
146
|
+
"$type": action_type
|
147
|
+
}
|
148
|
+
}
|
149
|
+
create_record(data)
|
150
|
+
end
|
151
|
+
|
152
|
+
def like(post_url)
|
153
|
+
post = get_post_by_url(post_url)
|
154
|
+
post_action(post, "app.bsky.feed.like")
|
155
|
+
end
|
156
|
+
|
157
|
+
def repost(post_url)
|
158
|
+
post = get_post_by_url(post_url)
|
159
|
+
post_action(post, "app.bsky.feed.repost")
|
160
|
+
end
|
161
|
+
|
162
|
+
def follow(username)
|
163
|
+
profile_action(username, "app.bsky.graph.follow")
|
164
|
+
end
|
165
|
+
|
166
|
+
# def unfollow(username)
|
167
|
+
# profile_action(username, "app.bsky.graph.unfollow(?)")
|
168
|
+
# end
|
169
|
+
#NONE OF THESE WORK
|
170
|
+
# def mute(username)
|
171
|
+
# profile_action(username, "app.bsky.graph.mute")
|
172
|
+
# end
|
173
|
+
|
174
|
+
# def unmute(username)
|
175
|
+
# profile_action(username, "app.bsky.graph.unmute")
|
176
|
+
# end
|
177
|
+
|
178
|
+
|
179
|
+
def block(username)
|
180
|
+
profile_action(username, "app.bsky.graph.block")
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
def get_latest_post(username)
|
185
|
+
feed = get_latest_n_posts(username, 1)
|
186
|
+
feed.feed.first
|
187
|
+
end
|
188
|
+
|
189
|
+
def get_latest_n_posts(username, n)
|
190
|
+
query = Bskyrb::AppBskyFeedGetauthorfeed::GetAuthorFeed::Input.new.tap do |q|
|
191
|
+
q.actor = username
|
192
|
+
q.limit = n
|
193
|
+
end
|
194
|
+
hydrate_feed HTTParty.get(
|
195
|
+
get_author_feed_uri(session.pds, query),
|
196
|
+
headers: default_authenticated_headers(session)
|
197
|
+
), Bskyrb::AppBskyFeedGetauthorfeed::GetAuthorFeed::Output
|
198
|
+
end
|
199
|
+
|
200
|
+
def get_skyline(n)
|
201
|
+
query = Bskyrb::AppBskyFeedGettimeline::GetTimeline::Input.new.tap do |q|
|
202
|
+
q.limit = n
|
203
|
+
end
|
204
|
+
hydrate_feed HTTParty.get(
|
205
|
+
get_timeline_uri(session.pds, query),
|
206
|
+
headers: default_authenticated_headers(session)
|
207
|
+
), Bskyrb::AppBskyFeedGettimeline::GetTimeline::Output
|
208
|
+
end
|
209
|
+
|
210
|
+
def get_popular(n)
|
211
|
+
query = Bskyrb::AppBskyUnspeccedGetpopular::GetPopular::Input.new.tap do |q|
|
212
|
+
q.limit = n
|
213
|
+
end
|
214
|
+
hydrate_feed HTTParty.get(
|
215
|
+
get_popular_uri(session.pds, query),
|
216
|
+
headers: default_authenticated_headers(session)
|
217
|
+
), Bskyrb::AppBskyUnspeccedGetpopular::GetPopular::Output
|
218
|
+
end
|
219
|
+
|
220
|
+
def hydrate_feed(response_hash, klass)
|
221
|
+
klass.from_hash(response_hash).tap do |feed|
|
222
|
+
feed.feed = response_hash["feed"].map do |h|
|
223
|
+
Bskyrb::AppBskyFeedDefs::FeedViewPost.from_hash(h).tap do |obj|
|
224
|
+
obj.post = Bskyrb::AppBskyFeedDefs::PostView.from_hash h["post"]
|
225
|
+
obj.reply = Bskyrb::AppBskyFeedDefs::ReplyRef.from_hash h["reply"] if h["reply"]
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require "uri"
|
2
|
+
require "httparty"
|
3
|
+
|
4
|
+
# def bsky_register(pds, user, password, invcode, email) # lets you create new accounts on a pds directly from ruby
|
5
|
+
# # doesn't go within session class because it wouldn't work unless you already have an account
|
6
|
+
# data = {
|
7
|
+
# "email": email,
|
8
|
+
# "handle": user + ".bsky.social", # defaulting to bsky.social handles because you can't add dns records for a DID that doesn't exist yet
|
9
|
+
# "inviteCode": invcode,
|
10
|
+
# "password": password,
|
11
|
+
# }
|
12
|
+
# resp = HTTParty.post(
|
13
|
+
# "#{pds}/xrpc/com.atproto.server.createAccount",
|
14
|
+
# body: data.to_json,
|
15
|
+
# headers: {'Content-Type' => 'application/json'}
|
16
|
+
# )
|
17
|
+
# resp
|
18
|
+
# end
|
19
|
+
|
20
|
+
module Bskyrb
|
21
|
+
module RequestUtils
|
22
|
+
def resolve_handle(pds, username)
|
23
|
+
HTTParty.get(
|
24
|
+
"#{pds}/xrpc/com.atproto.identity.resolveHandle?handle=#{username}"
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def query_obj_to_query_params(q)
|
29
|
+
out = "?"
|
30
|
+
q.to_h.each do |key, value|
|
31
|
+
out += "#{key}=#{value}&" unless value.nil? || (value.class.method_defined?(:empty?) && value.empty?)
|
32
|
+
end
|
33
|
+
out.slice(0...-1)
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_headers
|
37
|
+
{"Content-Type" => "application/json"}
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_record_uri(pds)
|
41
|
+
"#{pds}/xrpc/com.atproto.repo.createRecord"
|
42
|
+
end
|
43
|
+
|
44
|
+
def upload_blob_uri(pds)
|
45
|
+
"#{pds}/xrpc/com.atproto.repo.uploadBlob"
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_post_thread_uri(pds, query)
|
49
|
+
"#{pds}/xrpc/app.bsky.feed.getPostThread#{query_obj_to_query_params(query)}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_author_feed_uri(pds, query)
|
53
|
+
"#{pds}/xrpc/app.bsky.feed.getAuthorFeed#{query_obj_to_query_params(query)}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_timeline_uri(pds, query)
|
57
|
+
"#{pds}/xrpc/app.bsky.feed.getTimeline#{query_obj_to_query_params(query)}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_popular_uri(pds, query)
|
61
|
+
"#{pds}/xrpc/app.bsky.unspecced.getPopular#{query_obj_to_query_params(query)}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def default_authenticated_headers(session)
|
65
|
+
default_headers.merge({
|
66
|
+
Authorization: "Bearer #{session.access_token}"
|
67
|
+
})
|
68
|
+
end
|
69
|
+
|
70
|
+
def at_post_link(pds, url)
|
71
|
+
# e.g. "https://staging.bsky.app/profile/naia.bsky.social/post/3jszsrnruws27"
|
72
|
+
url = url.to_s
|
73
|
+
# regex by chatgpt:
|
74
|
+
raise "The provided URL #{url} does not match the expected schema" unless /https:\/\/[a-zA-Z0-9.-]+\/profile\/[a-zA-Z0-9.-]+\/post\/[a-zA-Z0-9.-]+/.match?(url)
|
75
|
+
username = url.split("/")[-3]
|
76
|
+
did = resolve_handle(pds, username)["did"]
|
77
|
+
post_id = url.split("/")[-1]
|
78
|
+
"at://#{did}/app.bsky.feed.post/#{post_id}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Credentials
|
83
|
+
attr_reader :username, :pw
|
84
|
+
|
85
|
+
def initialize(username, pw)
|
86
|
+
@username = username
|
87
|
+
@pw = pw
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Session
|
92
|
+
include RequestUtils
|
93
|
+
|
94
|
+
attr_reader :credentials, :pds, :access_token, :refresh_token, :did
|
95
|
+
|
96
|
+
def initialize(credentials, pds, should_open = true)
|
97
|
+
@credentials = credentials
|
98
|
+
@pds = pds
|
99
|
+
open! if should_open
|
100
|
+
end
|
101
|
+
|
102
|
+
def open!
|
103
|
+
uri = URI("#{pds}/xrpc/com.atproto.server.createSession")
|
104
|
+
response = HTTParty.post(
|
105
|
+
uri,
|
106
|
+
body: {identifier: credentials.username, password: credentials.pw}.to_json,
|
107
|
+
headers: default_headers
|
108
|
+
)
|
109
|
+
@access_token = response["accessJwt"]
|
110
|
+
@refresh_token = response["refreshJwt"]
|
111
|
+
@did = response["did"]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
end
|
data/lib/bskyrb.rb
ADDED
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bskyrb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.5'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shreyan Jain
|
8
|
+
- Tynan Burke
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2023-
|
12
|
+
date: 2023-05-02 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: json
|
@@ -66,13 +67,20 @@ dependencies:
|
|
66
67
|
- - ">="
|
67
68
|
- !ruby/object:Gem::Version
|
68
69
|
version: '0'
|
69
|
-
description: A
|
70
|
+
description: A Ruby gem for interacting with bsky/atproto
|
70
71
|
email:
|
71
72
|
- shreyan.jain.9@outlook.com
|
72
73
|
executables: []
|
73
74
|
extensions: []
|
74
75
|
extra_rdoc_files: []
|
75
|
-
files:
|
76
|
+
files:
|
77
|
+
- "./lib/bskyrb.rb"
|
78
|
+
- "./lib/bskyrb/codegen.rb"
|
79
|
+
- "./lib/bskyrb/firehose.rb"
|
80
|
+
- "./lib/bskyrb/generated_classes.rb"
|
81
|
+
- "./lib/bskyrb/records.rb"
|
82
|
+
- "./lib/bskyrb/session.rb"
|
83
|
+
- "./lib/bskyrb/version.rb"
|
76
84
|
homepage: https://github.com/ShreyanJain9/bskyrb
|
77
85
|
licenses:
|
78
86
|
- MIT
|