Atmosfire 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2e059c19199525a9436b51dc419fa63dd881800b3a2bbb976350742da931a131
4
+ data.tar.gz: b19e78694cf71cc16a563d0a8f7e6a863c9f216107cee4226efa1f553dd8ba57
5
+ SHA512:
6
+ metadata.gz: f2abdb67eb28583c69ec45e6d1c8c140dd61b568060fe5dc95f74666cfb4bdb132c9c88fbcad15af46c4bb1f248a5f0a2edc28fd42231b9209909757ec765b6f
7
+ data.tar.gz: 3073642340513a9c597586258dcb26c0451d085c297776730de66e35870c64b29728ddcad01b871019e9b13421712d93e2af7031b7a9c32eeab0100a232bfede
@@ -0,0 +1,73 @@
1
+ # typed: true
2
+
3
+ module Atmosfire
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
+ rulesets = [
12
+ AtUriParser.create_rule(%r{^#{Regexp.escape("https://")}(bsky\.app)/profile/(.+)/post/([\w]+)$}) do |handle, collection, rkey, pds|
13
+ handle.start_with?("did:") ? did = handle : did = resolve_handle(handle, pds)
14
+ AtUri.new(did, "app.bsky.feed.post", rkey)
15
+ end,
16
+
17
+ AtUriParser.create_rule(%r{^at://(.+)/(.+)/(\w+)$}) do |handle, collection, rkey, pds|
18
+ handle.start_with?("did:") ? did = handle : did = resolve_handle(handle, pds)
19
+ AtUri.new(did, collection, rkey)
20
+ end,
21
+
22
+ ]
23
+ AtUriParser.parse(url, rulesets, pds: atp_host)
24
+ end
25
+ end
26
+ end
27
+
28
+ module Atmosfire
29
+ module AtUriParser
30
+ extend T::Sig
31
+ Rule = Struct.new(:pattern, :transform)
32
+
33
+ sig { params(url: String, rulesets: T::Array[Rule], pds: String).returns(T.nilable(AtUri)) }
34
+ def self.parse(url, rulesets, pds: "https://bsky.social")
35
+ rulesets.each do |ruleset|
36
+ match_data = url.match(ruleset.pattern)
37
+ next unless match_data
38
+
39
+ at_uri = ruleset.transform.call(match_data, pds)
40
+ return at_uri if at_uri.is_a?(AtUri)
41
+ end
42
+
43
+ nil
44
+ end
45
+
46
+ def self.create_rule(pattern, &block)
47
+ transform = Proc.new do |match_data, pds|
48
+ block.call(*match_data.captures, pds)
49
+ end
50
+ Rule.new(pattern, transform)
51
+ end
52
+ end
53
+
54
+ AtUri = Struct.new :repo, :collection, :rkey do
55
+ extend T::Sig
56
+
57
+ def initialize(*args)
58
+ if args.count == 1
59
+ parts = args[0].split("/")
60
+ repo, collection, rkey = parts[2], parts[3], parts[4]
61
+ elsif args.count == 3
62
+ repo, collection, rkey = args
63
+ end
64
+ super(repo, collection, rkey)
65
+ end
66
+
67
+ sig { returns(String) }
68
+
69
+ def to_s
70
+ "at://#{repo}/#{collection}/#{rkey}"
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,48 @@
1
+ # typed: true
2
+ module Atmosfire
3
+ class Repo
4
+ Collection = Struct.new :repo, :collection do
5
+ include RequestUtils
6
+ extend T::Sig
7
+
8
+ sig { params(repo: Atmosfire::Repo, lexicon_name: String).void }
9
+
10
+ def initialize(repo, lexicon_name)
11
+ super(repo, lexicon_name)
12
+ end
13
+
14
+ sig { params(limit: Integer).returns(T::Array[Atmosfire::Record]) }
15
+
16
+ def list_records(limit = 10)
17
+ self.repo.pds_endpoint
18
+ .get.com_atproto_repo_listRecords(
19
+ repo: self.repo.did,
20
+ collection: self.collection,
21
+ limit: limit,
22
+ )["records"]
23
+ .map { |record|
24
+ Atmosfire::Record.from_hash(record)
25
+ }
26
+ end
27
+
28
+ sig { returns(String) }
29
+
30
+ def to_s
31
+ "at://#{self.repo.did}/#{self.collection}"
32
+ end
33
+
34
+ sig { params(rkey: String).returns(T.nilable(Atmosfire::Record)) }
35
+
36
+ def [](rkey)
37
+ Atmosfire::Record.from_uri(
38
+ T.must(
39
+ at_uri(
40
+ "at://#{self.repo.did}/#{@collection}/#{rkey}"
41
+ )
42
+ ),
43
+ self.repo.pds
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,84 @@
1
+ # typed: true
2
+ module Atmosfire
3
+ Record = Struct.new :uri, :cid, :timestamp, :content do
4
+ extend T::Sig
5
+ class << self
6
+ extend T::Sig
7
+
8
+ sig { params(json_hash: Hash).returns(T.nilable(Atmosfire::Record)) }
9
+
10
+ def from_hash(json_hash)
11
+ timestamp = nil
12
+ timestamp = Time.parse json_hash["value"]["createdAt"] if json_hash["value"]["createdAt"]
13
+ raw_content = json_hash["value"]
14
+ new(AtUri.new(json_hash["uri"]), json_hash["cid"], timestamp, raw_content)
15
+ end
16
+
17
+ sig { params(uri: Atmosfire::AtUri, pds: String).returns(T.nilable(Atmosfire::Record)) }
18
+
19
+ def from_uri(uri, pds = "https://bsky.social")
20
+ self.from_hash XRPC::Client.new(pds).get.com_atproto_repo_getRecord(
21
+ repo: uri.repo,
22
+ collection: uri.collection,
23
+ rkey: uri.rkey,
24
+ )
25
+ end
26
+
27
+ sig { params(content_hash: Hash, session: Atmosfire::Session, rkey: T.nilable(String)).returns(T.nilable(Atmosfire::Record)) }
28
+
29
+ def create(content_hash, session, rkey = nil)
30
+ return nil if content_hash["$type"].nil?
31
+ if rkey.nil?
32
+ rec = from_uri(session.xrpc.post.com_atproto_repo_createRecord(
33
+ repo: session.did,
34
+ collection: content_hash["$type"],
35
+ record: content_hash,
36
+ )["uri"])
37
+ return rec
38
+ else rec = from_uri(session.xrpc.post.com_atproto_repo_createRecord(
39
+ repo: session.did,
40
+ collection: content_hash["$type"],
41
+ rkey: rkey,
42
+ record: content_hash,
43
+ )["uri"])
44
+ return rec; end
45
+ end
46
+ end
47
+
48
+ def refresh(pds = "https://bsky.social")
49
+ self.class.from_uri(self.uri, pds)
50
+ end
51
+
52
+ sig { params(session: Atmosfire::Session).returns(T.nilable(Atmosfire::Record)) }
53
+
54
+ def update(session)
55
+ self.delete(session)
56
+ session.xrpc.post.com_atproto_repo_createRecord(
57
+ repo: session.did,
58
+ collection: self.uri.collection,
59
+ rkey: self.uri.rkey,
60
+ record: self.content,
61
+ )
62
+ self.class.from_uri(self.uri, session.pds)
63
+ end
64
+
65
+ def put(session)
66
+ session.xrpc.post.com_atproto_repo_putRecord(
67
+ repo: session.did,
68
+ collection: self.uri.collection,
69
+ rkey: self.uri.rkey,
70
+ record: self.content,
71
+ )
72
+ end
73
+
74
+ sig { params(session: Atmosfire::Session).returns(T.nilable(Integer)) }
75
+
76
+ def delete(session)
77
+ session.xrpc.post.com_atproto_repo_deleteRecord(
78
+ repo: session.did,
79
+ collection: self.uri.collection,
80
+ rkey: self.uri.rkey,
81
+ )
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,46 @@
1
+ # typed: true
2
+
3
+ class Atmosfire::Repo
4
+ include Atmosfire::RequestUtils
5
+ extend T::Sig
6
+
7
+ sig { params(username: String, pds: String, open: T::Boolean, authenticate: T.nilable(Atmosfire::Session)).void }
8
+
9
+ def initialize(username, pds = "https://bsky.social", open: true, authenticate: nil)
10
+ @pds = pds
11
+ @pds_endpoint = XRPC::Client.new(pds)
12
+ if username.start_with?("did:")
13
+ @did = username
14
+ else
15
+ @did = resolve_handle(username, pds)
16
+ end
17
+ @record_list = []
18
+ if open == true
19
+ open!
20
+ end
21
+ end
22
+
23
+ def open!
24
+ @collections = describe_repo["collections"]
25
+ end
26
+
27
+ sig { returns(Hash) }
28
+
29
+ def describe_repo
30
+ @pds_endpoint.get.com_atproto_repo_describeRepo(repo: @did)
31
+ end
32
+
33
+ sig { returns(Hash) }
34
+
35
+ def did_document
36
+ describe_repo()["didDoc"]
37
+ end
38
+
39
+ sig { params(collection: String).returns(Atmosfire::Repo::Collection) }
40
+
41
+ def [](collection)
42
+ Collection.new(self, collection)
43
+ end
44
+
45
+ attr_reader :did, :record_list, :pds, :pds_endpoint
46
+ end
@@ -0,0 +1,73 @@
1
+ module Atmosfire
2
+ class Error < StandardError; end
3
+
4
+ class HTTPError < Error; end
5
+
6
+ class UnauthorizedError < HTTPError; end
7
+
8
+ module RequestUtils # Goal is to replace with pure XRPC eventually
9
+ def resolve_handle(username, pds = "https://bsky.social")
10
+ (XRPC::Client.new(pds).get.com_atproto_identity_resolveHandle(handle: username))["did"]
11
+ end
12
+
13
+ def query_obj_to_query_params(q)
14
+ out = "?"
15
+ q.to_h.each do |key, value|
16
+ out += "#{key}=#{value}&" unless value.nil? || (value.class.method_defined?(:empty?) && value.empty?)
17
+ end
18
+ out.slice(0...-1)
19
+ end
20
+
21
+ def default_headers
22
+ { "Content-Type" => "application/json" }
23
+ end
24
+
25
+ def create_session_uri(pds)
26
+ "#{pds}/xrpc/com.atproto.server.createSession"
27
+ end
28
+
29
+ def delete_session_uri(pds)
30
+ "#{pds}/xrpc/com.atproto.server.deleteSession"
31
+ end
32
+
33
+ def refresh_session_uri(pds)
34
+ "#{pds}/xrpc/com.atproto.server.refreshSession"
35
+ end
36
+
37
+ def get_session_uri(pds)
38
+ "#{pds}/xrpc/com.atproto.server.getSession"
39
+ end
40
+
41
+ def create_record_uri(pds)
42
+ "#{pds}/xrpc/com.atproto.repo.createRecord"
43
+ end
44
+
45
+ def delete_record_uri(pds)
46
+ "#{pds}/xrpc/com.atproto.repo.deleteRecord"
47
+ end
48
+
49
+ def mute_actor_uri(pds)
50
+ "#{pds}/xrpc/app.bsky.graph.muteActor"
51
+ end
52
+
53
+ def upload_blob_uri(pds)
54
+ "#{pds}/xrpc/com.atproto.repo.uploadBlob"
55
+ end
56
+
57
+ def get_post_thread_uri(pds, query)
58
+ "#{pds}/xrpc/app.bsky.feed.getPostThread#{query_obj_to_query_params(query)}"
59
+ end
60
+
61
+ def default_authenticated_headers(session)
62
+ default_headers.merge({
63
+ Authorization: "Bearer #{session.access_token}",
64
+ })
65
+ end
66
+
67
+ def refresh_token_headers(session)
68
+ default_headers.merge({
69
+ Authorization: "Bearer #{session.refresh_token}",
70
+ })
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,70 @@
1
+ # typed: true
2
+
3
+ require "atmosfire/requests"
4
+
5
+ module Atmosfire
6
+ Credentials = Struct.new :username, :pw, :pds do
7
+ extend T::Sig
8
+
9
+ sig { params(username: String, pw: String, pds: String).void }
10
+
11
+ def initialize(username, pw, pds = "https://bsky.social")
12
+ super
13
+ self.pds ||= "https://bsky.social"
14
+ end
15
+ end
16
+
17
+ class Session
18
+ include RequestUtils
19
+ extend T::Sig
20
+
21
+ attr_reader :pds, :access_token, :refresh_token, :did, :xrpc
22
+
23
+ sig { params(credentials: Atmosfire::Credentials, should_open: T::Boolean).void }
24
+
25
+ def initialize(credentials, should_open = true)
26
+ @credentials = credentials
27
+ @pds = credentials.pds
28
+ open! if should_open
29
+ end
30
+
31
+ def open!
32
+ @xrpc = XRPC::Client.new(@pds)
33
+ response = @xrpc.post.com_atproto_server_createSession(identifier: @credentials.username, password: @credentials.pw)
34
+
35
+ raise UnauthorizedError if response["accessJwt"].nil?
36
+
37
+ @access_token = response["accessJwt"]
38
+ @refresh_token = response["refreshJwt"]
39
+ @did = response["did"]
40
+
41
+ @xrpc = XRPC::Client.new(@pds, @access_token)
42
+ @refresher = XRPC::Client.new(@pds, @refresh_token)
43
+ end
44
+
45
+ def refresh!
46
+ response = @refresher.post.com_atproto_server_refreshSession
47
+ raise UnauthorizedError if response["accessJwt"].nil?
48
+ @access_token = response["accessJwt"]
49
+ @refresh_token = response["refreshJwt"]
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
@@ -0,0 +1,3 @@
1
+ module Atmosfire
2
+ VERSION = "0.0.1"
3
+ end
data/lib/atmosfire.rb ADDED
@@ -0,0 +1,9 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+ require "sorbet-runtime"
4
+ require "xrpc"
5
+ require "atmosfire/at_uri"
6
+ require "atmosfire/session"
7
+ require "atmosfire/collection"
8
+ require "atmosfire/repo"
9
+ require "atmosfire/record"
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: Atmosfire
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Shreyan Jain
8
+ - Tynan Burke
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2023-07-24 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 atmosfire
29
+ email:
30
+ - shreyan.jain.9@outlook.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - "./lib/atmosfire.rb"
36
+ - "./lib/atmosfire/at_uri.rb"
37
+ - "./lib/atmosfire/collection.rb"
38
+ - "./lib/atmosfire/record.rb"
39
+ - "./lib/atmosfire/repo.rb"
40
+ - "./lib/atmosfire/requests.rb"
41
+ - "./lib/atmosfire/session.rb"
42
+ - "./lib/atmosfire/version.rb"
43
+ homepage: https://github.com/ShreyanJain9/atmosfire
44
+ licenses:
45
+ - MIT
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.4.15
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Interact with the AT Protocol using Ruby
66
+ test_files: []