minisky 0.2.0 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +17 -15
- data/example/fetch_my_posts.rb +46 -0
- data/example/post_skeet.rb +34 -0
- data/example/science_feed.rb +52 -0
- data/lib/minisky/errors.rb +55 -0
- data/lib/minisky/minisky.rb +14 -5
- data/lib/minisky/requests.rb +102 -22
- data/lib/minisky/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f4836f5b8d8de9c5f9af0368144fe60f274e1b61176d1c919b70d3fd645a3f3
|
4
|
+
data.tar.gz: 8c87cdbb362b19651e848804aa6c551c88abc759bbdd3df6e609b62b94a7da4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84052665f10f23db1513a5135dcb9032f0fbc4c5ae55fc7bbeb15e26d85904acda3ac55170e58d8901e5549ceaaafa34aa038ec8987da9dfdded5349080f12d2
|
7
|
+
data.tar.gz: 5557b97bb51b8feb757a2d16dccfefd7dfd31655a833b379681d89cc924717e234baa19523cda89368f6610481cf11c0dd9494355539d4c7364c9913a05c2ca1
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
## [0.3.0] - 2023-10-05
|
2
|
+
|
3
|
+
* authentication improvements & changes:
|
4
|
+
- Minisky now automatically manages access tokens, calling `check_access` manually is not necessary (set `auto_manage_tokens` to `false` to disable this)
|
5
|
+
- `check_access` now just checks token's expiry time instead of making a request to `getSession`
|
6
|
+
- added `send_auth_headers` option - set to `false` to not set auth header automatically, which is the default
|
7
|
+
- removed default config file name - explicit file name is now required
|
8
|
+
- Minisky can now be used in unauthenticated mode - pass `nil` as the config file name
|
9
|
+
- added `reset_tokens` helper method
|
10
|
+
* refactored response handling - typed errors are now raised on non-success response status
|
11
|
+
* `user` wrapper can also be used for writing fields to the config
|
12
|
+
* improved error handling
|
13
|
+
|
1
14
|
## [0.2.0] - 2023-09-02
|
2
15
|
|
3
16
|
* more consistent handling of parameters in the main methods:
|
data/README.md
CHANGED
@@ -13,12 +13,12 @@ To install the Minisky gem, run the command:
|
|
13
13
|
|
14
14
|
Or alternatively, add it to the `Gemfile` file for Bundler:
|
15
15
|
|
16
|
-
gem 'minisky', '~> 0.
|
16
|
+
gem 'minisky', '~> 0.3'
|
17
17
|
|
18
18
|
|
19
19
|
## Usage
|
20
20
|
|
21
|
-
First, you need to create a `.yml` config file
|
21
|
+
First, you need to create a `.yml` config file with the authentication data, e.g. `bluesky.yml`. It should look like this:
|
22
22
|
|
23
23
|
```yaml
|
24
24
|
id: my.bsky.username
|
@@ -29,12 +29,12 @@ The `id` can be either your handle, or your DID, or the email you've used to sig
|
|
29
29
|
|
30
30
|
After you log in, this file will also be used to store your access & request tokens and DID. The data in the config file can be accessed through a `user` wrapper property that exposes them as methods, e.g. the password is available as `user.pass` and the DID as `user.did`.
|
31
31
|
|
32
|
-
Next, create the Minisky client instance, passing the server name (at the moment there is only one server at `bsky.social`, but there will be more once federation support goes live):
|
32
|
+
Next, create the Minisky client instance, passing the server name and the config file name (at the moment there is only one server at `bsky.social`, but there will be more once federation support goes live):
|
33
33
|
|
34
34
|
```rb
|
35
35
|
require 'minisky'
|
36
36
|
|
37
|
-
bsky = Minisky.new('bsky.social')
|
37
|
+
bsky = Minisky.new('bsky.social', 'bluesky.yml')
|
38
38
|
bsky.check_access
|
39
39
|
```
|
40
40
|
|
@@ -43,17 +43,22 @@ bsky.check_access
|
|
43
43
|
Now, you can make requests to the Bluesky API using `get_request` and `post_request`:
|
44
44
|
|
45
45
|
```rb
|
46
|
-
bsky.get_request('com.atproto.repo.listRecords', {
|
46
|
+
json = bsky.get_request('com.atproto.repo.listRecords', {
|
47
47
|
repo: bsky.user.did,
|
48
48
|
collection: 'app.bsky.feed.like'
|
49
49
|
})
|
50
50
|
|
51
|
+
json['records'].each do |r|
|
52
|
+
puts r['value']['subject']['uri']
|
53
|
+
end
|
54
|
+
|
51
55
|
bsky.post_request('com.atproto.repo.createRecord', {
|
52
56
|
repo: bsky.user.did,
|
53
57
|
collection: 'app.bsky.feed.post',
|
54
58
|
record: {
|
55
59
|
text: "Hello world!",
|
56
|
-
createdAt: Time.now.iso8601
|
60
|
+
createdAt: Time.now.iso8601,
|
61
|
+
langs: ["en"]
|
57
62
|
}
|
58
63
|
})
|
59
64
|
```
|
@@ -65,7 +70,7 @@ The third useful method you can use is `#fetch_all`, which loads multiple pagina
|
|
65
70
|
```rb
|
66
71
|
time_limit = Time.now - 86400 * 30
|
67
72
|
|
68
|
-
bsky.fetch_all('com.atproto.repo.listRecords',
|
73
|
+
posts = bsky.fetch_all('com.atproto.repo.listRecords',
|
69
74
|
{ repo: bsky.user.did, collection: 'app.bsky.feed.post' },
|
70
75
|
field: 'records',
|
71
76
|
max_pages: 10,
|
@@ -75,7 +80,7 @@ bsky.fetch_all('com.atproto.repo.listRecords',
|
|
75
80
|
There is also a `progress` option you can use to print some kind of character for every page load. E.g. pass `progress: '.'` to print dots as the pages are loading:
|
76
81
|
|
77
82
|
```rb
|
78
|
-
bsky.fetch_all('com.atproto.repo.listRecords',
|
83
|
+
likes = bsky.fetch_all('com.atproto.repo.listRecords',
|
79
84
|
{ repo: bsky.user.did, collection: 'app.bsky.feed.like' },
|
80
85
|
field: 'records',
|
81
86
|
progress: '.')
|
@@ -87,19 +92,16 @@ This will output a line like this:
|
|
87
92
|
.................
|
88
93
|
```
|
89
94
|
|
95
|
+
You can find more examples in the [example](https://github.com/mackuba/minisky/tree/master/example) directory.
|
96
|
+
|
97
|
+
|
90
98
|
## Customization
|
91
99
|
|
92
100
|
The `Minisky` client currently supports one configuration option:
|
93
101
|
|
94
102
|
- `default_progress` - a progress character to automatically use for `#fetch_all` calls (default: `nil`)
|
95
103
|
|
96
|
-
|
97
|
-
|
98
|
-
```rb
|
99
|
-
bsky = Minisky.new('bsky.social', 'config/access.yml')
|
100
|
-
```
|
101
|
-
|
102
|
-
Alternatively, instead of using the `Minisky` class, you can make your own class that includes the `Minisky::Requests` module and provides a different way to load & save the config, e.g. from a JSON file:
|
104
|
+
Instead of using the `Minisky` class, you can also make your own class that includes the `Minisky::Requests` module and provides a different way to load & save the config, e.g. from a JSON file:
|
103
105
|
|
104
106
|
```rb
|
105
107
|
class BlueskyClient
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Example: sync all posts from your account (excluding replies and reposts) to a local JSON file. When run again, it
|
4
|
+
# will only fetch new posts since the last time and append them to the file.
|
5
|
+
#
|
6
|
+
# Requires a bluesky.yml config file in the same directory with contents like this:
|
7
|
+
# id: your.handle
|
8
|
+
# pass: secretpass
|
9
|
+
|
10
|
+
# load minisky from a local folder - you normally won't need this
|
11
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
12
|
+
|
13
|
+
require 'minisky'
|
14
|
+
|
15
|
+
CONFIG_FILE = File.join(__dir__, 'bluesky.yml')
|
16
|
+
POSTS_FILE = File.join(__dir__, 'posts.json')
|
17
|
+
|
18
|
+
# create a client instance
|
19
|
+
bsky = Minisky.new('bsky.social', CONFIG_FILE)
|
20
|
+
|
21
|
+
# print progress dots when loading multiple pages
|
22
|
+
bsky.default_progress = '.'
|
23
|
+
|
24
|
+
# load previously saved posts; we will only fetch posts newer than the last saved before
|
25
|
+
posts = File.exist?(POSTS_FILE) ? JSON.parse(File.read(POSTS_FILE)) : []
|
26
|
+
latest_date = posts[0] && posts[0]['indexedAt']
|
27
|
+
|
28
|
+
# fetch all posts from my timeline (without replies) until the target timestamp
|
29
|
+
results = bsky.fetch_all('app.bsky.feed.getAuthorFeed',
|
30
|
+
{ actor: bsky.user.did, filter: 'posts_no_replies', limit: 100 },
|
31
|
+
field: 'feed',
|
32
|
+
break_when: latest_date && proc { |x| x['post']['indexedAt'] <= latest_date }
|
33
|
+
)
|
34
|
+
|
35
|
+
# trim some data to save space
|
36
|
+
new_posts = results.map { |x| x['post'] }
|
37
|
+
.reject { |x| x['author']['did'] != bsky.user.did } # skip reposts
|
38
|
+
.map { |x| x.except('author') } # skip author profile info
|
39
|
+
|
40
|
+
posts = new_posts + posts
|
41
|
+
|
42
|
+
puts
|
43
|
+
puts "Fetched #{new_posts.length} new posts (total = #{posts.length})"
|
44
|
+
|
45
|
+
# save all new and old posts back to the file
|
46
|
+
File.write(POSTS_FILE, JSON.pretty_generate(posts))
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Example: make a new post (aka "skeet") with text passed in the argument to the script.
|
4
|
+
#
|
5
|
+
# Requires a bluesky.yml config file in the same directory with contents like this:
|
6
|
+
# id: your.handle
|
7
|
+
# pass: secretpass
|
8
|
+
|
9
|
+
# load minisky from a local folder - you normally won't need this
|
10
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
11
|
+
|
12
|
+
require 'minisky'
|
13
|
+
|
14
|
+
if ARGV[0].to_s.empty?
|
15
|
+
puts "Usage: #{$PROGRAM_NAME} <text>"
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
|
19
|
+
text = ARGV[0]
|
20
|
+
|
21
|
+
# create a client instance
|
22
|
+
bsky = Minisky.new('bsky.social', File.join(__dir__, 'bluesky.yml'))
|
23
|
+
|
24
|
+
bsky.post_request('com.atproto.repo.createRecord', {
|
25
|
+
repo: bsky.user.did,
|
26
|
+
collection: 'app.bsky.feed.post',
|
27
|
+
record: {
|
28
|
+
text: text,
|
29
|
+
langs: ["en"],
|
30
|
+
createdAt: Time.now.iso8601
|
31
|
+
}
|
32
|
+
})
|
33
|
+
|
34
|
+
puts "Posted ✓"
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Example: load last 10 posts from the "What's Science" feed and print the post text, data and author handle to the
|
4
|
+
# terminal. Does not require any authentication.
|
5
|
+
#
|
6
|
+
# It's definitely not the most efficient way to do this, but it demonstrates how to load single records from the API.
|
7
|
+
|
8
|
+
# load minisky from a local folder - you normally won't need this
|
9
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
10
|
+
|
11
|
+
require 'minisky'
|
12
|
+
require 'time'
|
13
|
+
|
14
|
+
# the "What's Science" custom feed by @bossett.bsky.social
|
15
|
+
# the service host is hardcoded here, ideally you should fetch the feed record and read the hostname from there
|
16
|
+
FEED_HOST = "bs.bossett.io"
|
17
|
+
FEED_URI = "at://did:plc:jfhpnnst6flqway4eaeqzj2a/app.bsky.feed.generator/for-science"
|
18
|
+
|
19
|
+
# fetch the feed from the feed server (getFeedSkeleton returns only a list or URIs of posts)
|
20
|
+
# pass nil as the config file parameter to create an unauthenticated client
|
21
|
+
feed_api = Minisky.new(FEED_HOST, nil)
|
22
|
+
feed = feed_api.get_request('app.bsky.feed.getFeedSkeleton', { feed: FEED_URI, limit: 10 })
|
23
|
+
|
24
|
+
# second client instance for the Bluesky API - again, pass nil to use without authentication
|
25
|
+
bsky = Minisky.new('bsky.social', nil)
|
26
|
+
|
27
|
+
# for each post URI, fetch the post record and the profile of its author
|
28
|
+
entries = feed['feed'].map do |r|
|
29
|
+
# AT URI is always: at://<did>/<collection>/<rkey>
|
30
|
+
did, collection, rkey = r['post'].gsub('at://', '').split('/')
|
31
|
+
|
32
|
+
print '.'
|
33
|
+
post = bsky.get_request('com.atproto.repo.getRecord', { repo: did, collection: collection, rkey: rkey })
|
34
|
+
author = bsky.get_request('com.atproto.repo.describeRepo', { repo: did })
|
35
|
+
|
36
|
+
[post, author]
|
37
|
+
end
|
38
|
+
|
39
|
+
puts
|
40
|
+
|
41
|
+
entries.each do |post, author|
|
42
|
+
handle = author['handle']
|
43
|
+
timestamp = Time.parse(post['value']['createdAt']).getlocal
|
44
|
+
link = "https://bsky.app/profile/#{handle}/post/#{post['uri'].split('/').last}"
|
45
|
+
|
46
|
+
puts "@#{handle} • #{timestamp} • #{link}"
|
47
|
+
puts
|
48
|
+
puts post['value']['text']
|
49
|
+
puts
|
50
|
+
puts "=" * 120
|
51
|
+
puts
|
52
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative 'minisky'
|
2
|
+
|
3
|
+
class Minisky
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class AuthError < Error
|
8
|
+
def initialize(message)
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class BadResponse < Error
|
14
|
+
attr_reader :status, :data
|
15
|
+
|
16
|
+
def initialize(status, status_message, data)
|
17
|
+
@status = status
|
18
|
+
@data = data
|
19
|
+
|
20
|
+
message = if error_message
|
21
|
+
"#{status} #{status_message}: #{error_message}"
|
22
|
+
else
|
23
|
+
"#{status} #{status_message}"
|
24
|
+
end
|
25
|
+
|
26
|
+
super(message)
|
27
|
+
end
|
28
|
+
|
29
|
+
def error_type
|
30
|
+
@data['error'] if @data.is_a?(Hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
def error_message
|
34
|
+
@data['message'] if @data.is_a?(Hash)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class ClientErrorResponse < BadResponse
|
39
|
+
end
|
40
|
+
|
41
|
+
class ServerErrorResponse < BadResponse
|
42
|
+
end
|
43
|
+
|
44
|
+
class ExpiredTokenError < ClientErrorResponse
|
45
|
+
end
|
46
|
+
|
47
|
+
class UnexpectedRedirect < BadResponse
|
48
|
+
attr_reader :location
|
49
|
+
|
50
|
+
def initialize(status, status_message, location)
|
51
|
+
super(status, status_message, { 'message' => "Unexpected redirect: #{location}" })
|
52
|
+
@location = location
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/minisky/minisky.rb
CHANGED
@@ -1,18 +1,27 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
|
3
3
|
class Minisky
|
4
|
-
DEFAULT_CONFIG_FILE = 'bluesky.yml'
|
5
|
-
|
6
4
|
attr_reader :host, :config
|
7
5
|
|
8
|
-
def initialize(host, config_file
|
6
|
+
def initialize(host, config_file)
|
9
7
|
@host = host
|
10
8
|
@config_file = config_file
|
11
|
-
|
9
|
+
|
10
|
+
if @config_file
|
11
|
+
@config = YAML.load(File.read(@config_file))
|
12
|
+
|
13
|
+
if user.id.nil? || user.pass.nil?
|
14
|
+
raise AuthError, "Missing user id or password in the config file #{@config_file}"
|
15
|
+
end
|
16
|
+
else
|
17
|
+
@config = {}
|
18
|
+
@send_auth_headers = false
|
19
|
+
@auto_manage_tokens = false
|
20
|
+
end
|
12
21
|
end
|
13
22
|
|
14
23
|
def save_config
|
15
|
-
File.write(@config_file, YAML.dump(@config))
|
24
|
+
File.write(@config_file, YAML.dump(@config)) if @config_file
|
16
25
|
end
|
17
26
|
end
|
18
27
|
|
data/lib/minisky/requests.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
require_relative 'minisky'
|
2
|
+
require_relative 'errors'
|
2
3
|
|
4
|
+
require 'base64'
|
3
5
|
require 'json'
|
4
6
|
require 'net/http'
|
5
|
-
require '
|
7
|
+
require 'time'
|
6
8
|
require 'uri'
|
7
9
|
|
8
10
|
class Minisky
|
@@ -15,13 +17,27 @@ class Minisky
|
|
15
17
|
!!(access_token && refresh_token)
|
16
18
|
end
|
17
19
|
|
18
|
-
def method_missing(name)
|
19
|
-
|
20
|
+
def method_missing(name, *args)
|
21
|
+
if name.end_with?('=')
|
22
|
+
@config[name.to_s.chop] = args[0]
|
23
|
+
else
|
24
|
+
@config[name.to_s]
|
25
|
+
end
|
20
26
|
end
|
21
27
|
end
|
22
28
|
|
23
29
|
module Requests
|
24
30
|
attr_accessor :default_progress
|
31
|
+
attr_writer :send_auth_headers
|
32
|
+
attr_writer :auto_manage_tokens
|
33
|
+
|
34
|
+
def send_auth_headers
|
35
|
+
instance_variable_defined?('@send_auth_headers') ? @send_auth_headers : true
|
36
|
+
end
|
37
|
+
|
38
|
+
def auto_manage_tokens
|
39
|
+
instance_variable_defined?('@auto_manage_tokens') ? @auto_manage_tokens : true
|
40
|
+
end
|
25
41
|
|
26
42
|
def base_url
|
27
43
|
@base_url ||= "https://#{host}/xrpc"
|
@@ -31,7 +47,9 @@ class Minisky
|
|
31
47
|
@user ||= User.new(config)
|
32
48
|
end
|
33
49
|
|
34
|
-
def get_request(method, params = nil, auth:
|
50
|
+
def get_request(method, params = nil, auth: default_auth_mode)
|
51
|
+
check_access if auto_manage_tokens && auth == true
|
52
|
+
|
35
53
|
headers = authentication_header(auth)
|
36
54
|
url = URI("#{base_url}/#{method}")
|
37
55
|
|
@@ -39,21 +57,22 @@ class Minisky
|
|
39
57
|
url.query = URI.encode_www_form(params)
|
40
58
|
end
|
41
59
|
|
42
|
-
|
60
|
+
response = Net::HTTP.get_response(url, headers)
|
61
|
+
handle_response(response)
|
43
62
|
end
|
44
63
|
|
45
|
-
def post_request(method, params = nil, auth:
|
64
|
+
def post_request(method, params = nil, auth: default_auth_mode)
|
65
|
+
check_access if auto_manage_tokens && auth == true
|
66
|
+
|
46
67
|
headers = authentication_header(auth).merge({ "Content-Type" => "application/json" })
|
47
68
|
body = params ? params.to_json : ''
|
48
69
|
|
49
70
|
response = Net::HTTP.post(URI("#{base_url}/#{method}"), body, headers)
|
50
|
-
|
51
|
-
|
52
|
-
JSON.parse(response.body)
|
71
|
+
handle_response(response)
|
53
72
|
end
|
54
73
|
|
55
74
|
def fetch_all(method, params = nil, field:,
|
56
|
-
auth:
|
75
|
+
auth: default_auth_mode, break_when: nil, max_pages: nil, progress: @default_progress)
|
57
76
|
data = []
|
58
77
|
params = {} if params.nil?
|
59
78
|
pages = 0
|
@@ -80,16 +99,16 @@ class Minisky
|
|
80
99
|
def check_access
|
81
100
|
if !user.logged_in?
|
82
101
|
log_in
|
83
|
-
|
84
|
-
|
85
|
-
get_request('com.atproto.server.getSession')
|
86
|
-
rescue OpenURI::HTTPError
|
87
|
-
perform_token_refresh
|
88
|
-
end
|
102
|
+
elsif token_expiration_date(user.access_token) < Time.now + 60
|
103
|
+
perform_token_refresh
|
89
104
|
end
|
90
105
|
end
|
91
106
|
|
92
107
|
def log_in
|
108
|
+
if user.id.nil? || user.pass.nil?
|
109
|
+
raise AuthError, "To log in, please provide a user id and password"
|
110
|
+
end
|
111
|
+
|
93
112
|
data = {
|
94
113
|
identifier: user.id,
|
95
114
|
password: user.pass
|
@@ -97,35 +116,96 @@ class Minisky
|
|
97
116
|
|
98
117
|
json = post_request('com.atproto.server.createSession', data, auth: false)
|
99
118
|
|
100
|
-
|
101
|
-
|
102
|
-
|
119
|
+
user.did = json['did']
|
120
|
+
user.access_token = json['accessJwt']
|
121
|
+
user.refresh_token = json['refreshJwt']
|
103
122
|
|
104
123
|
save_config
|
105
124
|
json
|
106
125
|
end
|
107
126
|
|
108
127
|
def perform_token_refresh
|
128
|
+
if user.refresh_token.nil?
|
129
|
+
raise AuthError, "Can't refresh access token - refresh token is missing"
|
130
|
+
end
|
131
|
+
|
109
132
|
json = post_request('com.atproto.server.refreshSession', auth: user.refresh_token)
|
110
133
|
|
111
|
-
|
112
|
-
|
134
|
+
user.access_token = json['accessJwt']
|
135
|
+
user.refresh_token = json['refreshJwt']
|
113
136
|
|
114
137
|
save_config
|
115
138
|
json
|
116
139
|
end
|
117
140
|
|
141
|
+
def reset_tokens
|
142
|
+
user.access_token = nil
|
143
|
+
user.refresh_token = nil
|
144
|
+
save_config
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
118
148
|
private
|
119
149
|
|
150
|
+
def default_auth_mode
|
151
|
+
!!send_auth_headers
|
152
|
+
end
|
153
|
+
|
120
154
|
def authentication_header(auth)
|
121
155
|
if auth.is_a?(String)
|
122
156
|
{ 'Authorization' => "Bearer #{auth}" }
|
123
157
|
elsif auth
|
124
|
-
|
158
|
+
if user.access_token
|
159
|
+
{ 'Authorization' => "Bearer #{user.access_token}" }
|
160
|
+
else
|
161
|
+
raise AuthError, "Can't send auth headers, access token is missing"
|
162
|
+
end
|
125
163
|
else
|
126
164
|
{}
|
127
165
|
end
|
128
166
|
end
|
167
|
+
|
168
|
+
def token_expiration_date(token)
|
169
|
+
parts = token.split('.')
|
170
|
+
raise AuthError, "Invalid access token format" unless parts.length == 3
|
171
|
+
|
172
|
+
begin
|
173
|
+
payload = JSON.parse(Base64.decode64(parts[1]))
|
174
|
+
rescue JSON::ParserError
|
175
|
+
raise AuthError, "Couldn't decode payload from access token"
|
176
|
+
end
|
177
|
+
|
178
|
+
exp = payload['exp']
|
179
|
+
raise AuthError, "Invalid token expiry data" unless exp.is_a?(Numeric) && exp > 0
|
180
|
+
|
181
|
+
Time.at(exp)
|
182
|
+
end
|
183
|
+
|
184
|
+
def handle_response(response)
|
185
|
+
status = response.code.to_i
|
186
|
+
message = response.message
|
187
|
+
|
188
|
+
case response
|
189
|
+
when Net::HTTPSuccess
|
190
|
+
JSON.parse(response.body)
|
191
|
+
when Net::HTTPRedirection
|
192
|
+
raise UnexpectedRedirect.new(status, message, response['location'])
|
193
|
+
else
|
194
|
+
data = (response.content_type == 'application/json') ? JSON.parse(response.body) : response.body
|
195
|
+
|
196
|
+
error_class = if data.is_a?(Hash) && data['error'] == 'ExpiredToken'
|
197
|
+
ExpiredTokenError
|
198
|
+
elsif response.is_a?(Net::HTTPClientError)
|
199
|
+
ClientErrorResponse
|
200
|
+
elsif response.is_a?(Net::HTTPServerError)
|
201
|
+
ServerErrorResponse
|
202
|
+
else
|
203
|
+
BadResponse
|
204
|
+
end
|
205
|
+
|
206
|
+
raise error_class.new(status, message, data)
|
207
|
+
end
|
208
|
+
end
|
129
209
|
end
|
130
210
|
|
131
211
|
include Requests
|
data/lib/minisky/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minisky
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kuba Suder
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-10-05 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A very simple client class that lets you log in to the Bluesky API and
|
14
14
|
make any requests there.
|
@@ -21,7 +21,11 @@ files:
|
|
21
21
|
- CHANGELOG.md
|
22
22
|
- LICENSE.txt
|
23
23
|
- README.md
|
24
|
+
- example/fetch_my_posts.rb
|
25
|
+
- example/post_skeet.rb
|
26
|
+
- example/science_feed.rb
|
24
27
|
- lib/minisky.rb
|
28
|
+
- lib/minisky/errors.rb
|
25
29
|
- lib/minisky/minisky.rb
|
26
30
|
- lib/minisky/requests.rb
|
27
31
|
- lib/minisky/version.rb
|