at_protocol 0.0.4
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.
- 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: []
|