at_protocol 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/at_protocol/at_uri.rb +99 -0
- data/lib/at_protocol/collection.rb +62 -0
- data/lib/at_protocol/record.rb +90 -0
- data/lib/at_protocol/repo.rb +67 -0
- data/lib/at_protocol/requests.rb +125 -0
- data/lib/at_protocol/session.rb +88 -0
- data/lib/at_protocol/version.rb +4 -0
- data/lib/at_protocol/writes.rb +155 -0
- data/lib/at_protocol.rb +27 -0
- metadata +67 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 88028815424d12616955d54125fb23ad627812290c2c52a8d491bb108ebd54fa
|
4
|
+
data.tar.gz: d68b5af3850c75174ef573809f2d87a4012c707d82079601cdd3351da5c317ed
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 37cdd260697f6461ed9215f4d06b932988a292a5fc53afde8d555a1dc04c5355819026742528997d5c2d476a9618ddf396a972f794c73cab11784e6fff39c520
|
7
|
+
data.tar.gz: 5cd9f10e044b3edfdc37ce71abd01ae53fd66bfe4fa9669157d0a2f507e5d6190f3ed4b89490fb012dc9868ac2c7e2722fa35967428934cf1efec697c6def618
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
module ATProto
|
4
|
+
module RequestUtils
|
5
|
+
extend T::Sig
|
6
|
+
include Kernel
|
7
|
+
|
8
|
+
sig { params(url: String, atp_host: String).returns(T.nilable(AtUri)) }
|
9
|
+
|
10
|
+
def at_uri(url, atp_host = "https://bsky.social")
|
11
|
+
AtUriParser.parse(url, AtUriParser::RuleSets, pds: atp_host)
|
12
|
+
end
|
13
|
+
|
14
|
+
module_function :at_uri
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ATProto
|
19
|
+
CID = Skyfall::CID
|
20
|
+
|
21
|
+
module AtUriParser
|
22
|
+
extend T::Sig
|
23
|
+
|
24
|
+
class << self
|
25
|
+
include RequestUtils
|
26
|
+
end
|
27
|
+
|
28
|
+
Rule = Struct.new(:pattern, :transform)
|
29
|
+
|
30
|
+
sig { params(url: T.any(String, AtUri), rulesets: T::Array[Rule], pds: String).returns(T.nilable(AtUri)) }
|
31
|
+
def self.parse(url, rulesets, pds: "https://bsky.social")
|
32
|
+
return url if url.is_a?(AtUri)
|
33
|
+
rulesets.each do |ruleset|
|
34
|
+
match_data = url.match(ruleset.pattern)
|
35
|
+
next unless match_data
|
36
|
+
|
37
|
+
at_uri = ruleset.transform.call(match_data, pds)
|
38
|
+
return at_uri if at_uri.is_a?(AtUri)
|
39
|
+
end
|
40
|
+
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.create_rule(pattern, &block)
|
45
|
+
transform = Proc.new do |match_data, pds|
|
46
|
+
block.call(*match_data.captures, pds)
|
47
|
+
end
|
48
|
+
Rule.new(pattern, transform)
|
49
|
+
end
|
50
|
+
RuleSets = [
|
51
|
+
AtUriParser.create_rule(%r{^#{Regexp.escape("https://")}(bsky\.app)/profile/(.+)/post/([\w]+)$}) do |_, handle, rkey, pds|
|
52
|
+
handle.start_with?("did:") ? did = handle : did = resolve_handle(handle, pds)
|
53
|
+
AtUri.new(repo: did, collection: "app.bsky.feed.post", rkey: rkey)
|
54
|
+
end,
|
55
|
+
|
56
|
+
AtUriParser.create_rule(%r{^#{Regexp.escape("https://")}(bsky\.app)/profile/(.+)$}) do |_, handle, pds|
|
57
|
+
handle.start_with?("did:") ? did = handle : did = resolve_handle(handle, pds)
|
58
|
+
AtUri.new(repo: did)
|
59
|
+
end,
|
60
|
+
|
61
|
+
AtUriParser.create_rule(%r{^at://(.+)/(.+)/(\w+)$}) do |handle, collection, rkey, pds|
|
62
|
+
handle.start_with?("did:") ? did = handle : did = resolve_handle(handle, pds)
|
63
|
+
AtUri.new(repo: did, collection: collection, rkey: rkey)
|
64
|
+
end,
|
65
|
+
AtUriParser.create_rule(%r{^at://(.+)/(.+)$}) do |handle, collection, pds|
|
66
|
+
handle.start_with?("did:") ? did = handle : did = resolve_handle(handle, pds)
|
67
|
+
AtUri.new(repo: did, collection: collection)
|
68
|
+
end,
|
69
|
+
AtUriParser.create_rule(%r{^at://(.+)$}) do |handle, pds|
|
70
|
+
handle.start_with?("did:") ? did = handle : did = resolve_handle(handle, pds)
|
71
|
+
AtUri.new(repo: did)
|
72
|
+
end,
|
73
|
+
|
74
|
+
]
|
75
|
+
end
|
76
|
+
|
77
|
+
class AtUri < T::Struct
|
78
|
+
extend T::Sig
|
79
|
+
const :repo, T.any(ATProto::Repo, String)
|
80
|
+
const :collection, T.nilable(T.any(ATProto::Repo::Collection, String))
|
81
|
+
const :rkey, T.nilable(String)
|
82
|
+
|
83
|
+
def resolve(pds: "https://bsky.social")
|
84
|
+
if @collection.nil?
|
85
|
+
Repo.new(@repo.to_s, pds)
|
86
|
+
elsif @rkey.nil?
|
87
|
+
Repo::Collection.new(Repo.new(@repo.to_s, pds), @collection.to_s, pds)
|
88
|
+
else
|
89
|
+
Record.from_uri(self, pds)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
sig { returns(String) }
|
94
|
+
|
95
|
+
def to_s
|
96
|
+
"at://#{@repo}/#{@collection.nil? ? "" : "#{@collection}/"}#{@rkey}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# typed: true
|
2
|
+
module ATProto
|
3
|
+
class Repo
|
4
|
+
class Collection < T::Struct
|
5
|
+
include RequestUtils
|
6
|
+
include Enumerable
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
const(:repo, ATProto::Repo)
|
10
|
+
const(:collection, String)
|
11
|
+
|
12
|
+
sig { params(limit: Integer).returns(T::Array[ATProto::Record]) }
|
13
|
+
|
14
|
+
def list(limit = 10)
|
15
|
+
self.repo.xrpc
|
16
|
+
.get.com_atproto_repo_listRecords(
|
17
|
+
repo: self.repo.did,
|
18
|
+
collection: self.collection,
|
19
|
+
limit: limit,
|
20
|
+
)["records"]
|
21
|
+
.map { |record|
|
22
|
+
ATProto::Record.from_hash(record)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def list_all()
|
27
|
+
T.must(get_paginated_data(self.repo, :com_atproto_repo_listRecords.to_s, key: "records", params: { repo: self.repo.to_s, collection: self.to_s }, cursor: nil) do |record|
|
28
|
+
ATProto::Record.from_hash(record)
|
29
|
+
end)
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { returns(String) }
|
33
|
+
|
34
|
+
def to_uri
|
35
|
+
"at://#{self.repo.did}/#{self.collection}/"
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { returns(String) }
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@collection
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { params(rkey: String).returns(T.nilable(ATProto::Record)) }
|
45
|
+
|
46
|
+
def [](rkey)
|
47
|
+
ATProto::Record.from_uri(
|
48
|
+
T.must(
|
49
|
+
at_uri(
|
50
|
+
"at://#{self.repo.did}/#{@collection}/#{rkey}"
|
51
|
+
)
|
52
|
+
),
|
53
|
+
self.repo.pds
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def each(&block)
|
58
|
+
list_all.each(&block)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# typed: false
|
2
|
+
module ATProto
|
3
|
+
class Record < T::Struct
|
4
|
+
const(:uri, ATProto::AtUri)
|
5
|
+
const(:cid, Skyfall::CID)
|
6
|
+
const(:timestamp, T.untyped)
|
7
|
+
prop(:content, Hash)
|
8
|
+
extend T::Sig
|
9
|
+
class << self
|
10
|
+
extend T::Sig
|
11
|
+
include RequestUtils
|
12
|
+
|
13
|
+
sig { params(json_hash: Hash).returns(T.nilable(ATProto::Record)) }
|
14
|
+
|
15
|
+
def from_hash(json_hash)
|
16
|
+
return nil if json_hash["value"].nil?
|
17
|
+
timestamp = nil
|
18
|
+
timestamp = Time.parse json_hash["value"]["createdAt"] if json_hash["value"] && json_hash["value"]["createdAt"]
|
19
|
+
raw_content = json_hash["value"]
|
20
|
+
new(
|
21
|
+
uri: at_uri(
|
22
|
+
T.must(json_hash["uri"])
|
23
|
+
),
|
24
|
+
cid: CID.from_json(json_hash["cid"]),
|
25
|
+
timestamp: timestamp, content: raw_content,
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { params(uri: ATProto::AtUri, pds: String).returns(T.nilable(ATProto::Record)) }
|
30
|
+
|
31
|
+
def from_uri(uri, pds = "https://bsky.social")
|
32
|
+
from_hash(XRPC::Client.new(pds).get.com_atproto_repo_getRecord(
|
33
|
+
repo: uri.repo.to_s,
|
34
|
+
collection: "#{uri.collection}",
|
35
|
+
rkey: uri.rkey,
|
36
|
+
))
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { params(content_hash: Hash, session: ATProto::Session, rkey: T.nilable(String)).returns(T.nilable(ATProto::Record)) }
|
40
|
+
|
41
|
+
def create(content_hash, session, rkey = nil)
|
42
|
+
return nil if content_hash["$type"].nil?
|
43
|
+
params = {
|
44
|
+
repo: session.did,
|
45
|
+
collection: content_hash["$type"],
|
46
|
+
record: content_hash,
|
47
|
+
}
|
48
|
+
params[:rkey] = rkey unless rkey.nil?
|
49
|
+
from_uri(at_uri(session.xrpc.post.com_atproto_repo_createRecord(params)["uri"]))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
dynamic_attr_reader(:to_uri) { "at://#{self.uri.repo}/#{self.uri.collection}/#{self.uri.rkey}" }
|
53
|
+
dynamic_attr_reader(:strongref) { StrongRef.new(uri: self.uri, cid: self.cid) }
|
54
|
+
|
55
|
+
def refresh(pds = "https://bsky.social")
|
56
|
+
self.class.from_uri(self.uri, pds)
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { params(session: ATProto::Session).returns(T.nilable(ATProto::Record)) }
|
60
|
+
|
61
|
+
def put(session)
|
62
|
+
session.then(&to_write(:update)).uri.resolve(pds: session.pds)
|
63
|
+
end
|
64
|
+
|
65
|
+
sig { params(session: ATProto::Session).returns(T.nilable(Integer)) }
|
66
|
+
|
67
|
+
def delete(session)
|
68
|
+
session.then(&to_write)
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { params(type: Symbol).returns(ATProto::Writes::Write) }
|
72
|
+
|
73
|
+
def to_write(type = :delete)
|
74
|
+
ATProto::Writes::Write.new(
|
75
|
+
{
|
76
|
+
action: Writes::Write::Action.deserialize(type),
|
77
|
+
value: (self.content if type == :update),
|
78
|
+
collection: self.uri.collection,
|
79
|
+
rkey: self.uri.rkey,
|
80
|
+
}.compact
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
sig { params(other: ATProto::Record).returns(T::Boolean) }
|
85
|
+
|
86
|
+
def ==(other)
|
87
|
+
self.cid.to_s == other.cid.to_s
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
class ATProto::Repo
|
4
|
+
include ATProto::RequestUtils
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { params(username: String, pds: String, open: T::Boolean, authenticate: T.nilable(ATProto::Session)).void }
|
8
|
+
|
9
|
+
# @param username [String] The username or DID (Decentralized Identifier) to use.
|
10
|
+
# @param pds [String] The URL of the personal data server (default: "https://bsky.social").
|
11
|
+
# @param open [Boolean] Whether to open the repository or not (default: true).
|
12
|
+
# @param authenticate [NilClass, Object] Additional authentication data (default: nil).
|
13
|
+
|
14
|
+
def initialize(username, pds = "https://bsky.social", open: true, authenticate: nil)
|
15
|
+
@pds = T.let pds, String
|
16
|
+
@xrpc = T.let(XRPC::Client.new(pds), XRPC::Client)
|
17
|
+
if username.start_with?("did:")
|
18
|
+
@did = T.let(username, String)
|
19
|
+
else
|
20
|
+
@did = T.let(resolve_handle(username, pds), String)
|
21
|
+
end
|
22
|
+
@record_list = []
|
23
|
+
if open == true
|
24
|
+
open!
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def open!
|
29
|
+
@collections = describe_repo["collections"]
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { returns(String) }
|
33
|
+
|
34
|
+
def to_uri
|
35
|
+
"at://#{@did}/"
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { returns(String) }
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@did
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { returns(Hash) }
|
45
|
+
|
46
|
+
def describe_repo
|
47
|
+
@xrpc.get.com_atproto_repo_describeRepo(repo: @did)
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { returns(Hash) }
|
51
|
+
|
52
|
+
def did_document
|
53
|
+
describe_repo()["didDoc"]
|
54
|
+
end
|
55
|
+
|
56
|
+
sig { params(collection: String).returns(ATProto::Repo::Collection) }
|
57
|
+
|
58
|
+
def [](collection)
|
59
|
+
Collection.new(repo: self, collection: collection)
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
"Repo(#{@did})"
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_reader :did, :record_list, :pds, :xrpc
|
67
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# typed: true
|
2
|
+
module ATProto
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
class HTTPError < Error; end
|
6
|
+
|
7
|
+
class UnauthorizedError < HTTPError; end
|
8
|
+
|
9
|
+
module RequestUtils # Goal is to replace with pure XRPC eventually
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
def resolve_handle(username, pds = "https://bsky.social")
|
13
|
+
(XRPC::Client.new(pds).get.com_atproto_identity_resolveHandle(handle: username))["did"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def query_obj_to_query_params(q)
|
17
|
+
out = "?"
|
18
|
+
q.to_h.each do |key, value|
|
19
|
+
out += "#{key}=#{value}&" unless value.nil? || (value.class.method_defined?(:empty?) && value.empty?)
|
20
|
+
end
|
21
|
+
out.slice(0...-1)
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_headers
|
25
|
+
{ "Content-Type" => "application/json" }
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_session_uri(pds)
|
29
|
+
"#{pds}/xrpc/com.atproto.server.createSession"
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_session_uri(pds)
|
33
|
+
"#{pds}/xrpc/com.atproto.server.deleteSession"
|
34
|
+
end
|
35
|
+
|
36
|
+
def refresh_session_uri(pds)
|
37
|
+
"#{pds}/xrpc/com.atproto.server.refreshSession"
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_session_uri(pds)
|
41
|
+
"#{pds}/xrpc/com.atproto.server.getSession"
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete_record_uri(pds)
|
45
|
+
"#{pds}/xrpc/com.atproto.repo.deleteRecord"
|
46
|
+
end
|
47
|
+
|
48
|
+
def mute_actor_uri(pds)
|
49
|
+
"#{pds}/xrpc/app.bsky.graph.muteActor"
|
50
|
+
end
|
51
|
+
|
52
|
+
def upload_blob_uri(pds)
|
53
|
+
"#{pds}/xrpc/com.atproto.repo.uploadBlob"
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_post_thread_uri(pds, query)
|
57
|
+
"#{pds}/xrpc/app.bsky.feed.getPostThread#{query_obj_to_query_params(query)}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def default_authenticated_headers(session)
|
61
|
+
default_headers.merge({
|
62
|
+
Authorization: "Bearer #{session.access_token}",
|
63
|
+
})
|
64
|
+
end
|
65
|
+
|
66
|
+
def refresh_token_headers(session)
|
67
|
+
default_headers.merge({
|
68
|
+
Authorization: "Bearer #{session.refresh_token}",
|
69
|
+
})
|
70
|
+
end
|
71
|
+
|
72
|
+
sig {
|
73
|
+
params(
|
74
|
+
session: T.any(ATProto::Session, ATProto::Repo),
|
75
|
+
method: String,
|
76
|
+
key: String,
|
77
|
+
params: Hash,
|
78
|
+
cursor: T.nilable(
|
79
|
+
String
|
80
|
+
),
|
81
|
+
map_block: T.nilable(Proc),
|
82
|
+
)
|
83
|
+
.returns(T
|
84
|
+
.nilable(
|
85
|
+
Array
|
86
|
+
))
|
87
|
+
}
|
88
|
+
|
89
|
+
def get_paginated_data(session, method, key:, params:, cursor: nil, &map_block)
|
90
|
+
params.merge({ limit: 100, cursor: cursor }).then do |send_data|
|
91
|
+
session.xrpc.get.public_send(method, **send_data).then do |response|
|
92
|
+
response.dig(key).then do |data|
|
93
|
+
if data.nil? || data.empty?
|
94
|
+
return []
|
95
|
+
end
|
96
|
+
|
97
|
+
if block_given?
|
98
|
+
data.map(&map_block).then do |results|
|
99
|
+
response.dig("cursor").then do |next_cursor|
|
100
|
+
if next_cursor.nil?
|
101
|
+
return results
|
102
|
+
else
|
103
|
+
get_paginated_data(session, method, key: key, params: params, cursor: next_cursor, &map_block).then do |next_results|
|
104
|
+
return results + next_results
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
else
|
110
|
+
response.dig("cursor").then do |next_cursor|
|
111
|
+
if next_cursor.nil?
|
112
|
+
return data
|
113
|
+
else
|
114
|
+
get_paginated_data(session, method, key: key, params: params, cursor: next_cursor, &map_block).then do |next_results|
|
115
|
+
return data + next_results
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
module ATProto
|
4
|
+
Credentials = Struct.new :username, :pw, :pds do
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { params(username: String, pw: String, pds: String).void }
|
8
|
+
|
9
|
+
def initialize(username, pw, pds = "https://bsky.social")
|
10
|
+
super
|
11
|
+
self.pds ||= "https://bsky.social"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Session
|
16
|
+
include RequestUtils
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
attr_reader :pds, :access_token, :refresh_token, :did, :xrpc
|
20
|
+
|
21
|
+
sig { params(credentials: ATProto::Credentials, should_open: T::Boolean).void }
|
22
|
+
|
23
|
+
def initialize(credentials, should_open = true)
|
24
|
+
@credentials = credentials
|
25
|
+
@pds = credentials.pds
|
26
|
+
open! if should_open
|
27
|
+
end
|
28
|
+
|
29
|
+
def open!
|
30
|
+
@xrpc = XRPC::Client.new(@pds)
|
31
|
+
response = @xrpc.post.com_atproto_server_createSession(identifier: @credentials.username, password: @credentials.pw)
|
32
|
+
|
33
|
+
raise UnauthorizedError if response["accessJwt"].nil?
|
34
|
+
|
35
|
+
@access_token = response["accessJwt"]
|
36
|
+
@refresh_token = response["refreshJwt"]
|
37
|
+
@did = response["did"]
|
38
|
+
|
39
|
+
@xrpc = XRPC::Client.new(@pds, @access_token)
|
40
|
+
@refresher = XRPC::Client.new(@pds, @refresh_token)
|
41
|
+
end
|
42
|
+
|
43
|
+
def refresh!
|
44
|
+
response = @refresher.post.com_atproto_server_refreshSession
|
45
|
+
raise UnauthorizedError if response["accessJwt"].nil?
|
46
|
+
@access_token = response["accessJwt"]
|
47
|
+
@refresh_token = response["refreshJwt"]
|
48
|
+
@xrpc = XRPC::Client.new(@pds, @access_token)
|
49
|
+
@refresher = XRPC::Client.new(@pds, @refresh_token)
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { returns(T.nilable(Hash)) }
|
53
|
+
|
54
|
+
def get_session
|
55
|
+
@xrpc.get.com_atproto_server_getSession
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete!
|
59
|
+
response = HTTParty.post(
|
60
|
+
URI(delete_session_uri(pds)),
|
61
|
+
headers: refresh_token_headers(self),
|
62
|
+
)
|
63
|
+
if response.code == 200
|
64
|
+
{ success: true }
|
65
|
+
else
|
66
|
+
raise UnauthorizedError
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module ATProto
|
73
|
+
class TokenSession < Session
|
74
|
+
extend T::Sig
|
75
|
+
|
76
|
+
sig { params(token: String, pds: String).void }
|
77
|
+
|
78
|
+
def initialize(token, pds = "https://bsky.social")
|
79
|
+
@token = token
|
80
|
+
@pds = pds
|
81
|
+
open!
|
82
|
+
end
|
83
|
+
|
84
|
+
def open!
|
85
|
+
@xrpc = XRPC::Client.new(@pds, @token)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# typed: true
|
2
|
+
module ATProto
|
3
|
+
class Writes < T::Struct
|
4
|
+
class Write < T::Struct
|
5
|
+
class Action < T::Enum
|
6
|
+
enums do
|
7
|
+
Create = new(:create)
|
8
|
+
Update = new(:update)
|
9
|
+
Delete = new(:delete)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
extend T::Sig
|
14
|
+
prop(:action, Action)
|
15
|
+
prop(:value, T.nilable(Hash))
|
16
|
+
prop(:collection, String)
|
17
|
+
prop(:rkey, T.nilable(String))
|
18
|
+
|
19
|
+
sig { returns(T::Hash[Symbol, T.any(String, Symbol, Hash)]) }
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
{
|
23
|
+
:"$type" => "com.atproto.repo.applyWrites##{self.action.serialize}",
|
24
|
+
action: self.action.serialize,
|
25
|
+
value: self.value || nil,
|
26
|
+
collection: self.collection,
|
27
|
+
rkey: self.rkey || nil,
|
28
|
+
}.compact
|
29
|
+
end
|
30
|
+
|
31
|
+
## If you want to use with individual actions instead of applyWrites:
|
32
|
+
def endpoint_name
|
33
|
+
case self.action
|
34
|
+
when Action::Create
|
35
|
+
"com_atproto_repo_createRecord"
|
36
|
+
when Action::Update
|
37
|
+
"com_atproto_repo_putRecord"
|
38
|
+
when Action::Delete
|
39
|
+
"com_atproto_repo_deleteRecord"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_individual_hash(session)
|
44
|
+
case self.action
|
45
|
+
when Action::Create
|
46
|
+
{
|
47
|
+
repo: session.did,
|
48
|
+
collection: self.collection.to_s,
|
49
|
+
record: self.value,
|
50
|
+
rkey: self.rkey || nil,
|
51
|
+
}.compact
|
52
|
+
when Action::Update
|
53
|
+
{
|
54
|
+
repo: session.did,
|
55
|
+
collection: self.collection.to_s,
|
56
|
+
rkey: self.rkey,
|
57
|
+
record: self.value,
|
58
|
+
}.compact
|
59
|
+
when Action::Delete
|
60
|
+
{
|
61
|
+
repo: session.did,
|
62
|
+
collection: self.collection.to_s,
|
63
|
+
rkey: self.rkey,
|
64
|
+
}.compact
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_proc
|
69
|
+
->session {
|
70
|
+
session.xrpc.post.send(self.endpoint_name, **self.to_individual_hash(session)).then do |response|
|
71
|
+
if response.is_a?(Numeric)
|
72
|
+
response
|
73
|
+
elsif response.is_a?(Hash)
|
74
|
+
ATProto::Record::StrongRef.new(
|
75
|
+
uri: RequestUtils.at_uri(response["uri"]),
|
76
|
+
cid: Skyfall::CID.from_json(response["cid"]),
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
extend T::Sig
|
85
|
+
prop(:writes, T::Array[Write])
|
86
|
+
prop(:repo, ATProto::Repo)
|
87
|
+
prop(:session, ATProto::Session)
|
88
|
+
|
89
|
+
sig { returns(Hash) }
|
90
|
+
|
91
|
+
def to_h
|
92
|
+
{
|
93
|
+
writes: self.writes.map(&:to_h).compact,
|
94
|
+
repo: self.repo.to_s,
|
95
|
+
}.compact
|
96
|
+
end
|
97
|
+
|
98
|
+
def apply
|
99
|
+
self.session.xrpc.post.com_atproto_repo_applyWrites(**to_h)
|
100
|
+
end
|
101
|
+
|
102
|
+
class << self
|
103
|
+
extend T::Sig
|
104
|
+
sig { params(block: Proc).returns(T::Array[Write]) }
|
105
|
+
|
106
|
+
def generate(&block)
|
107
|
+
Collector.new.instance_eval(&block)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Writes
|
113
|
+
class Collector
|
114
|
+
include RequestUtils
|
115
|
+
extend T::Sig
|
116
|
+
|
117
|
+
def initialize
|
118
|
+
@writes = []
|
119
|
+
end
|
120
|
+
|
121
|
+
sig { params(hash: Hash).returns(T::Array[Write]) }
|
122
|
+
|
123
|
+
def create(hash)
|
124
|
+
@writes << Write.new({
|
125
|
+
action: Write::Action::Create,
|
126
|
+
value: hash,
|
127
|
+
collection: hash["$type"] || hash[:"$type"],
|
128
|
+
})
|
129
|
+
end
|
130
|
+
|
131
|
+
sig { params(uri: T.any(String, ATProto::AtUri), hash: Hash).returns(T::Array[Write]) }
|
132
|
+
|
133
|
+
def update(uri, hash)
|
134
|
+
aturi = at_uri(uri)
|
135
|
+
@writes << Write.new({
|
136
|
+
action: Write::Action::Update,
|
137
|
+
value: hash,
|
138
|
+
collection: T.must(aturi).collection.to_s,
|
139
|
+
rkey: T.must(aturi).rkey,
|
140
|
+
})
|
141
|
+
end
|
142
|
+
|
143
|
+
sig { params(uri: T.any(String, ATProto::AtUri)).returns(T::Array[Write]) }
|
144
|
+
|
145
|
+
def delete(uri)
|
146
|
+
aturi = at_uri(uri)
|
147
|
+
@writes << Write.new({
|
148
|
+
action: Write::Action::Delete,
|
149
|
+
collection: T.must(aturi).collection.to_s,
|
150
|
+
rkey: T.must(aturi).rkey,
|
151
|
+
})
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/at_protocol.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
require "sorbet-runtime"
|
4
|
+
|
5
|
+
class Class
|
6
|
+
def dynamic_attr_reader(attr_name, &block)
|
7
|
+
define_method(attr_name) do
|
8
|
+
instance_variable = "@#{attr_name}"
|
9
|
+
if instance_variable_defined?(instance_variable)
|
10
|
+
instance_variable_get(instance_variable)
|
11
|
+
else
|
12
|
+
instance_variable_set(instance_variable, instance_eval(&block))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
require :"skyfall/cid".to_s
|
19
|
+
require "xrpc"
|
20
|
+
require "at_protocol/requests"
|
21
|
+
require "at_protocol/session"
|
22
|
+
require "at_protocol/repo"
|
23
|
+
require "at_protocol/collection"
|
24
|
+
require "at_protocol/at_uri"
|
25
|
+
require "at_protocol/writes"
|
26
|
+
require "at_protocol/record"
|
27
|
+
require "at_protocol/helpers/strongref"
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: at_protocol
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shreyan Jain
|
8
|
+
- Tynan Burke
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2023-08-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: xrpc
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 0.1.5
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 0.1.5
|
28
|
+
description: A Ruby gem for interacting with AT Protocol
|
29
|
+
email:
|
30
|
+
- shreyan.jain.9@outlook.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- "./lib/at_protocol.rb"
|
36
|
+
- "./lib/at_protocol/at_uri.rb"
|
37
|
+
- "./lib/at_protocol/collection.rb"
|
38
|
+
- "./lib/at_protocol/record.rb"
|
39
|
+
- "./lib/at_protocol/repo.rb"
|
40
|
+
- "./lib/at_protocol/requests.rb"
|
41
|
+
- "./lib/at_protocol/session.rb"
|
42
|
+
- "./lib/at_protocol/version.rb"
|
43
|
+
- "./lib/at_protocol/writes.rb"
|
44
|
+
homepage: https://github.com/ShreyanJain9/at_protocol
|
45
|
+
licenses:
|
46
|
+
- MIT
|
47
|
+
metadata: {}
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
requirements: []
|
63
|
+
rubygems_version: 3.4.15
|
64
|
+
signing_key:
|
65
|
+
specification_version: 4
|
66
|
+
summary: Interact with the AT Protocol using Ruby
|
67
|
+
test_files: []
|