bskyrb 0.4 → 0.5.1

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