minisky 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -1
- data/README.md +128 -13
- data/lib/minisky/minisky.rb +9 -92
- data/lib/minisky/requests.rb +132 -0
- data/lib/minisky/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50342531fa0d4bf18c527038f4644ad15bcbddc7b2468d5d9c9ff7985972f0ce
|
4
|
+
data.tar.gz: 0de2603028edb2a51c43e2b3e80f25dca6099f4e48ce7cdbf9be98c447d2e289
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f434becc1505afd47243f3c6f0ad397fd75e2548e606117980cec20a70215f8dbea3d4bcd8c8520dc4ad2733bfc42187b7fe20344fda5dfc7722e73e97980eb
|
7
|
+
data.tar.gz: 3ce2d7f864b6aea377ca518c78d8b7d1fcbe3ff04fe36a6462b005cacc5a56d0b76271412a395727891489695766e452e5bb89bc444a0ca713a163c859a0dc7d
|
data/CHANGELOG.md
CHANGED
@@ -1 +1,31 @@
|
|
1
|
-
## [
|
1
|
+
## [0.2.0] - 2023-09-02
|
2
|
+
|
3
|
+
* more consistent handling of parameters in the main methods:
|
4
|
+
- `auth` is now a named parameter
|
5
|
+
- access token is used by default, pass `nil` or an explicit token as `auth` to override
|
6
|
+
- `params` is always optional
|
7
|
+
* progress dots in `#fetch_all`:
|
8
|
+
- default is now to not print anything
|
9
|
+
- pass `'.'` or any other character/string to show progress
|
10
|
+
- set `default_progress` on the client object to use for all `#fetch_all` calls
|
11
|
+
* added `max_pages` option to `#fetch_all`
|
12
|
+
* `#login` and `#perform_token_refresh` methods use the JSON response as return value
|
13
|
+
* renamed `ident` field in the config hash to `id`
|
14
|
+
* config is now accessed in `Requests` from the client object as a `config` property instead of `@config` ivar
|
15
|
+
* config fields are exposed as a `user` wrapper object, e.g. `user.did` delegates to `@config['did']`
|
16
|
+
|
17
|
+
## [0.1.0] - 2023-09-01
|
18
|
+
|
19
|
+
- extracted most code to a `Requests` module that can be included into a different client class with custom config handling
|
20
|
+
- added `#check_access` method
|
21
|
+
- hostname is now passed as a parameter
|
22
|
+
- config file name can be passed as a parameter
|
23
|
+
- added tests
|
24
|
+
|
25
|
+
## [0.0.1] - 2023-08-30
|
26
|
+
|
27
|
+
Initial release - extracted from original gist:
|
28
|
+
|
29
|
+
- logging in and refreshing the token
|
30
|
+
- making GET & POST requests
|
31
|
+
- fetching paginated responses
|
data/README.md
CHANGED
@@ -1,31 +1,146 @@
|
|
1
1
|
# Minisky
|
2
2
|
|
3
|
-
|
3
|
+
Minisky is a minimal client of the Bluesky (ATProto) API. It provides a simple API client class that you can use to log in to the Bluesky API and make any GET and POST requests there. It's meant to be an easy way to start playing and experimenting with the AT Protocol API.
|
4
4
|
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/minisky`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
5
|
|
7
6
|
## Installation
|
8
7
|
|
9
|
-
|
8
|
+
To use Minisky, you need a reasonably new version of Ruby (2.6+). Such version should be preinstalled on macOS Big Sur and above and some Linux systems. Otherwise, you can install one using tools such as [RVM](https://rvm.io), [asdf](https://asdf-vm.com), [ruby-install](https://github.com/postmodern/ruby-install) or [ruby-build](https://github.com/rbenv/ruby-build), or `rpm` or `apt-get` on Linux.
|
10
9
|
|
11
|
-
|
10
|
+
To install the Minisky gem, run the command:
|
12
11
|
|
13
|
-
|
12
|
+
[sudo] gem install minisky
|
14
13
|
|
15
|
-
|
14
|
+
Or alternatively, add it to the `Gemfile` file for Bundler:
|
15
|
+
|
16
|
+
gem 'minisky', '~> 0.2'
|
16
17
|
|
17
|
-
$ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
18
18
|
|
19
19
|
## Usage
|
20
20
|
|
21
|
-
|
21
|
+
First, you need to create a `.yml` config file (by default, `bluesky.yml`) with the authentication data. It should look like this:
|
22
|
+
|
23
|
+
```yaml
|
24
|
+
id: my.bsky.username
|
25
|
+
pass: very-secret-password
|
26
|
+
```
|
27
|
+
|
28
|
+
The `id` can be either your handle, or your DID, or the email you've used to sign up. It's recommended that you use the "app password" that you can create in the settings instead of your main account password.
|
29
|
+
|
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
|
+
|
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):
|
33
|
+
|
34
|
+
```rb
|
35
|
+
require 'minisky'
|
36
|
+
|
37
|
+
bsky = Minisky.new('bsky.social')
|
38
|
+
bsky.check_access
|
39
|
+
```
|
40
|
+
|
41
|
+
`check_access` will check if an access token is saved, if not - it will log you in using the login & password, otherwise it will check if the token is still valid and refresh it if needed.
|
42
|
+
|
43
|
+
Now, you can make requests to the Bluesky API using `get_request` and `post_request`:
|
44
|
+
|
45
|
+
```rb
|
46
|
+
bsky.get_request('com.atproto.repo.listRecords', {
|
47
|
+
repo: bsky.user.did,
|
48
|
+
collection: 'app.bsky.feed.like'
|
49
|
+
})
|
50
|
+
|
51
|
+
bsky.post_request('com.atproto.repo.createRecord', {
|
52
|
+
repo: bsky.user.did,
|
53
|
+
collection: 'app.bsky.feed.post',
|
54
|
+
record: {
|
55
|
+
text: "Hello world!",
|
56
|
+
createdAt: Time.now.iso8601
|
57
|
+
}
|
58
|
+
})
|
59
|
+
```
|
60
|
+
|
61
|
+
The requests use the saved access token for authentication automatically. You can also pass `auth: false` or `auth: nil` to not send any authentication headers, or `auth: sometoken` to use a specific other token.
|
62
|
+
|
63
|
+
The third useful method you can use is `#fetch_all`, which loads multiple paginated responses and collects all returned items on a single list (you need to pass the name of the field that contains the items in the response). Optionally, you can also specify a limit of pages to load as `max_pages: n`, or a break condition `break_when` to stop fetching when any item matches it. You can use it to e.g. to fetch all of your posts from the last 30 days, but not earlier:
|
64
|
+
|
65
|
+
```rb
|
66
|
+
time_limit = Time.now - 86400 * 30
|
67
|
+
|
68
|
+
bsky.fetch_all('com.atproto.repo.listRecords',
|
69
|
+
{ repo: bsky.user.did, collection: 'app.bsky.feed.post' },
|
70
|
+
field: 'records',
|
71
|
+
max_pages: 10,
|
72
|
+
break_when: ->(x) { Time.parse(x['value']['createdAt']) < time_limit })
|
73
|
+
```
|
74
|
+
|
75
|
+
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
|
+
|
77
|
+
```rb
|
78
|
+
bsky.fetch_all('com.atproto.repo.listRecords',
|
79
|
+
{ repo: bsky.user.did, collection: 'app.bsky.feed.like' },
|
80
|
+
field: 'records',
|
81
|
+
progress: '.')
|
82
|
+
```
|
83
|
+
|
84
|
+
This will output a line like this:
|
85
|
+
|
86
|
+
```
|
87
|
+
.................
|
88
|
+
```
|
89
|
+
|
90
|
+
## Customization
|
91
|
+
|
92
|
+
The `Minisky` client currently supports one configuration option:
|
93
|
+
|
94
|
+
- `default_progress` - a progress character to automatically use for `#fetch_all` calls (default: `nil`)
|
95
|
+
|
96
|
+
When creating the `Minisky` instance, you can pass a name of the YAML config file to use instead of the default:
|
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:
|
103
|
+
|
104
|
+
```rb
|
105
|
+
class BlueskyClient
|
106
|
+
include Minisky::Requests
|
107
|
+
|
108
|
+
attr_reader :config
|
109
|
+
|
110
|
+
def initialize(config_file)
|
111
|
+
@config_file = config_file
|
112
|
+
@config = JSON.parse(File.read(@config_file))
|
113
|
+
end
|
114
|
+
|
115
|
+
def host
|
116
|
+
'bsky.social'
|
117
|
+
end
|
118
|
+
|
119
|
+
def save_config
|
120
|
+
File.write(@config_file, JSON.pretty_generate(@config))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
It can then be used just like the `Minisky` class:
|
126
|
+
|
127
|
+
```rb
|
128
|
+
bsky = BlueskyClient.new('config/access.json')
|
129
|
+
bsky.check_access
|
130
|
+
bsky.get_request(...)
|
131
|
+
```
|
132
|
+
|
133
|
+
The class needs to provide:
|
134
|
+
|
135
|
+
- a `host` method or property that returns the hostname of the server
|
136
|
+
- a `config` property which returns a hash or a hash-like object with the configuration and user data - it needs to support reading and writing arbitrary key-value pairs with string keys
|
137
|
+
- a `save_config` method which persists the config object to the chosen storage
|
22
138
|
|
23
|
-
## Development
|
24
139
|
|
25
|
-
|
140
|
+
## Credits
|
26
141
|
|
27
|
-
|
142
|
+
Copyright © 2023 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/mackuba.eu)).
|
28
143
|
|
29
|
-
|
144
|
+
The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
|
30
145
|
|
31
|
-
Bug reports and pull requests are welcome
|
146
|
+
Bug reports and pull requests are welcome 😎
|
data/lib/minisky/minisky.rb
CHANGED
@@ -1,102 +1,19 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'net/http'
|
3
|
-
require 'open-uri'
|
4
1
|
require 'yaml'
|
5
2
|
|
6
3
|
class Minisky
|
7
|
-
|
4
|
+
DEFAULT_CONFIG_FILE = 'bluesky.yml'
|
8
5
|
|
9
|
-
|
10
|
-
@config = YAML.load(File.read(CONFIG_FILE))
|
11
|
-
@base_url = "https://#{@config['host']}/xrpc"
|
12
|
-
end
|
13
|
-
|
14
|
-
def my_id
|
15
|
-
@config['ident']
|
16
|
-
end
|
17
|
-
|
18
|
-
def my_did
|
19
|
-
@config['did']
|
20
|
-
end
|
21
|
-
|
22
|
-
def access_token
|
23
|
-
@config['access_token']
|
24
|
-
end
|
6
|
+
attr_reader :host, :config
|
25
7
|
|
26
|
-
def
|
27
|
-
@
|
8
|
+
def initialize(host, config_file = DEFAULT_CONFIG_FILE)
|
9
|
+
@host = host
|
10
|
+
@config_file = config_file
|
11
|
+
@config = YAML.load(File.read(@config_file))
|
28
12
|
end
|
29
13
|
|
30
14
|
def save_config
|
31
|
-
File.write(
|
32
|
-
end
|
33
|
-
|
34
|
-
def log_in
|
35
|
-
json = post_request('com.atproto.server.createSession', {
|
36
|
-
identifier: @config['ident'],
|
37
|
-
password: @config['pass']
|
38
|
-
})
|
39
|
-
|
40
|
-
@config['did'] = json['did']
|
41
|
-
@config['access_token'] = json['accessJwt']
|
42
|
-
@config['refresh_token'] = json['refreshJwt']
|
43
|
-
save_config
|
44
|
-
end
|
45
|
-
|
46
|
-
def perform_token_refresh
|
47
|
-
json = post_request('com.atproto.server.refreshSession', nil, refresh_token)
|
48
|
-
@config['access_token'] = json['accessJwt']
|
49
|
-
@config['refresh_token'] = json['refreshJwt']
|
50
|
-
save_config
|
51
|
-
end
|
52
|
-
|
53
|
-
def fetch_all(method, params, auth = nil, field:, break_when: ->(x) { false }, progress: true)
|
54
|
-
data = []
|
55
|
-
|
56
|
-
loop do
|
57
|
-
print '.' if progress
|
58
|
-
|
59
|
-
response = get_request(method, params, auth)
|
60
|
-
records = response[field]
|
61
|
-
cursor = response['cursor']
|
62
|
-
|
63
|
-
data.concat(records)
|
64
|
-
params[:cursor] = cursor
|
65
|
-
|
66
|
-
break if cursor.nil? || records.empty? || records.any? { |x| break_when.call(x) }
|
67
|
-
end
|
68
|
-
|
69
|
-
data.reject { |x| break_when.call(x) }
|
70
|
-
end
|
71
|
-
|
72
|
-
def get_request(method, params = nil, auth = nil)
|
73
|
-
headers = {}
|
74
|
-
headers['Authorization'] = "Bearer #{auth}" if auth
|
75
|
-
|
76
|
-
url = "#{@base_url}/#{method}"
|
77
|
-
|
78
|
-
if params && !params.empty?
|
79
|
-
url += "?" + params.map { |k, v|
|
80
|
-
if v.is_a?(Array)
|
81
|
-
v.map { |x| "#{k}=#{x}" }.join('&')
|
82
|
-
else
|
83
|
-
"#{k}=#{v}"
|
84
|
-
end
|
85
|
-
}.join('&')
|
86
|
-
end
|
87
|
-
|
88
|
-
JSON.parse(URI.open(url, headers).read)
|
89
|
-
end
|
90
|
-
|
91
|
-
def post_request(method, params, auth = nil)
|
92
|
-
headers = { "Content-Type" => "application/json" }
|
93
|
-
headers['Authorization'] = "Bearer #{auth}" if auth
|
94
|
-
|
95
|
-
body = params ? params.to_json : ''
|
96
|
-
|
97
|
-
response = Net::HTTP.post(URI("#{@base_url}/#{method}"), body, headers)
|
98
|
-
raise "Invalid response: #{response.code} #{response.body}" if response.code.to_i / 100 != 2
|
99
|
-
|
100
|
-
JSON.parse(response.body)
|
15
|
+
File.write(@config_file, YAML.dump(@config))
|
101
16
|
end
|
102
17
|
end
|
18
|
+
|
19
|
+
require_relative 'requests'
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require_relative 'minisky'
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
require 'open-uri'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
class Minisky
|
9
|
+
class User
|
10
|
+
def initialize(config)
|
11
|
+
@config = config
|
12
|
+
end
|
13
|
+
|
14
|
+
def logged_in?
|
15
|
+
!!(access_token && refresh_token)
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(name)
|
19
|
+
@config[name.to_s]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Requests
|
24
|
+
attr_accessor :default_progress
|
25
|
+
|
26
|
+
def base_url
|
27
|
+
@base_url ||= "https://#{host}/xrpc"
|
28
|
+
end
|
29
|
+
|
30
|
+
def user
|
31
|
+
@user ||= User.new(config)
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_request(method, params = nil, auth: true)
|
35
|
+
headers = authentication_header(auth)
|
36
|
+
url = URI("#{base_url}/#{method}")
|
37
|
+
|
38
|
+
if params && !params.empty?
|
39
|
+
url.query = URI.encode_www_form(params)
|
40
|
+
end
|
41
|
+
|
42
|
+
JSON.parse(URI.open(url, headers).read)
|
43
|
+
end
|
44
|
+
|
45
|
+
def post_request(method, params = nil, auth: true)
|
46
|
+
headers = authentication_header(auth).merge({ "Content-Type" => "application/json" })
|
47
|
+
body = params ? params.to_json : ''
|
48
|
+
|
49
|
+
response = Net::HTTP.post(URI("#{base_url}/#{method}"), body, headers)
|
50
|
+
raise "Invalid response: #{response.code} #{response.body}" if response.code.to_i / 100 != 2
|
51
|
+
|
52
|
+
JSON.parse(response.body)
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch_all(method, params = nil, field:,
|
56
|
+
auth: true, break_when: nil, max_pages: nil, progress: @default_progress)
|
57
|
+
data = []
|
58
|
+
params = {} if params.nil?
|
59
|
+
pages = 0
|
60
|
+
|
61
|
+
loop do
|
62
|
+
print(progress) if progress
|
63
|
+
|
64
|
+
response = get_request(method, params, auth: auth)
|
65
|
+
records = response[field]
|
66
|
+
cursor = response['cursor']
|
67
|
+
|
68
|
+
data.concat(records)
|
69
|
+
params[:cursor] = cursor
|
70
|
+
pages += 1
|
71
|
+
|
72
|
+
break if !cursor || records.empty? || pages == max_pages
|
73
|
+
break if break_when && records.any? { |x| break_when.call(x) }
|
74
|
+
end
|
75
|
+
|
76
|
+
data.delete_if { |x| break_when.call(x) } if break_when
|
77
|
+
data
|
78
|
+
end
|
79
|
+
|
80
|
+
def check_access
|
81
|
+
if !user.logged_in?
|
82
|
+
log_in
|
83
|
+
else
|
84
|
+
begin
|
85
|
+
get_request('com.atproto.server.getSession')
|
86
|
+
rescue OpenURI::HTTPError
|
87
|
+
perform_token_refresh
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def log_in
|
93
|
+
data = {
|
94
|
+
identifier: user.id,
|
95
|
+
password: user.pass
|
96
|
+
}
|
97
|
+
|
98
|
+
json = post_request('com.atproto.server.createSession', data, auth: false)
|
99
|
+
|
100
|
+
config['did'] = json['did']
|
101
|
+
config['access_token'] = json['accessJwt']
|
102
|
+
config['refresh_token'] = json['refreshJwt']
|
103
|
+
|
104
|
+
save_config
|
105
|
+
json
|
106
|
+
end
|
107
|
+
|
108
|
+
def perform_token_refresh
|
109
|
+
json = post_request('com.atproto.server.refreshSession', auth: user.refresh_token)
|
110
|
+
|
111
|
+
config['access_token'] = json['accessJwt']
|
112
|
+
config['refresh_token'] = json['refreshJwt']
|
113
|
+
|
114
|
+
save_config
|
115
|
+
json
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def authentication_header(auth)
|
121
|
+
if auth.is_a?(String)
|
122
|
+
{ 'Authorization' => "Bearer #{auth}" }
|
123
|
+
elsif auth
|
124
|
+
{ 'Authorization' => "Bearer #{user.access_token}" }
|
125
|
+
else
|
126
|
+
{}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
include Requests
|
132
|
+
end
|
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.0
|
4
|
+
version: 0.2.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-09-02 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.
|
@@ -23,6 +23,7 @@ files:
|
|
23
23
|
- README.md
|
24
24
|
- lib/minisky.rb
|
25
25
|
- lib/minisky/minisky.rb
|
26
|
+
- lib/minisky/requests.rb
|
26
27
|
- lib/minisky/version.rb
|
27
28
|
- sig/minisky.rbs
|
28
29
|
homepage: https://github.com/mackuba/minisky
|