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.
@@ -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