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