bskyrb 0.3 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bskyrb
4
+ VERSION = "0.5"
5
+ end
data/lib/bskyrb.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bskyrb/session"
4
+ require "bskyrb/records"
5
+ require "bskyrb/generated_classes"
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.3'
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-04-23 00:00:00.000000000 Z
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 script for interacting with bsky/atproto
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