bskyrb 0.4 → 0.5.1

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,190 @@
1
+ # typed: true
2
+ module Bskyrb
3
+ class RecordManager
4
+ include RequestUtils
5
+ attr_reader :session
6
+
7
+ def initialize(session)
8
+ @session = session
9
+ end
10
+
11
+ def get_post_by_url(url, depth = 10)
12
+ # e.g. "https://staging.bsky.app/profile/naia.bsky.social/post/3jszsrnruws27"
13
+ # or "at://did:plc:scx5mrfxxrqlfzkjcpbt3xfr/app.bsky.feed.post/3jszsrnruws27"
14
+ # regex by chatgpt:
15
+ query = Bskyrb::AppBskyFeedGetpostthread::GetPostThread::Input.new.tap do |q|
16
+ q.uri = at_post_link(session.pds, url)
17
+ q.depth = depth
18
+ end
19
+ res = HTTParty.get(
20
+ get_post_thread_uri(session.pds, query),
21
+ headers: default_authenticated_headers(session),
22
+ )
23
+ Bskyrb::AppBskyFeedDefs::PostView.from_hash res["thread"]["post"]
24
+ end
25
+
26
+ def upload_blob(blob_path, content_type)
27
+ # only images?
28
+ image_bytes = File.binread(blob_path)
29
+ HTTParty.post(
30
+ upload_blob_uri(session.pds),
31
+ body: image_bytes,
32
+ headers: default_authenticated_headers(session),
33
+ )
34
+ end
35
+
36
+ def create_record(input)
37
+ unless input.is_a?(Hash) || input.class.name.include?("Input")
38
+ raise "`create_record` takes an Input class or a hash"
39
+ end
40
+ HTTParty.post(
41
+ create_record_uri(session.pds),
42
+ body: input.to_h.compact.to_json,
43
+ headers: default_authenticated_headers(session),
44
+ )
45
+ end
46
+
47
+ def delete_record(collection, rkey)
48
+ data = { collection: collection, repo: session.did, rkey: rkey }
49
+ HTTParty.post(
50
+ delete_record_uri(session),
51
+ body: data.to_json,
52
+ headers: default_authenticated_headers(session),
53
+ )
54
+ end
55
+
56
+ def create_post(text)
57
+ input = Bskyrb::ComAtprotoRepoCreaterecord::CreateRecord::Input.from_hash({
58
+ "collection" => "app.bsky.feed.post",
59
+ "$type" => "app.bsky.feed.post",
60
+ "repo" => session.did,
61
+ "record" => {
62
+ "$type" => "app.bsky.feed.post",
63
+ "createdAt" => DateTime.now.iso8601(3),
64
+ "text" => text,
65
+ },
66
+ })
67
+ create_record(input)
68
+ end
69
+
70
+ def create_reply(replylink, text)
71
+ reply_to = get_post_by_url(replylink)
72
+ input = Bskyrb::ComAtprotoRepoCreaterecord::CreateRecord::Input.from_hash({
73
+ "collection" => "app.bsky.feed.post",
74
+ "$type" => "app.bsky.feed.post",
75
+ "repo" => session.did,
76
+
77
+ "record" => {
78
+ "reply" => {
79
+ "parent" => {
80
+ "uri" => reply_to.uri,
81
+ "cid" => reply_to.cid,
82
+ },
83
+ "root" => {
84
+ "uri" => reply_to.uri,
85
+ "cid" => reply_to.cid,
86
+ },
87
+ },
88
+ "$type" => "app.bsky.feed.post",
89
+ "createdAt" => DateTime.now.iso8601(3),
90
+ "text" => text,
91
+ },
92
+ })
93
+ create_record(input)
94
+ end
95
+
96
+ def profile_action(username, type)
97
+ input = Bskyrb::ComAtprotoRepoCreaterecord::CreateRecord::Input.from_hash({
98
+ "collection" => type,
99
+ "repo" => session.did,
100
+ "record" => {
101
+ "subject" => resolve_handle(session.pds, username)["did"],
102
+ "createdAt" => DateTime.now.iso8601(3),
103
+ "$type" => type,
104
+ },
105
+ })
106
+ create_record(input)
107
+ end
108
+
109
+ def post_action(post, action_type)
110
+ data = {
111
+ collection: action_type,
112
+ repo: session.did,
113
+ record: {
114
+ subject: {
115
+ uri: post.uri,
116
+ cid: post.cid,
117
+ },
118
+ createdAt: DateTime.now.iso8601(3),
119
+ "$type": action_type,
120
+ },
121
+ }
122
+ create_record(data)
123
+ end
124
+
125
+ def like(post_url)
126
+ post = get_post_by_url(post_url)
127
+ post_action(post, "app.bsky.feed.like")
128
+ end
129
+
130
+ def repost(post_url)
131
+ post = get_post_by_url(post_url)
132
+ post_action(post, "app.bsky.feed.repost")
133
+ end
134
+
135
+ def follow(username)
136
+ profile_action(username, "app.bsky.graph.follow")
137
+ end
138
+
139
+ def block(username)
140
+ profile_action(username, "app.bsky.graph.block")
141
+ end
142
+
143
+ def get_latest_post(username)
144
+ feed = get_latest_n_posts(username, 1)
145
+ feed.feed.first
146
+ end
147
+
148
+ def get_latest_n_posts(username, n)
149
+ query = Bskyrb::AppBskyFeedGetauthorfeed::GetAuthorFeed::Input.new.tap do |q|
150
+ q.actor = username
151
+ q.limit = n
152
+ end
153
+ hydrate_feed HTTParty.get(
154
+ get_author_feed_uri(session.pds, query),
155
+ headers: default_authenticated_headers(session),
156
+ ), Bskyrb::AppBskyFeedGetauthorfeed::GetAuthorFeed::Output
157
+ end
158
+
159
+ def get_skyline(n)
160
+ query = Bskyrb::AppBskyFeedGettimeline::GetTimeline::Input.new.tap do |q|
161
+ q.limit = n
162
+ end
163
+ hydrate_feed HTTParty.get(
164
+ get_timeline_uri(session.pds, query),
165
+ headers: default_authenticated_headers(session),
166
+ ), Bskyrb::AppBskyFeedGettimeline::GetTimeline::Output
167
+ end
168
+
169
+ def get_popular(n)
170
+ query = Bskyrb::AppBskyUnspeccedGetpopular::GetPopular::Input.new.tap do |q|
171
+ q.limit = n
172
+ end
173
+ hydrate_feed HTTParty.get(
174
+ get_popular_uri(session.pds, query),
175
+ headers: default_authenticated_headers(session),
176
+ ), Bskyrb::AppBskyUnspeccedGetpopular::GetPopular::Output
177
+ end
178
+
179
+ def hydrate_feed(response_hash, klass)
180
+ klass.from_hash(response_hash).tap do |feed|
181
+ feed.feed = response_hash["feed"].map do |h|
182
+ Bskyrb::AppBskyFeedDefs::FeedViewPost.from_hash(h).tap do |obj|
183
+ obj.post = Bskyrb::AppBskyFeedDefs::PostView.from_hash h["post"]
184
+ obj.reply = Bskyrb::AppBskyFeedDefs::ReplyRef.from_hash h["reply"] if h["reply"]
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,126 @@
1
+ # typed: false
2
+ require "uri"
3
+ require "httparty"
4
+
5
+ module Bskyrb
6
+ module RequestUtils
7
+ def resolve_handle(pds, username)
8
+ resolveHandle = XRPC::Endpoint.new(pds, "com.atproto.identity.resolveHandle", :handle)
9
+ resolveHandle.get(handle: username)
10
+ end
11
+
12
+ def query_obj_to_query_params(q)
13
+ out = "?"
14
+ q.to_h.each do |key, value|
15
+ out += "#{key}=#{value}&" unless value.nil? || (value.class.method_defined?(:empty?) && value.empty?)
16
+ end
17
+ out.slice(0...-1)
18
+ end
19
+
20
+ def default_headers
21
+ { "Content-Type" => "application/json" }
22
+ end
23
+
24
+ def create_record_uri(pds)
25
+ "#{pds}/xrpc/com.atproto.repo.createRecord"
26
+ end
27
+
28
+ def delete_record_uri(pds)
29
+ "#{pds}/xrpc/com.atproto.repo.deleteRecord"
30
+ end
31
+
32
+ def upload_blob_uri(pds)
33
+ "#{pds}/xrpc/com.atproto.repo.uploadBlob"
34
+ end
35
+
36
+ def get_post_thread_uri(pds, query)
37
+ "#{pds}/xrpc/app.bsky.feed.getPostThread#{query_obj_to_query_params(query)}"
38
+ end
39
+
40
+ def get_author_feed_uri(pds, query)
41
+ "#{pds}/xrpc/app.bsky.feed.getAuthorFeed#{query_obj_to_query_params(query)}"
42
+ end
43
+
44
+ def get_timeline_uri(pds, query)
45
+ "#{pds}/xrpc/app.bsky.feed.getTimeline#{query_obj_to_query_params(query)}"
46
+ end
47
+
48
+ def get_popular_uri(pds, query)
49
+ "#{pds}/xrpc/app.bsky.unspecced.getPopular#{query_obj_to_query_params(query)}"
50
+ end
51
+
52
+ def default_authenticated_headers(session)
53
+ default_headers.merge({
54
+ Authorization: "Bearer #{session.access_token}",
55
+ })
56
+ end
57
+
58
+ def at_post_link(pds, url)
59
+ url = url.to_s
60
+
61
+ # Check if the URL is already in AT format
62
+ if url.start_with?("at://")
63
+ # Validate the username and post ID in the URL
64
+ parts = url.split("/")
65
+ if parts.length == 5 && parts[3] == "app.bsky.feed.post"
66
+ username = parts[2]
67
+ post_id = parts[4]
68
+ if post_id
69
+ return url
70
+ end
71
+ end
72
+
73
+ # If the URL is not valid, raise an error
74
+ raise "The provided URL #{url} is not a valid AT URL"
75
+ end
76
+
77
+ # Validate the format of the regular URL and extract the username and post ID
78
+ regex = /https:\/\/[a-zA-Z0-9.-]+\/profile\/[a-zA-Z0-9.-]+\/post\/[a-zA-Z0-9.-]+/
79
+ raise "The provided URL #{url} does not match the expected schema" unless regex.match?(url)
80
+ username = url.split("/")[-3]
81
+ post_id = url.split("/")[-1]
82
+
83
+ # Validate the username and post ID in the AT URL
84
+ did = resolve_handle(pds, username)["did"]
85
+
86
+ # Construct the AT URL
87
+ "at://#{did}/app.bsky.feed.post/#{post_id}"
88
+ end
89
+ end
90
+
91
+ class Credentials
92
+ attr_reader :username, :pw
93
+
94
+ def initialize(username, pw)
95
+ @username = username
96
+ @pw = pw
97
+ end
98
+ end
99
+
100
+ class Session
101
+ include RequestUtils
102
+
103
+ attr_reader :credentials, :pds, :access_token, :refresh_token, :did
104
+
105
+ def initialize(credentials, pds, should_open = true)
106
+ @credentials = credentials
107
+ @pds = pds
108
+ open! if should_open
109
+ end
110
+
111
+ def open!
112
+ uri = URI("#{pds}/xrpc/com.atproto.server.createSession")
113
+ response = HTTParty.post(
114
+ uri,
115
+ body: { identifier: credentials.username, password: credentials.pw }.to_json,
116
+ headers: default_headers,
117
+ )
118
+
119
+ raise UnauthorizedError if response.code == 401
120
+
121
+ @access_token = response["accessJwt"]
122
+ @refresh_token = response["refreshJwt"]
123
+ @did = response["did"]
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Bskyrb
5
+ VERSION = "0.5.1"
6
+ end
data/lib/bskyrb.rb ADDED
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "bskyrb/error"
5
+ require "bskyrb/session"
6
+ require "bskyrb/records"
7
+ require "bskyrb/generated_classes"
8
+ require "xrpc"
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'
4
+ version: 0.5.1
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-05-01 00:00:00.000000000 Z
12
+ date: 2023-05-15 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: json
@@ -24,20 +25,6 @@ dependencies:
24
25
  - - ">="
25
26
  - !ruby/object:Gem::Version
26
27
  version: '2.0'
27
- - !ruby/object:Gem::Dependency
28
- name: net-http
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
28
  - !ruby/object:Gem::Dependency
42
29
  name: date
43
30
  requirement: !ruby/object:Gem::Requirement
@@ -66,13 +53,21 @@ dependencies:
66
53
  - - ">="
67
54
  - !ruby/object:Gem::Version
68
55
  version: '0'
69
- description: A script for interacting with bsky/atproto
56
+ description: A Ruby gem for interacting with bsky/atproto
70
57
  email:
71
58
  - shreyan.jain.9@outlook.com
72
59
  executables: []
73
60
  extensions: []
74
61
  extra_rdoc_files: []
75
- files: []
62
+ files:
63
+ - "./lib/bskyrb.rb"
64
+ - "./lib/bskyrb/codegen.rb"
65
+ - "./lib/bskyrb/error.rb"
66
+ - "./lib/bskyrb/firehose.rb"
67
+ - "./lib/bskyrb/generated_classes.rb"
68
+ - "./lib/bskyrb/records.rb"
69
+ - "./lib/bskyrb/session.rb"
70
+ - "./lib/bskyrb/version.rb"
76
71
  homepage: https://github.com/ShreyanJain9/bskyrb
77
72
  licenses:
78
73
  - MIT