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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afde2461017bd9cc0127a64921329a07f884377d5e434dd9b81f15e42493f5b2
4
- data.tar.gz: 29c86293b904fa973cdc2c2a74e17a7f8096ac02c97377d39c9daef9a1cdc194
3
+ metadata.gz: 50342531fa0d4bf18c527038f4644ad15bcbddc7b2468d5d9c9ff7985972f0ce
4
+ data.tar.gz: 0de2603028edb2a51c43e2b3e80f25dca6099f4e48ce7cdbf9be98c447d2e289
5
5
  SHA512:
6
- metadata.gz: 23f5239dddeed66c6ad1a4195d53adea8d7b35d8dbc2a0680d39351a7ca9b5aeb42ef8ccdb38d4d2cd589406e93cc90ae9b5cea9f3ad4f91d3586e3b3e7fc85f
7
- data.tar.gz: fdfce69b885db91ca2efbe19a92cdfc2e11e7fd5ac363be71e47f1e7c4186ba160f9dd2f99ae55950f3404b33a5a72014e3f14e917ceb76cbca7657cce09ddf6
6
+ metadata.gz: 2f434becc1505afd47243f3c6f0ad397fd75e2548e606117980cec20a70215f8dbea3d4bcd8c8520dc4ad2733bfc42187b7fe20344fda5dfc7722e73e97980eb
7
+ data.tar.gz: 3ce2d7f864b6aea377ca518c78d8b7d1fcbe3ff04fe36a6462b005cacc5a56d0b76271412a395727891489695766e452e5bb89bc444a0ca713a163c859a0dc7d
data/CHANGELOG.md CHANGED
@@ -1 +1,31 @@
1
- ## [Unreleased]
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
- TODO: Delete this and the text below, and describe your gem
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
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
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
- Install the gem and add to the application's Gemfile by executing:
10
+ To install the Minisky gem, run the command:
12
11
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
12
+ [sudo] gem install minisky
14
13
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
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
- TODO: Write usage instructions here
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
140
+ ## Credits
26
141
 
27
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
142
+ Copyright © 2023 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/mackuba.eu)).
28
143
 
29
- ## Contributing
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 on GitHub at https://github.com/[USERNAME]/minisky.
146
+ Bug reports and pull requests are welcome 😎
@@ -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
- CONFIG_FILE = 'bluesky.yml'
4
+ DEFAULT_CONFIG_FILE = 'bluesky.yml'
8
5
 
9
- def initialize
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 refresh_token
27
- @config['refresh_token']
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(CONFIG_FILE, YAML.dump(@config))
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
@@ -1,5 +1,5 @@
1
1
  require_relative 'minisky'
2
2
 
3
3
  class Minisky
4
- VERSION = "0.0.1"
4
+ VERSION = "0.2.0"
5
5
  end
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.1
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-08-30 00:00:00.000000000 Z
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