Atmosfire 0.0.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.
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: []