x 0.19.0 → 1.0.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/.yardopts +9 -0
- data/CHANGELOG.md +648 -15
- data/README.md +506 -73
- data/SECURITY.md +66 -0
- data/UPGRADING.md +670 -0
- data/lib/x/version.rb +16 -1
- data/lib/x.rb +13 -1
- data/sig/manifest.yaml +5 -0
- data/sig/x.rbs +5 -361
- metadata +57 -49
- data/bin/console +0 -10
- data/bin/setup +0 -6
- data/lib/x/account_uploader.rb +0 -168
- data/lib/x/authenticator.rb +0 -21
- data/lib/x/bearer_token_authenticator.rb +0 -36
- data/lib/x/client.rb +0 -191
- data/lib/x/client_credentials.rb +0 -208
- data/lib/x/connection.rb +0 -183
- data/lib/x/errors/bad_gateway.rb +0 -6
- data/lib/x/errors/bad_request.rb +0 -6
- data/lib/x/errors/client_error.rb +0 -6
- data/lib/x/errors/connection_exception.rb +0 -6
- data/lib/x/errors/error.rb +0 -4
- data/lib/x/errors/forbidden.rb +0 -6
- data/lib/x/errors/gateway_timeout.rb +0 -6
- data/lib/x/errors/gone.rb +0 -6
- data/lib/x/errors/http_error.rb +0 -84
- data/lib/x/errors/internal_server_error.rb +0 -6
- data/lib/x/errors/invalid_media_type.rb +0 -6
- data/lib/x/errors/network_error.rb +0 -6
- data/lib/x/errors/not_acceptable.rb +0 -6
- data/lib/x/errors/not_found.rb +0 -6
- data/lib/x/errors/payload_too_large.rb +0 -6
- data/lib/x/errors/server_error.rb +0 -6
- data/lib/x/errors/service_unavailable.rb +0 -6
- data/lib/x/errors/too_many_redirects.rb +0 -6
- data/lib/x/errors/too_many_requests.rb +0 -58
- data/lib/x/errors/unauthorized.rb +0 -6
- data/lib/x/errors/unprocessable_entity.rb +0 -6
- data/lib/x/media_upload_validator.rb +0 -36
- data/lib/x/media_uploader.rb +0 -234
- data/lib/x/oauth2_authenticator.rb +0 -169
- data/lib/x/oauth_authenticator.rb +0 -182
- data/lib/x/rate_limit.rb +0 -89
- data/lib/x/redirect_handler.rb +0 -109
- data/lib/x/request_builder.rb +0 -93
- data/lib/x/response_parser.rb +0 -81
- data/lib/x/stream_parser.rb +0 -75
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,620 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-10-06
|
|
9
|
+
|
|
10
|
+
See [UPGRADING.md](https://github.com/sferik/x-ruby/blob/main/UPGRADING.md) for the changes that code written for 0.19 needs.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
* Split the gem into five gems released in lockstep: `x-core`, `x-uploads`, `x-streams`, `x-resources`, and `x`
|
|
14
|
+
* `x-core` is the HTTP client and declares `X::Error`, the base of every error the gems raise
|
|
15
|
+
* `x-uploads` uploads media, profile images, and banners; `x-streams` reads streams and filtered-stream rules
|
|
16
|
+
* `x-resources` holds the resource objects and makes its requests through the client it is given
|
|
17
|
+
* `x` depends on all four and mixes the object, upload, and streaming methods into `X::Client`
|
|
18
|
+
* Every public class is named directly under `X`
|
|
19
|
+
* `x` depends on exactly its own version of the other four; `x-uploads`, `x-streams`, and `x-resources` each depend on `x-core` with `>= 1.0.0, < 2`
|
|
20
|
+
* Add `X::Resources::Error`, `X::Uploads::Error`, and `X::Streams::Error` to rescue the failures of one gem alone
|
|
21
|
+
* Ship a `CHANGELOG.md` with `x-core`, `x-uploads`, `x-streams`, and `x-resources`, which each gemspec's `changelog_uri` names
|
|
22
|
+
* Publish the API documentation of every gem at https://sferik.github.io/x-ruby/api/, which the `documentation_uri` of `x` names
|
|
23
|
+
* Each gem ships a `.yardopts`, so its documentation on rubydoc.info leaves out the private API
|
|
24
|
+
* Add `X.gem_version` and the `gem_version` of `X::Core`, `X::Uploads`, `X::Streams`, and `X::Resources`, each a `Gem::Version`
|
|
25
|
+
* Load `x-uploads` from `require "x"`, so `X::Uploads::MediaUpload` and `X::Uploads::Account` need no further require
|
|
26
|
+
* Add `X::Client#get_stream`, which sends a GET and yields an `X::StreamResponse` before its body is read
|
|
27
|
+
* It reads the `status`, `headers`, and `rate_limit` of the response, and its body with `read_body`
|
|
28
|
+
* `read_body` passes binary chunks to a block, or returns the whole body as a UTF-8 String, nil for a response without one
|
|
29
|
+
* Sends the client's credentials, headers, timeouts, and proxy, and refreshes a rejected token as any request does
|
|
30
|
+
* Asks for a body that is not compressed, which Net::HTTP would hold back in blocks, unless a header names another encoding
|
|
31
|
+
* Raises the `X::HTTPError` of a failed response; otherwise returns what the block returns
|
|
32
|
+
* `read_body` raises `X::NetworkError` for a socket error while reading the body, inside the block, which can rescue it
|
|
33
|
+
* `read_body` reads the body once, inside the block: a second read, unless both read it whole, or one after the block returns, raises `X::Error`, which is not retried
|
|
34
|
+
* A `dup` or `clone` of the response shares its reads, and a frozen response is read as any other
|
|
35
|
+
* An error of the block reaches the caller as raised
|
|
36
|
+
* `x-streams` reads the streams of the API with it
|
|
37
|
+
* Add `X::Client#with_retries`, `memoized`, and `memoize`, kept throughout 1.x for the gems that extend a client
|
|
38
|
+
* `x-uploads` sends each chunk of an upload with `with_retries`; `x-resources` keeps the authenticated user with `memoize`
|
|
39
|
+
* Pass a block to `get`, `post`, `put`, and `delete` to receive the `X::Response` of each response the request gets
|
|
40
|
+
* It is passed what `on_response` is passed, at the same points, refused responses and retries included
|
|
41
|
+
* The client's `on_response` runs first
|
|
42
|
+
* Pass query parameters to `get`, `post`, `put`, `delete`, and `stream` as `params:`
|
|
43
|
+
* nil values are dropped, Arrays are joined with commas, and a `Time` is sent in UTC as ISO 8601
|
|
44
|
+
* Encode a `post` or `put` body that is a Hash or an Array as JSON
|
|
45
|
+
* A body that is not a String, a Hash, or an Array, such as an IO or a Symbol, raises `ArgumentError` before any request
|
|
46
|
+
* Send a form body with the `form:` of `post` and `put`, whose fields are encoded as `params:` are
|
|
47
|
+
* Passing both a body and `form:` raises `ArgumentError`
|
|
48
|
+
* Add the `headers:` option of `X::Client.new`, headers sent with every request and stream of the client
|
|
49
|
+
* A Hash of String or Symbol names to String values; anything else raises `ArgumentError` when the client is built
|
|
50
|
+
* A header passed to a request or `get_stream` replaces the client's, which replaces the gem's default of that name
|
|
51
|
+
* Names are case-insensitive, copies made with `with` keep them, and credential headers are dropped on a cross-origin redirect
|
|
52
|
+
* `X::Client#headers` reads them back frozen, each named by the String it is sent with, as `{"user-agent" => ...}`
|
|
53
|
+
* Token requests carry them too: the app-only token fetch, OAuth 2.0 refreshes, and the code exchange of `X::OAuth2Authorization`
|
|
54
|
+
* The token request's own `Authorization`, `Content-Type`, and `Accept` win, and a client's `Authorization` header is never sent with one
|
|
55
|
+
* Pass an `X::Response` to the `on_response` of a client after every request, and for each object a stream delivers
|
|
56
|
+
* `X::Response#resource_counts`, `rate_limits`, and `rate_limit` read the resources the response returned and the rate limits it reported
|
|
57
|
+
* An `on_response` that is neither nil nor responds to `call` raises `ArgumentError` when the client is built
|
|
58
|
+
* Add `X::Response#headers` and `X::HTTPError#headers`, frozen Hashes with lowercase names and repeated fields comma-joined
|
|
59
|
+
* Read each `Set-Cookie` with `http_response.get_fields("set-cookie")`
|
|
60
|
+
* Retry a request refused for a rate limit up to `max_rate_limit_retries` times (default 0)
|
|
61
|
+
* It waits as long as the refusal asks, up to `max_rate_limit_wait` seconds (default 900), plus up to 5 seconds of jitter
|
|
62
|
+
* A refusal that asks no wait and names no reset waits a minute, doubling for each retry after
|
|
63
|
+
* A refusal for the usage cap of the project, which `X::Problem#usage_capped?` tells, raises at once
|
|
64
|
+
* Retry a failed request up to the `max_retries:` of `X::Client.new` (default 2; 0 raises at once)
|
|
65
|
+
* A `GET`, `PUT`, or `DELETE` is retried after `X::ServerError`, `X::RequestTimeout`, or a network error before it was sent
|
|
66
|
+
* The wait starts at up to a second and doubles, up to a minute, with jitter; a longer `Retry-After` is waited out
|
|
67
|
+
* A `Retry-After` of more than a minute raises at once; a `POST` and any other 4xx are not retried for these, though a 429 and a rejected OAuth 2.0 token are, as the bullets on them say
|
|
68
|
+
* An error raised by `on_response`, a request's block, or `from_response` is never retried
|
|
69
|
+
* Add the defaults of `X::Client` as constants: `DEFAULT_MAX_REDIRECTS`, `DEFAULT_MAX_RATE_LIMIT_RETRIES`, `DEFAULT_MAX_RATE_LIMIT_WAIT`, `DEFAULT_MAX_RETRIES`
|
|
70
|
+
* `X::Client::DEFAULT_OPEN_TIMEOUT`, `DEFAULT_READ_TIMEOUT`, `DEFAULT_WRITE_TIMEOUT`, and `DEFAULT_KEEP_ALIVE_TIMEOUT` replace those of `X::Connection`
|
|
71
|
+
* `X::StreamingClient::DEFAULT_MAX_RECONNECTS` sits beside its `DEFAULT_READ_TIMEOUT`
|
|
72
|
+
* Keep up to 16 idle connections open per host between requests, for `keep_alive_timeout` seconds (default 30)
|
|
73
|
+
* `keep_alive_timeout` is a setting of `X::Client` and `X::OAuth2Authorization`
|
|
74
|
+
* Copies made with `with` that connect the same way share the client's connections
|
|
75
|
+
* `X::Client#close` closes them, and those of the copies that share them; a later request opens one again
|
|
76
|
+
* Copy a client with some options changed with `X::Client#with`, which takes the options of `X::Client.new`
|
|
77
|
+
* e.g. `client.with(base_url: "https://api.x.com/1.1/")`, or `client.with(access_token: nil, access_token_secret: nil)` for app-only
|
|
78
|
+
* A copy given no new credentials shares the client's authenticator, and so its fetched or refreshed tokens
|
|
79
|
+
* A copy that shares an OAuth 2.0 authenticator raises `ArgumentError` for an `expires_at`
|
|
80
|
+
* A copy with other credentials holds `refresh_token`, `expires_at`, `scopes`, `save_tokens`, and `load_tokens` only when given them
|
|
81
|
+
* `X::Client#expires_at` reads the expiration of the last refresh
|
|
82
|
+
* Authenticate a client with an authenticator built elsewhere, given as the `authenticator:` of `X::Client.new` or `with`
|
|
83
|
+
* Raises `ArgumentError` beside credentials or an `expires_at`, or for anything that is not an `X::Authenticator`
|
|
84
|
+
* A custom authenticator subclasses `X::Authenticator` and overrides its public `headers`
|
|
85
|
+
* `headers` is passed a request answering `http_method`, `uri`, `body`, and `[]`, typed as `X::_AuthenticatorRequest`
|
|
86
|
+
* The refreshes of a shared `X::OAuth2Authenticator` reach the `save_tokens` of every client that uses it
|
|
87
|
+
* A client given an `X::OAuth2Authenticator` holds no app credentials, so its `app_only` raises `X::UnsupportedOperation`
|
|
88
|
+
* Add `X::Client#app_only`, a copy of a client that authenticates as the app
|
|
89
|
+
* It sends the client's bearer token, or one it fetches once with the API key and secret, shared with copies
|
|
90
|
+
* It raises `X::UnsupportedOperation` for an OAuth 2.0 user client that holds no bearer token, API key, or secret
|
|
91
|
+
* Authenticate as the app given an `api_key` and `api_key_secret` without access tokens, through `X::AppOnlyAuthenticator`
|
|
92
|
+
* It fetches a bearer token on the first request, and fetches another and resends once when the API answers 401
|
|
93
|
+
* Refresh an OAuth 2.0 access token when it expires, given the `expires_at:` (a `Time`) of `X::Client`, or when the API answers 401
|
|
94
|
+
* The request is sent again with the new token; a token refreshed less than a minute ago is not refreshed again
|
|
95
|
+
* A 401 from another origin refreshes nothing
|
|
96
|
+
* Store the tokens of each OAuth 2.0 refresh with the `save_tokens:` of `X::Client.new`, a callable passed an `X::OAuth2Tokens`
|
|
97
|
+
* It is called once the refresh releases its lock, in order, skipping a refresh another has replaced
|
|
98
|
+
* If a hook raises, the others still run, then `X::TokenReportFailed` is raised, holding the `tokens` and `client`
|
|
99
|
+
* So does a `Timeout::Error` a hook raises, even one `Timeout.timeout(5, Timeout::Error)` raises around the request
|
|
100
|
+
* A `save_tokens` that does not respond to `call` raises `ArgumentError` when the client is built
|
|
101
|
+
* An authenticator built by hand reports its refreshes to no hook of its own, only to those of the clients it is given to
|
|
102
|
+
* Add `X::OAuth2Tokens`, the frozen `access_token`, `refresh_token`, `expires_at`, and `scopes` of a refresh or code exchange
|
|
103
|
+
* `as_json`/`to_json` write it, and `X::OAuth2Tokens.from_json` reads it back, with String or Symbol keys
|
|
104
|
+
* It marshals, and writes YAML, in a versioned format every 1.x release reads
|
|
105
|
+
* `X::OAuth2Tokens.new` raises `ArgumentError` for an empty or non-String token or an `expires_at` that is not a `Time`
|
|
106
|
+
* Share a user's tokens among processes with `load_tokens:`, a callable that returns the stored `X::OAuth2Tokens` or nil
|
|
107
|
+
* Taken by `X::Client.new`, `X::Client#with`, `X::OAuth2Authorization#client`, and `X::OAuth2Authenticator.new`
|
|
108
|
+
* A refresh reads the store first and takes newer tokens there rather than spend its own refresh token
|
|
109
|
+
* A refresh refused with `invalid_request` or `invalid_grant` reads the store again before raising
|
|
110
|
+
* One that does not respond to `call` raises `ArgumentError`; one that returns anything else raises `TypeError`
|
|
111
|
+
* Read the scopes X granted an OAuth 2.0 token with `scopes` on `X::OAuth2Tokens`, `X::OAuth2Authenticator`, and `X::Client`
|
|
112
|
+
* A frozen Array of Strings, or nil when not known
|
|
113
|
+
* `X::Client.new`, `with`, and `X::OAuth2Authenticator.new` take `scopes:`, so `X::Client.new(client_id:, **tokens.to_h)` works
|
|
114
|
+
* Scopes that are not an Array of scope names, or given beside other than OAuth 2.0 credentials, raise `ArgumentError`
|
|
115
|
+
* Authorize an app with the OAuth 2.0 authorization code flow and PKCE with `X::OAuth2Authorization`
|
|
116
|
+
* It builds the authorization URL, and exchanges the code of the redirect for `tokens` or a `client`
|
|
117
|
+
* The tokens hold no app credentials; build a client as `X::Client.new(client_id:, client_secret:, **tokens.to_h)`
|
|
118
|
+
* It raises `X::AuthorizationDenied` when the user declines, the state does not match, or the redirect is invalid
|
|
119
|
+
* It raises `X::AuthorizationError` when X refuses the code, and `ArgumentError` for a state, client ID, or redirect URI that is nil, empty, or not a String
|
|
120
|
+
* `client` checks its options before spending the code, and passes the first tokens to its `save_tokens`
|
|
121
|
+
* It takes `headers:`, which the code is exchanged with and the client it builds is given, as a gateway may require
|
|
122
|
+
* Refresh the tokens of a public OAuth 2.0 client, given a `client_id`, `access_token`, and `refresh_token` without a `client_secret`
|
|
123
|
+
* Authenticate with an OAuth 2.0 access token that is not refreshed, given a `client_id` and `access_token` without a `refresh_token`
|
|
124
|
+
* An access token the API rejects raises `X::Unauthorized`, and `refresh!` raises `X::UnsupportedOperation`
|
|
125
|
+
* Add `X::Authenticator#user_id`, the user an OAuth 1.0a access token acts for, or nil
|
|
126
|
+
* Add `inspect` to `X::Client` and the authenticators that never reveals credentials
|
|
127
|
+
* Add `X::HTTPError#status`, `body`, `problems`, and `problem`, and `http_response`, an escape hatch for the transport's response
|
|
128
|
+
* It is a `Net::HTTPResponse` today, and its class is not part of what 1.x promises
|
|
129
|
+
* `status` is an Integer, as `X::Response#status` is
|
|
130
|
+
* `problem` is the problem the body describes, or else the first error it names
|
|
131
|
+
* Add `X::Problem`, what the API said of a request, answered by `X::HTTPError#problem` and reported by `x-resources`
|
|
132
|
+
* Reads `title`, `detail`, `type`, `resource_type`, `resource_id`, `parameter`, `value`, and `message`
|
|
133
|
+
* `about?` tells whether it names a resource, or an Integer or String identifier, comparing them as Strings
|
|
134
|
+
* Equals a problem of the same attributes, writes JSON with `as_json`/`to_json`, and marshals in a versioned format
|
|
135
|
+
* Add `X::UnsupportedFormat`, an `X::Error` raised when reading back a Marshal, YAML, or JSON format a release does not read
|
|
136
|
+
* Raise `X::PaymentRequired` (402), `X::MethodNotAllowed` (405), `X::RequestTimeout` (408), `X::UnsupportedMediaType` (415), and `X::UnavailableForLegalReasons` (451)
|
|
137
|
+
* Each is an `X::ClientError`; X sends 402 when the account that pays for the app has no credit left
|
|
138
|
+
* Name the request an error was raised for: `X::HTTPError`, `X::NetworkError`, `X::InvalidResponse`, and `X::TooManyRedirects` read `http_method` and `uri`
|
|
139
|
+
* The message names it without its query, as `GET /2/users/1: Could not find user`
|
|
140
|
+
* A redirected request is named by the last request sent
|
|
141
|
+
* Add `X::InvalidResponse#body`, `status`, and `headers` for a successful response that is not JSON
|
|
142
|
+
* `body` falls back to the body of the response once it has been read whole, and never reads a stream
|
|
143
|
+
* Add `X::TooManyRequests#exhausted_rate_limits` and `#limiting_rate_limit`
|
|
144
|
+
* Add `X::UnsupportedOperation`, an `X::Error` declared by `x-core`, raised for anything the API offers no way to do
|
|
145
|
+
* e.g. hydrating or refreshing an `X::Poll` or an `X::Place` that is not hydrated, as in "X::Poll cannot be fetched by id"
|
|
146
|
+
* A finder the API lacks is not defined: `X::Poll` and `X::Place` answer none, `X::List`, `X::Community`, and `X::DirectMessage` only `find` and `find!`
|
|
147
|
+
* Add the upload methods to `X::Client`: `upload_media`, `chunked_upload_media`, `await_media_processing`, and `await_media_processing!`
|
|
148
|
+
* And `add_alt_text`, `add_subtitles`, `update_profile_image`, and `update_profile_banner`, each calling an uploader with the client
|
|
149
|
+
* As in `client.create_post("Look", media_ids: [client.upload_media("cat.jpg")])`
|
|
150
|
+
* `chunked_upload_media` returns the `X::UploadedMedia` once it is finalized, without waiting for processing
|
|
151
|
+
* `await_media_processing!` raises if processing failed, where `await_media_processing` returns the status
|
|
152
|
+
* Media that already says its processing ended, or an image's upload response, is returned without a request
|
|
153
|
+
* They come from `X::Uploads::API`, which code using `x-core` and `x-uploads` alone can include; a `client:` option raises `ArgumentError`
|
|
154
|
+
* Return an `X::UploadedMedia` in place of a Hash from the uploaders and their client methods
|
|
155
|
+
* From `upload`, `chunked_upload`, `await_processing`, `await_processing!`, `add_alt_text`, and `add_subtitles`
|
|
156
|
+
* It reads `id` and `media_id` (Integers), `media_key`, `bytesize`, `expires_after_secs`, `processing_info`, `state`, and `check_after_secs`
|
|
157
|
+
* It tells `processing?`, `failed?`, and `ready?`, and still reads as a Hash with `[]`, `fetch`, `dig`, `key?`, and `to_h`
|
|
158
|
+
* It is frozen, writes `as_json`/`to_json`, and marshals in a versioned format; one without an `"id"` of 1 to 19 digits raises `ArgumentError`
|
|
159
|
+
* `add_alt_text` and `add_subtitles` return the media they describe, ready to attach to a post
|
|
160
|
+
* Infer the media category of an upload from the bytes the media begins with, or else its extension, so `upload` takes any file
|
|
161
|
+
* Videos and large GIFs upload in chunks, and `upload` waits for X to process them
|
|
162
|
+
* `upload` takes `media_type:`, `chunk_size:`, and `concurrency:`, and raises `ArgumentError` for any other unknown keyword
|
|
163
|
+
* Take the media as the positional argument of the `X::Uploads::MediaUpload` and `X::Uploads::Account` methods, with `client:` as a keyword
|
|
164
|
+
* Describe uploaded media with the `alt_text:` of `X::Uploads::MediaUpload.upload` or with `X::Uploads::Metadata.add_alt_text`
|
|
165
|
+
* Alt text that is not a String, not convertible to UTF-8, empty, or over 1,000 characters raises `ArgumentError` before a request
|
|
166
|
+
* It is sent again after a server or network error
|
|
167
|
+
* An upload that cannot add its alt text raises `X::AltTextFailed`, which holds the uploaded `media`, for any `StandardError` but a `Timeout::Error` or an `X::TokenReportFailed`, which are raised as they are, as an interrupt is, but a timeout raised with its class, as by `Timeout.timeout(5, Timeout::Error)`, that lands in `save_tokens` is an `X::TokenReportFailed`, holding the tokens
|
|
168
|
+
* Attach uploaded subtitles to a video with `X::Uploads::Metadata.add_subtitles`
|
|
169
|
+
* `media_category:` defaults to `"tweet_video"`, and takes `"amplify_video"` in any case, or `TweetVideo`/`AmplifyVideo`
|
|
170
|
+
* Any other category raises `ArgumentError`
|
|
171
|
+
* It is sent again after a server or network error
|
|
172
|
+
* Share uploaded media, or give it other owners, with the `shared:` and `additional_owners:` of `upload`, `chunked_upload`, and `upload_media`
|
|
173
|
+
* Media given `shared: true` uploads in chunks; an image of unknown type, such as HEIC, given an image `media_category:` is sent as JPEG
|
|
174
|
+
* A `shared` other than true, false, or nil, or `additional_owners` that are not user IDs, raise `ArgumentError`
|
|
175
|
+
* Add `X::Uploads::MediaUpload::DEFAULT_CONCURRENCY` (4) and `MAX_CONCURRENCY` (16), the chunks a chunked upload sends at once
|
|
176
|
+
* Add `X::Uploads::MediaUpload::DEFAULT_PROCESSING_TIMEOUT` (600 seconds) and `AMPLIFY_VIDEO`, beside `TWEET_VIDEO`
|
|
177
|
+
* Name the media category of an upload with a Symbol, in any case, as in `media_category: :tweet_video`
|
|
178
|
+
* Rescue the errors `x-uploads` raises of its own with `X::Uploads::Error`
|
|
179
|
+
* It is the base of `X::AltTextFailed`, `X::ChunkedUploadFailed`, `X::InvalidMedia`, and `X::MissingMediaData`
|
|
180
|
+
* And of `X::MediaProcessingCheckFailed`, `X::MediaProcessingFailed`, and `X::MediaProcessingTimeout`
|
|
181
|
+
* `X::InvalidMediaType`, an `X::InvalidMedia`, is raised for media of a type the API does not take
|
|
182
|
+
* A bad category, chunk size, concurrency, alt text, or processing timeout raises `ArgumentError` instead
|
|
183
|
+
* Each error that holds media takes an optional message and `media:`; `X::MediaProcessingTimeout` takes `timeout:` too
|
|
184
|
+
* Raise `X::MediaProcessingCheckFailed`, which holds the uploaded `media`, when a check of its processing fails
|
|
185
|
+
* Raise `X::ChunkedUploadFailed`, which holds the initialized `media`, when a chunk or the finalize fails
|
|
186
|
+
* Such as a server error, a file deleted, closed, or shrunk mid-upload, or a finalize response without media
|
|
187
|
+
* It and `X::MediaProcessingCheckFailed` are raised for any `StandardError`, as the `cause`, such as one `on_response` raises
|
|
188
|
+
* A `Timeout::Error`, an `X::MediaProcessingTimeout`, or an interrupt is raised as it is, but one raised with its class, as by `Timeout.timeout(5, Timeout::Error)`, that lands in `save_tokens` is an `X::TokenReportFailed`, holding the tokens
|
|
189
|
+
* An `X::TokenReportFailed` is raised as it is too, from a chunk, the finalize, or a check, so `rescue X::TokenReportFailed` stores its `tokens`
|
|
190
|
+
* Take an upload's result, a media ID, a media key, or anything that answers `media_key` in `await_processing`, `await_processing!`, `add_alt_text`, and `add_subtitles`
|
|
191
|
+
* Anything else, or an ID that is not 1 to 19 digits, raises `ArgumentError` before a request
|
|
192
|
+
* The signatures type what answers `media_key` as `X::Uploads::_MediaKeyed`
|
|
193
|
+
* Upload a GIF with a single frame as an image, since X fails to process it as a GIF
|
|
194
|
+
* Stream with `X::StreamingClient`, which `X::Client#streaming` builds with the client's credentials, base URL, classes, and `on_response`
|
|
195
|
+
* `streaming` takes `read_timeout:`, `max_reconnects:`, and `on_reconnect:`; other settings come from a copy, as `client.with(open_timeout: 2).streaming`
|
|
196
|
+
* `stream` raises `ArgumentError` when given no block, before it opens the stream
|
|
197
|
+
* Stop the streams of a streaming client from any thread, or the trap of a signal, with `X::StreamingClient#stop`
|
|
198
|
+
* Each stream stops the next time it waits on the API, at once if idle or waiting to reconnect
|
|
199
|
+
* A stream a Fiber scheduler runs, as in an `Async` task, stops once its block next returns, at the next keep-alive, or within a second as it waits to reconnect
|
|
200
|
+
* A stopped streaming client stays stopped: a stream asked of it later returns nil at once, without a request
|
|
201
|
+
* Each stopped `stream` call returns nil, `stop` returns nil, and `stopped?` tells a stopped streaming client
|
|
202
|
+
* A block, `on_response`, or `on_reconnect` running at that moment runs to its end first
|
|
203
|
+
* `streaming` builds a new streaming client each time, so keep the one you stop in a variable
|
|
204
|
+
* Stop a stream from its block: `break` stops it and returns its value, and `throw` unwinds past it
|
|
205
|
+
* An error of the block, `on_response`, or the object class stops it and reaches the caller, `StopIteration` included
|
|
206
|
+
* Read and change the rules of the filtered stream with `rules`, `add_rules`, and `delete_rules` on `X::StreamingClient`
|
|
207
|
+
* A rule is a frozen `X::StreamRule` of its `value`, `tag`, and `id` (an Integer, or nil for a rule to add)
|
|
208
|
+
* An `id` is an Integer that is not negative or a String of digits alone; anything else raises `ArgumentError`
|
|
209
|
+
* Add an `X::StreamRule`, a Hash of `value` and `tag`, or a String; delete by rule, `id`, or value
|
|
210
|
+
* An `X::MatchingRule` deletes the rule it names, so `delete_rules(post.matching_rules)` deletes the rules a post matched
|
|
211
|
+
* Anything else, such as a post, raises `ArgumentError` before a request
|
|
212
|
+
* `dry_run: true` checks the rules and changes none; `rules` takes `params:` and reads every page
|
|
213
|
+
* The problems the API reports of rules it does not add or delete are yielded as `X::Problem`s, or without a block raise `X::RulesRejected`
|
|
214
|
+
* A rule the app already has raises nothing, and is not among the rules `add_rules` returns, which are those it added; a block is still yielded its `DuplicateRule` problem
|
|
215
|
+
* A rule X rejects, such as an invalid one, may keep it from adding any, and it reports that one alone; the rules returned say which were added
|
|
216
|
+
* `X::RulesRejected#added` and `#deleted_count` hold what was done; no rules send no request
|
|
217
|
+
* Stream, and read and change stream rules, as the app from a client that signs with OAuth 1.0a, which the stream endpoints refuse
|
|
218
|
+
* A client of an OAuth 2.0 user without app credentials streams as the user, so X's 403 raises `X::Forbidden`
|
|
219
|
+
* Reconnect a stream that ends, drops, or sends a line that is not JSON, backing off as X recommends
|
|
220
|
+
* Up to the `max_reconnects` of the streaming client in a row (unlimited by default), reset by an object or keep-alive read from a connection open for a minute
|
|
221
|
+
* So a stream whose connections each deliver something and drop backs off, and runs out of reconnects
|
|
222
|
+
* Then it raises the last error: `X::NetworkError` for a stream that ended or dropped, `X::InvalidResponse` for a non-JSON line
|
|
223
|
+
* A rate limit backs off from a minute, doubling, but raises `X::TooManyRequests` past `max_rate_limit_wait` or for the usage cap
|
|
224
|
+
* A server error, 408, or 409 backs off from 5 seconds, up to 320 seconds, or waits longer for its `Retry-After`
|
|
225
|
+
* A `Retry-After` past `max_rate_limit_wait` raises the error at once
|
|
226
|
+
* What arrived of a line the stream ended within is dropped, unparsed and unreported, and the stream reconnects as one that ended
|
|
227
|
+
* An error `on_response` raises for a failed response stops the stream and reaches the caller, as one for an object does
|
|
228
|
+
* A certificate that does not verify raises its `X::NetworkError` at once, since it would not verify on the next attempt
|
|
229
|
+
* Report each reconnect of a stream to `on_reconnect:`, a callable passed the error and the seconds it waits
|
|
230
|
+
* It is passed an error every time, never nil, and is called with those two arguments and no others throughout 1.x
|
|
231
|
+
* It can call `stop` to give up, as on a host that never resolves, and the stream returns nil
|
|
232
|
+
* An error it raises stops the stream and reaches the caller
|
|
233
|
+
* Read a stream with the `read_timeout` of the streaming client, 30 seconds by default, so a quiet stream reconnects
|
|
234
|
+
* Raise `X::StreamError`, an `X::Streams::Error`, for a stream line that holds errors and no data
|
|
235
|
+
* It holds the `problems`, `http_method`, and `uri`; `on_response` is passed the line first
|
|
236
|
+
* A line of `operational-disconnect`s alone, which `X::Problem#disconnect?` tells, reconnects; other problems stop the stream
|
|
237
|
+
* Add immutable, thread-safe resource classes, which descend from `X::Resource`
|
|
238
|
+
* `X::User`, `X::Post` (aliased `X::Tweet`), `X::List`, `X::DirectMessage`, `X::Space`, `X::Media`, `X::Poll`, `X::Place`, and `X::Community`
|
|
239
|
+
* Lists of resources or identifiers are frozen Arrays; copy one to change it
|
|
240
|
+
* A list the response omits, such as `post.urls`, reads as an empty Array; `user.connection_status` reads nil unless requested
|
|
241
|
+
* Text reads as the API sends it, so `post.text` and `direct_message.text` hold `&`, `<`, and `>` throughout 1.x
|
|
242
|
+
* Nested data, such as `post.entities` and `post.public_metrics`, reads as frozen Hashes keyed by String throughout 1.x
|
|
243
|
+
* Nested data the API sends as anything but an object, or a list of them, raises `X::InvalidAttribute`
|
|
244
|
+
* Rescue the failures of the object layer with `X::Resources::Error`
|
|
245
|
+
* It is the base of `X::MissingResource`, `X::UnreadableResponse`, `X::InvalidAttribute`, `X::MissingClient`, and `X::PageLimitReached`
|
|
246
|
+
* Include the object methods in a class of your own with `X::Resources::API`
|
|
247
|
+
* The class answers `get`, `post`, `put`, and `delete` as `X::Resources::_Client` types them, taking keywords it does not read
|
|
248
|
+
* The other modules and constants of `X::Resources` are private
|
|
249
|
+
* Compare resources by class and ID with `==`, `eql?`, and `hash`, so the same resource from different requests is equal
|
|
250
|
+
* Resolve references such as `post.author` and `post.replied_to` to included objects or ID stubs, one object per resource in a response
|
|
251
|
+
* Add `hydrate`, which fetches and memoizes the full resource, and `refresh`, which fetches it again
|
|
252
|
+
* A resource fetched with fewer than the default fields or expansions is not hydrated; one fetched with more is
|
|
253
|
+
* A resource without a client, such as one read back with `Marshal`, raises `X::MissingClient` from any request
|
|
254
|
+
* `FIELDS` and `EXPANSIONS` may grow in a minor release; add to `default_params` rather than list every value
|
|
255
|
+
* Hydrate the stubs of a page of `stubs`, or of a cursor that requests identifiers alone, together, in batch lookups of up to 100
|
|
256
|
+
* Lists, communities, and direct messages hydrate one at a time, as does a reference a page did not include; `hydrate_all` batches any
|
|
257
|
+
* Refer to a resource without a request with `X::User.from_id` and its equivalents, and tell such stubs apart with `stub?`
|
|
258
|
+
* Add `X::Cursor`, an `Enumerable` collection that fetches pages lazily at the maximum page size and caches them
|
|
259
|
+
* `refresh` and `prefetch` fetch pages again or ahead
|
|
260
|
+
* A page a prefetch failed to fetch raises the prefetch's error once it is reached, rather than be requested again
|
|
261
|
+
* A page that names as its next a token already read raises `X::UnreadableResponse`
|
|
262
|
+
* `page` takes an Integer index, raising `TypeError` for any other and `ArgumentError` for a negative one
|
|
263
|
+
* `X::Cursor.new` is private; cursors come from the collections, searches, and lookups
|
|
264
|
+
* Answer `X::Cursor#first`, `take`, `any?`, `none?`, `one?`, and `empty?` from as few resources as they need
|
|
265
|
+
* They read from pages already fetched, and request a page no larger than needed, raised to the endpoint's minimum
|
|
266
|
+
* So `any?`, `none?`, and `empty?` request one resource and `one?` two, or the 5 or 10 an endpoint such as a search takes at least
|
|
267
|
+
* A count that is not an Integer is converted as `Array#first` does; a String, or nil to `take`, raises `TypeError`
|
|
268
|
+
* Count a collection without paging it with `X::Cursor#published_count`, the number the API publishes
|
|
269
|
+
* Followers, followed users, and list memberships of a user, and members and followers of a list; nil for others
|
|
270
|
+
* Read a collection a page at a time with `X::Cursor#each_page`, yielding `X::Page`s
|
|
271
|
+
* A page holds `items`, `meta`, `result_count`, `next_token`, `previous_token`, and `problems`, and reads like an Array
|
|
272
|
+
* `X::Page.new` raises `ArgumentError` for problems that are not `X::Problem`s
|
|
273
|
+
* Request identifiers alone from a cursor with `ids`, as in `user.followers.ids`, and scan stubs with `stubs`
|
|
274
|
+
* Type-check the resources of a collection: `X::Cursor` and `X::Page` are generic in their signatures
|
|
275
|
+
* Read the collections of a resource as cursors
|
|
276
|
+
* A user's `followers`, `following`, `affiliates`, `posts`, `mentions`, `liked_posts`, `owned_lists`, `list_memberships`, and `followed_lists`
|
|
277
|
+
* A list's `members`, `followers`, and `posts`, a post's `quotes`, and a space's `posts`, and its `buyers` (OAuth 2.0 user only)
|
|
278
|
+
* Add `home_timeline`, `blocking`, and `muting` cursors to `X::User`
|
|
279
|
+
* Read the authenticated user's reposted posts with `reposts_of_me` on the client and `X::Post`, aliased as `retweets_of_me`
|
|
280
|
+
* Tell a post that replies, quotes, or reposts with `reply?`, `quote?`, and `repost?`, and read its target with `replied_to`, `quoted`, and `reposted`
|
|
281
|
+
* Add `X::Post#liked_by`, `reposted_by`, and `reposts`, and `references` on posts and direct messages
|
|
282
|
+
* Look up users and posts by ID in parallel batches of 100 with `X::User.find_all` and `X::Post.find_all`
|
|
283
|
+
* They return one resource per ID found, in the order asked, an ID given twice coming back twice
|
|
284
|
+
* Once a batch fails, no further batch is sent and its error is raised
|
|
285
|
+
* Say how many batches a lookup requests at once with `concurrency:` (default 4)
|
|
286
|
+
* Taken by `find_all`, `find_all_by_username`, `hydrate_all`, and `X::Space.find_all_by_creator`
|
|
287
|
+
* And by `find_all_users`, `find_all_users_by_username`, `find_all_posts`, `find_all_spaces`, `find_all_media`, and `find_all_spaces_by_creator`
|
|
288
|
+
* Anything but an Integer of at least 1 raises `ArgumentError`
|
|
289
|
+
* Hydrate many resources in parallel batches with `hydrate_all` on `X::User`, `X::Post`, `X::Space`, and `X::Media`
|
|
290
|
+
* It drops nil and resources not found, skips hydrated ones, and stores what it found in each resource
|
|
291
|
+
* A resource of another class raises `ArgumentError` before a request
|
|
292
|
+
* Add lookup, search, and action methods for the resources to `X::Client`
|
|
293
|
+
* `find_user`, `find_all_users`, `current_user!`, `find_post`, `find_all_posts`, `find_list`, and `find_space`
|
|
294
|
+
* `search_posts`, `search_all_posts`, `create_post`, `delete_post`, `direct_messages`, and `create_direct_message`
|
|
295
|
+
* `follow`, `unfollow`, `like`, `unlike`, `repost`, and `unrepost`
|
|
296
|
+
* The finders have `find_tweet` aliases
|
|
297
|
+
* Actions return true or false; `follow` returns true once it has asked to follow a protected user
|
|
298
|
+
* Look up a mix of IDs and usernames with `find_all_users`, and usernames alone, even all-digit ones, with `find_all_users_by_username`
|
|
299
|
+
* Look up a user by ID when given an Integer and by username when given a String
|
|
300
|
+
* Say which with `X::User.find_by_username`, `find_by_username!`, `find_all_by_username`, `find_by_id`, `find_by_id!`, and `find_all_by_id`
|
|
301
|
+
* Or on the client with `find_user_by_username`, `find_user_by_username!`, `find_all_users_by_username`, `find_user_by_id`, `find_user_by_id!`, and `find_all_users_by_id`
|
|
302
|
+
* Elsewhere, such as `follow` or `find_post`, an ID that is not a number raises `ArgumentError`
|
|
303
|
+
* So does a resource of another class, as `client.like(user)`, or any other object that answers `id`
|
|
304
|
+
* Accept a username with a leading `@` in `find_user`, `find_all_users`, and `X::User.find_all_by_username`
|
|
305
|
+
* Search users with `X::User.search` and `client.search_users`
|
|
306
|
+
* Request the largest page each search allows: 500 posts from `search_all_posts` (100 with context annotations), and 1,000 users from `search_users`
|
|
307
|
+
* Look up direct messages with `find_direct_message`, many spaces with `find_all_spaces`, and a conversation with `direct_messages_with`
|
|
308
|
+
* Look up the spaces of many creators with `X::Space.find_all_by_creator` and `find_all_spaces_by_creator`, in batches of 100
|
|
309
|
+
* Search spaces with `X::Space.search` and `search_spaces` on the client, a cursor over live or scheduled spaces
|
|
310
|
+
* Look up and search spaces, and read their posts, whatever the client authenticates with
|
|
311
|
+
* An OAuth 1.0a client, which the space endpoints refuse, requests them as the app; an OAuth 2.0 user client as the user unless it holds app credentials, when it too requests them as the app
|
|
312
|
+
* Read the topics of a space with `X::Space#topics`, each an `X::Topic` with a `name` and `description`
|
|
313
|
+
* Look up media by media key with `X::Media.find`, `find!`, and `find_all`, and `find_media`, `find_media!`, and `find_all_media`
|
|
314
|
+
* Each takes a media key, media, or an upload's result; a numeric media ID raises `ArgumentError`
|
|
315
|
+
* Read the numeric ID of media with `X::Media#media_id`, as `X::UploadedMedia#media_id` reads it; `X::Media#id` is the media key
|
|
316
|
+
* Add `X::Community`, with `find_community`, `find_community!`, `search_communities`, `post.community`, and the `community:` of `create_post`
|
|
317
|
+
* Alias every client method named for direct messages with `dm`: `find_dm`, `find_dm!`, `dms`, `dms_with`, `dms_in`, `create_dm`, `create_group_dm`, `create_dm_in`, and `delete_dm`
|
|
318
|
+
* Raise `X::MissingResource`, an `X::Resources::Error`, from `current_user!`, `X::User.current!`, `find!`, and the bang finders of the client
|
|
319
|
+
* `find_user!`, `find_user_by_username!`, `find_post!`, `find_list!`, `find_space!`, `find_media!`, `find_community!`, and `find_direct_message!`
|
|
320
|
+
* The message names what was looked up, as "Could not find X::User @sferik"; `problems` explains why
|
|
321
|
+
* It is not `X::NotFound`: a missing resource gives nil or `X::MissingResource` whether X answers 200 OK with no data or a 404 that reports the resource as not found
|
|
322
|
+
* The `X::NotFound` of such a 404 is its `cause`; any other 404, such as one from a client pointed at the wrong host or API version, or one to `current_user` or a `find_all_…`, raises `X::NotFound`
|
|
323
|
+
* Add `current_user` and `current_user!` to the client, as `X::User.current` and `.current!`; `current_user` returns nil if the API answers without a user
|
|
324
|
+
* Take the authenticated user's ID from an OAuth 1.0a access token with `current_user_id`, without a request
|
|
325
|
+
* Name the interface after posts: `post_count`, `pinned_post_id`, `most_recent_post_id`, `edit_history_post_ids`, `note_post`, `referenced_posts`, and `repost_count`
|
|
326
|
+
* The tweet-named methods, such as `create_tweet`, `tweets`, and `retweet_count`, remain as aliases
|
|
327
|
+
* A response that names fields after tweets, as a stream does, is still read
|
|
328
|
+
* Request fields and expansions by the names the X API documentation gives, such as `post.fields` and `referenced_posts`
|
|
329
|
+
* Request no `edit_history_post_ids` or `entities.mentions.username` expansion, which the object layer never reads
|
|
330
|
+
* Read the IDs of users, posts, lists, direct messages, communities, and polls, and the attributes that refer to them, as Integers
|
|
331
|
+
* The IDs of spaces, places, and media, media keys, `dm_conversation_id`, and usernames are Strings
|
|
332
|
+
* `X::Problem#resource_id` and `#value` are Strings, so match a problem to a resource with `problem.about?(user)`
|
|
333
|
+
* Raise `X::MissingResource`, holding the response's problems, when a request that creates a resource is answered without it
|
|
334
|
+
* From `X::Post.create`, `X::List.create`, `X::DirectMessage.create`, `create_group`, `create_in`, and their client methods
|
|
335
|
+
* So `create_post`, `create_list`, `create_dm`, and the rest never return nil
|
|
336
|
+
* A response whose data names no identifier raises it too
|
|
337
|
+
* Build the `reply`, `media`, and quote of a new post with the `reply_to:`, `media_ids:`, and `quote:` of `create_post` and `X::Post.create`
|
|
338
|
+
* A field passed with a String key, such as `"reply"` or `"attachments"`, is read as its Symbol, so it is sent once
|
|
339
|
+
* Take an upload's result, media, or a media key in the `media_ids:` of `create_post`, reading its media ID
|
|
340
|
+
* An ID that is not 1 to 19 digits, or anything else, raises `ArgumentError` before the request
|
|
341
|
+
* Post media without text: the text of `create_post`, `create_direct_message`, and their equivalents is optional
|
|
342
|
+
* A post or message with neither text nor any other field raises `ArgumentError`
|
|
343
|
+
* Attach uploaded media to a direct message with `media_ids:`
|
|
344
|
+
* Passing both `media_ids:` and `attachments:` raises `ArgumentError`; an empty `media_ids:` attaches nothing
|
|
345
|
+
* Start a group conversation with `X::DirectMessage.create_group` and `create_group_direct_message`, aliased `create_group_dm`
|
|
346
|
+
* Send to and read any conversation with `X::DirectMessage.create_in` and `.in`, and `create_direct_message_in` and `direct_messages_in`
|
|
347
|
+
* The client methods have `create_dm_in` and `dms_in` aliases
|
|
348
|
+
* Add `X::DirectMessage.delete`, `message.delete`, and `delete_direct_message` on the client
|
|
349
|
+
* Add `X::DirectMessage#peer(user)`, the other participant of a one-to-one conversation, nil for a group conversation
|
|
350
|
+
* Add `X::DirectMessage#from?`, false for a message that does not name its sender
|
|
351
|
+
* Add `X::Post#coordinates`, and `permalink` and `uri`, the x.com address of a post, user, list, or community
|
|
352
|
+
* Add `X::Post#urls` and `X::Post#expanded_text`, the text with each shortened link expanded, every link in one pass
|
|
353
|
+
* `expanded_text` HTML-escapes each URL it puts in, so the whole text reads escaped, as `text` does
|
|
354
|
+
* Read the full text of a post longer than 280 characters with `X::Post#text`, from its `note_post`
|
|
355
|
+
* `entities` and `urls` of a long post read from the note alone
|
|
356
|
+
* Add `X::Post#matching_rules`, the filtered-stream rules a post matched, each a frozen `X::MatchingRule` with an `id` and `tag`
|
|
357
|
+
* Building one whose `id` is not a String of digits or a non-negative Integer raises `ArgumentError`; `post.matching_rules` raises `X::InvalidAttribute` for one
|
|
358
|
+
* Add `X::User#profile_banner_url`, `parody?`, `identity_verified?`, `subscription_type`, `verified_followers_count`, `subscriber_count`, and `media_count`
|
|
359
|
+
* Add `X::User#receives_your_dm?`, `subscribes_to_you?`, and `subscription`; the predicates are false, and `subscription` nil, unless `user.fields` names them
|
|
360
|
+
* Add `X::Post#media_source_posts`, aliased `media_source_tweets`, the posts its attached media was first posted with
|
|
361
|
+
* Resolved from the `attachments.media_source_tweet` expansion, which lookups request
|
|
362
|
+
* Add `X::Post#display_text_range`, an exclusive `Range`, nil when absent, and `scopes`, `card_uri`, `article`, `article_title`, `media_metadata`, and `paid_partnership?`, and `X::DirectMessage#entities`
|
|
363
|
+
* Add `X::User#affiliation`, `affiliated_with_ids`, and `affiliated_with`; a user included in another resource holds none until hydrated
|
|
364
|
+
* Read `is_identity_verified` and `is_ticketed` as `X::User#identity_verified` and `X::Space#ticketed`, with `?` predicates
|
|
365
|
+
* Match resources against `case/in` patterns with `deconstruct_keys`, as in `post in {like_count: 100..}`
|
|
366
|
+
* Tweet-named aliases match too, as in `user in {pinned_tweet_id: Integer}`
|
|
367
|
+
* `X::Trend`, `X::PersonalizedTrend`, and `X::PostUsage` match by their readers
|
|
368
|
+
* Write a resource, problem, trend, usage, matching rule, or page as JSON with `as_json` and `to_json`, never with credentials
|
|
369
|
+
* Each marshals, and writes YAML, in a versioned format every 1.x release reads, coming back frozen
|
|
370
|
+
* A resource keeps the included objects it refers to, but not its client
|
|
371
|
+
* Read each but a page as a Hash with `to_h`
|
|
372
|
+
* A cursor raises `X::UnsupportedOperation` from `as_json`, `to_json`, and `to_h`, and `TypeError` from `Marshal.dump`; serialize `to_a`
|
|
373
|
+
* Report the partial errors of a successful response as `X::Problem`s
|
|
374
|
+
* `problems` on a resource holds those about it, the resources it refers to directly, or no identifier
|
|
375
|
+
* `problems` on a page holds every problem of its response; a finder's block receives each one
|
|
376
|
+
* Count posts matching a query with `X::Post.count`, `count_all`, `count_by_period`, and `count_all_by_period`
|
|
377
|
+
* On the client as `count_posts`, `count_all_posts`, `count_posts_by_period`, and `count_all_posts_by_period`, with tweet-named aliases
|
|
378
|
+
* Counts by period are keyed by the `Range` of `Time` each period spans
|
|
379
|
+
* A count that is not a String of digits or a non-negative Integer raises `X::InvalidAttribute`
|
|
380
|
+
* An OAuth 1.0a client counts as the app; an OAuth 2.0 user client without app credentials counts as the user
|
|
381
|
+
* Report the post usage of the app's project with `X::PostUsage.current` and `client.post_usage`
|
|
382
|
+
* They return nil, yielding the response's problems to a block, when it holds no usage
|
|
383
|
+
* `X::PostUsage.current!` and `client.post_usage!` raise `X::MissingResource` instead
|
|
384
|
+
* Includes its monthly cap, reset day, and usage by day and by app
|
|
385
|
+
* Read the trends of a place with `X::Trend.at` and `trends` on the client, given its WOEID, such as 1 for the world
|
|
386
|
+
* An ID that is not a number raises `ArgumentError`; `max_trends` limits the 50 returned
|
|
387
|
+
* An `X::Trend` reads its `name` and `post_count`; an OAuth 1.0a client requests trends as the app
|
|
388
|
+
* Read the trends X picks for the authenticated user with `X::PersonalizedTrend.all` and `personalized_trends`
|
|
389
|
+
* An `X::PersonalizedTrend` reads its `name`, `category`, `post_count_text`, and `trending_since_text`
|
|
390
|
+
* Bookmark a post and remove the bookmark with `bookmark` and `unbookmark` on `X::Client`
|
|
391
|
+
* Read bookmark folders with `X::User#bookmark_folders`, a cursor of `X::BookmarkFolder`, and a folder's posts with `bookmarks(folder:)`
|
|
392
|
+
* Add `block`, `unblock`, `mute`, and `unmute` to `X::Client`
|
|
393
|
+
* Add `X::List.create`, `.update`, `.delete`, `list.add_member`, `remove_member`, `update`, and `delete`, with client equivalents
|
|
394
|
+
* `create_list`, `update_list`, `delete_list`, `add_list_member`, and `remove_list_member`
|
|
395
|
+
* An update with no field to change raises `ArgumentError` before a request
|
|
396
|
+
* Follow, unfollow, pin, and unpin a list with `follow_list`, `unfollow_list`, `pin_list`, and `unpin_list` on `X::Client`, and read `X::User#pinned_lists`
|
|
397
|
+
* Hide and show a reply with `X::Post#hide_reply` and `#unhide_reply`, their class methods, and the client's `hide_reply` and `unhide_reply`
|
|
398
|
+
* Check whether a user follows another with `user.follows?`, and a list's membership with `list.member?`, without fetching every page
|
|
399
|
+
* `follows?` looks up `connection_status` once when either user is the authenticated user, and scans otherwise
|
|
400
|
+
* `member?` scans the smaller of a public list's members and the user's list memberships
|
|
401
|
+
* Limit the pages a scan or a count of posts reads with `max_pages:` (default nil, no limit)
|
|
402
|
+
* Taken by `X::List#member?`, `X::User#follows?`, `X::Post.count`, `count_all`, `count_by_period`, `count_all_by_period`, and their client methods
|
|
403
|
+
* Past the limit, with another page named, it raises `X::PageLimitReached`, an `X::Resources::Error`
|
|
404
|
+
* A value that is neither an Integer of at least 1 nor nil raises `ArgumentError` before a request
|
|
405
|
+
* Raise `X::InvalidAttribute`, an `X::Resources::Error`, for a response value that cannot be read as the API documents it
|
|
406
|
+
* Such as a timestamp that is not ISO 8601, a count that is not a whole number, or a flag that is not a boolean
|
|
407
|
+
* Its cause is the `ArgumentError` that refused the value
|
|
408
|
+
* Validate the identifier of a resource when it is built, so `X::User.new({"id" => "abc"})` raises `ArgumentError`
|
|
409
|
+
* A space or place ID is word characters alone; a `dm_conversation_id` that is not digits, or two numbers joined by a hyphen, raises `X::InvalidAttribute`
|
|
410
|
+
* Validate a username before building a path from it, so `find_user("../tweets/20")` raises `ArgumentError` without a request
|
|
411
|
+
|
|
412
|
+
### Changed
|
|
413
|
+
* Require Ruby 3.4 or later
|
|
414
|
+
* Hold `VERSION` in a String rather than a `Gem::Version`; use `gem_version` to compare versions
|
|
415
|
+
* Send requests to `api.x.com` rather than `api.twitter.com` by default
|
|
416
|
+
* Name the gem in the `User-Agent` header of every request, token requests included, as `x-ruby/1.0.0 ruby/3.4.0 (arm64-darwin24)`, where it named `X-Client`
|
|
417
|
+
* Move the HTTP client into `x-core` and the uploaders into `x-uploads`
|
|
418
|
+
* Rename `X::MediaUploader` to `X::Uploads::MediaUpload`, `X::AccountUploader` to `X::Uploads::Account`, and `X::MediaUploadValidator` to `X::Uploads::Validator`
|
|
419
|
+
* Rename `X::OAuthAuthenticator` to `X::OAuth1Authenticator`
|
|
420
|
+
* Rename `X::ConnectionException`, the error for 409 Conflict, to `X::Conflict`
|
|
421
|
+
* Rename `X::HTTPError#response` to `http_response`, and make that of `X::RateLimit` private
|
|
422
|
+
* Move `stream` from `X::Client` to `X::StreamingClient`
|
|
423
|
+
* Refresh OAuth 2.0 tokens with `X::OAuth2Authenticator#refresh!`, in place of `refresh_token!`
|
|
424
|
+
* It returns the frozen `X::OAuth2Tokens` of the refresh rather than the Hash of the token response
|
|
425
|
+
* It raises `X::UnsupportedOperation` for an authenticator that holds no refresh token
|
|
426
|
+
* Keep frozen copies of the credentials, tokens, header values, base URL, and proxy URL a client, an authenticator, an `X::OAuth2Authorization`, or `X::OAuth2Tokens` is given, so neither the caller nor what a reader returns can change what is sent, or where
|
|
427
|
+
* Keep secret credentials private on a client and its authenticators
|
|
428
|
+
* `api_key_secret`, `access_token`, `access_token_secret`, `bearer_token`, `client_secret`, and `refresh_token` no longer read off a client
|
|
429
|
+
* Nor do the secrets and tokens of the authenticators; `api_key`, `client_id`, and `expires_at` remain public
|
|
430
|
+
* An authenticator's `headers` still returns the `Authorization` header it sends, which holds a bearer or OAuth 2.0 token
|
|
431
|
+
* Store refreshed tokens from the `X::OAuth2Tokens` that `save_tokens` is passed
|
|
432
|
+
* Raise `TypeError` from `Marshal.dump`, `YAML.dump`, `as_json`, and `to_json` of a client, streaming client, authenticator, or `X::OAuth2Authorization`
|
|
433
|
+
* Keep the proxy of a client, streaming client, and connection private: `proxy_url` and the `proxy_*` readers are gone
|
|
434
|
+
* `inspect` leaves the proxy out
|
|
435
|
+
* Send credentials only to the origin of the `base_url`
|
|
436
|
+
* An endpoint or stream naming another scheme, host, or port gets no `Authorization`, `Cookie`, or `Proxy-Authorization` header
|
|
437
|
+
* Another API version on the same origin, such as `https://api.x.com/1.1/account/settings.json`, still gets them
|
|
438
|
+
* Send a header named by a Symbol with its underscores as hyphens, wherever headers are taken
|
|
439
|
+
* `headers: {content_type: "text/plain"}` sends `content-type`, replacing the default `Content-Type`
|
|
440
|
+
* It is dropped on a cross-origin redirect like the header it names
|
|
441
|
+
* Raise `ArgumentError` from `X::Client.new` for a `base_url` that is not an absolute http or https URL
|
|
442
|
+
* Or that holds a user, password, query, or fragment
|
|
443
|
+
* Raise `ArgumentError` from `X::Client.new` for credentials that do not form a complete set
|
|
444
|
+
* Or for a credential that belongs to no complete set, such as a `client_id` beside a `bearer_token`
|
|
445
|
+
* Or for OAuth 2.0 credentials beside a complete set of OAuth 1.0a credentials
|
|
446
|
+
* Or for an empty String credential, or one that is neither a String nor nil
|
|
447
|
+
* Or for an `expires_at` beside credentials it does not belong to
|
|
448
|
+
* Raise `ArgumentError` from the authenticators for a required credential that is nil or empty, or an `expires_at` that is not a `Time`
|
|
449
|
+
* Raise `ArgumentError` from `X::Client.new`, `with`, and `streaming` for settings out of range, naming the setting
|
|
450
|
+
* `max_redirects`, `max_rate_limit_retries`, and `max_retries` must be Integers of at least 0
|
|
451
|
+
* `max_rate_limit_wait` must be a number of at least 0, not NaN; `max_reconnects` an Integer of at least 0 or `Float::INFINITY`
|
|
452
|
+
* `open_timeout`, `read_timeout`, and `write_timeout` must be finite numbers of at least 0, or nil; `keep_alive_timeout` a finite number
|
|
453
|
+
* A stream's `read_timeout` must be at least 25 seconds, or nil
|
|
454
|
+
* `default_array_class` must be a Class, and `default_object_class` a Class or respond to `from_response`
|
|
455
|
+
* `debug_output` must be nil or take a String with `<<`, as an IO, a StringIO, or a Logger does; a String, such as `"debug.log"`, and an Integer are refused
|
|
456
|
+
* Raise `ArgumentError` from `get`, `post`, `put`, `delete`, `get_stream`, and `stream` for an endpoint that is not a valid URL
|
|
457
|
+
* Such as one that holds a space or bad `%` escape, or does not resolve to an http or https URL with a host
|
|
458
|
+
* An endpoint that is not a String, such as a Symbol or a URI, raises `ArgumentError` naming its class
|
|
459
|
+
* Raise `ArgumentError` from `post` and `put` for an unknown keyword, as `client.post("tweets", text: "Hello")`
|
|
460
|
+
* Resolve an endpoint that begins with a slash against the base URL, so `client.get("/users/me")` requests `/2/users/me`
|
|
461
|
+
* Open a connection with a 10-second timeout rather than 60; `read_timeout` and `write_timeout` stay 60
|
|
462
|
+
* Send each request once, turning off the automatic retry of `Net::HTTP`; `max_retries` decides what is sent again
|
|
463
|
+
* Send an idempotent request again on a new connection when a kept-alive connection had gone stale
|
|
464
|
+
* Only before the status and headers of its response are read; a response cut off after that is never sent again
|
|
465
|
+
* Raise `X::NetworkError` for a body cut off before its end, or shorter than its `Content-Length`, rather than read what arrived
|
|
466
|
+
* Wrap every network failure in `X::NetworkError`, so a stream reconnects after it
|
|
467
|
+
* Including `IOError`, `SystemCallError`, Net::HTTP's open, read, and write timeouts, `Net::ProtocolError`, `Zlib::Error`, `Net::HTTPBadResponse`, `OpenSSL::SSL::SSLError`, and `SocketError`, but not a `Timeout::Error` that `Timeout.timeout` raises around a request, which is raised as it is, but one raised with its class, as by `Timeout.timeout(5, Timeout::Error)`, that lands in `save_tokens` is an `X::TokenReportFailed`, holding the tokens
|
|
468
|
+
* Stop following redirects after exactly `max_redirects` hops instead of one more, so 0 follows none
|
|
469
|
+
* Request the token endpoints at the origin of the base URL, under the path it serves the API at, rather than at `api.x.com`
|
|
470
|
+
* The path is the base URL's minus a trailing API version segment
|
|
471
|
+
* With `base_url: "https://gateway.example/x/2/"`, an app-only token comes from `/x/oauth2/token` and a refresh goes to `/x/2/oauth2/token`
|
|
472
|
+
* `X::OAuth2Authorization` exchanges its code at the base URL of the client it builds
|
|
473
|
+
* Raise `X::AuthorizationError`, an `X::ClientError`, when X refuses a token refresh, code exchange, or app-only token
|
|
474
|
+
* It holds the OAuth 2.0 `error_code`, such as `invalid_request`, and the `status`, `headers`, and `body` of the response
|
|
475
|
+
* A token endpoint that fails to answer raises the `X::HTTPError` of its status, such as `X::ServerError`, instead
|
|
476
|
+
* A refresh refused after a 401 raises it with the `X::Unauthorized` as its cause
|
|
477
|
+
* Raise `X::AuthorizationDenied`, an `X::Error` with the `error_code` the redirect reported, for a redirect back from X that refuses
|
|
478
|
+
* Raise `X::InvalidResponse`, an `X::HTTPError`, for a successful response whose body is not JSON, where it returned nil
|
|
479
|
+
* Tag the bodies of responses, errors, and stream lines as UTF-8, so comparing them with non-ASCII text works
|
|
480
|
+
* A body that is not valid UTF-8 keeps its bytes; `valid_encoding?` tells it apart
|
|
481
|
+
* Read only rate limits a response reports in full, in base 10
|
|
482
|
+
* `X::TooManyRequests#retry_after` no longer raises `KeyError` or `ArgumentError` for a missing or malformed header
|
|
483
|
+
* Return nil from `X::TooManyRequests#reset_at`, `#reset_in`, and `#retry_after` when the response does not say when the limit resets
|
|
484
|
+
* Read `Retry-After` with `X::HTTPError#retry_after`, in seconds or as an HTTP date, or nil
|
|
485
|
+
* `X::TooManyRequests#retry_after` reads it, falling back on `#reset_in`; retries and reconnects wait for it
|
|
486
|
+
* Report every rate limit a response names from `X::TooManyRequests#rate_limits`, not just the exhausted ones
|
|
487
|
+
* `#rate_limit` reads the 15-minute limit; the exhausted limit that resets last is now `#limiting_rate_limit`
|
|
488
|
+
* Make `X::RateLimit.new` and `.reported?` private
|
|
489
|
+
* Make public, documented constructors for the errors of `x-core` and `X::Response`, so code that rescues them can be tested
|
|
490
|
+
* `X::HTTPError.new(status:, headers:, body:, http_method: nil, uri: nil)`, or `http_response:` in their place
|
|
491
|
+
* `raise X::NotFound` and `raise X::TooManyRequests, "slow down"` work as for any exception
|
|
492
|
+
* `X::Response.new(http_method:, uri:, status:, headers:, body:)`, or `http_response:` in their place
|
|
493
|
+
* A status outside 100 to 599, or headers that are not a Hash, raise `ArgumentError`
|
|
494
|
+
* Name the internal classes of `x-core` under `X::Core` and those of `x-streams` under `X::Streams`, as private constants
|
|
495
|
+
* Make `X::Connection` the internal `X::Core::Connection`
|
|
496
|
+
* The authenticators take no `connection:`
|
|
497
|
+
* `X::OAuth2Authorization` takes `base_url`, `proxy_url`, `open_timeout`, `read_timeout`, `write_timeout`, `debug_output`, and `headers` instead
|
|
498
|
+
* Make the constants that hold messages, patterns, or internal defaults of `x-core` private
|
|
499
|
+
* `X::HTTPError::JSON_CONTENT_TYPE_REGEXP` and `X::OAuth2Authenticator::EXPIRATION_BUFFER`
|
|
500
|
+
* Make `X::Uploads::Validator` and the MIME type and size constants of `X::Uploads::MediaUpload` private
|
|
501
|
+
* `MIME_TYPES`, `MIME_TYPE_MAP`, the `*_MIME_TYPE` constants, `BYTES_PER_MB`, `MAX_SIMPLE_UPLOAD_BYTES`, and the endpoints of `X::Uploads::Account`
|
|
502
|
+
* The media category constants, such as `TWEET_IMAGE`, remain public
|
|
503
|
+
* Make `infer_media_type` of `X::Uploads::MediaUpload` internal; pass `media_type:` to override the type an upload infers
|
|
504
|
+
* Ship the signatures of the public interface alone in each gem
|
|
505
|
+
* Document every error class of `x-core` and draw the whole hierarchy on `X::Error`
|
|
506
|
+
* Sign OAuth 1.0a requests, and build OAuth 2.0 token refreshes, with the [simple_oauth](https://github.com/laserlemon/simple_oauth) gem
|
|
507
|
+
* Take media as a path (`String` or `Pathname`) or an IO in `upload_media`, `upload`, `chunked_upload`, `update_profile_image`, and `update_profile_banner`
|
|
508
|
+
* A `File` or `Tempfile` is read a chunk at a time; another IO, such as a `StringIO`, is read whole
|
|
509
|
+
* A String holding a NUL byte or a line break raises `ArgumentError`; pass a path or a `StringIO`
|
|
510
|
+
* An IO that cannot be read, or that the system refuses to read, such as a socket that is not connected, or a `File` closed while it is read, raises `X::InvalidMedia`
|
|
511
|
+
* Infer the type of media from the bytes it begins with before the name of its file
|
|
512
|
+
* GIF, PNG, JPEG, BMP, TIFF, WebP, MP4, QuickTime, WebM, MPEG-TS, and WebVTT are recognized
|
|
513
|
+
* Media of a type nothing names, such as HEIC or SubRip in a `StringIO`, raises `X::InvalidMediaType` without `media_category:`
|
|
514
|
+
* A file named as a type every file of which begins with a signature, but without it, such as TypeScript named `.ts`, raises `X::InvalidMediaType`; otherwise the bytes win over the name
|
|
515
|
+
* A type the media category does not take, such as MP4 with `"tweet_gif"`, raises `X::InvalidMediaType`
|
|
516
|
+
* Raise `X::MissingMediaData` instead of `KeyError` or nil when an upload, status check, or metadata response holds no media
|
|
517
|
+
* Its `problems` hold the problems the response reported, the first named in its message
|
|
518
|
+
* Raise `X::InvalidMedia` for a file that does not exist, and `X::MediaProcessingFailed` for media that fails to process, instead of `RuntimeError`
|
|
519
|
+
* `X::MediaProcessingFailed#media` holds what X reported as an `X::UploadedMedia`
|
|
520
|
+
* Raise `ArgumentError` from `await_processing`, `add_alt_text`, and `add_subtitles` for media without an `"id"`
|
|
521
|
+
* Raise `ArgumentError` from `add_subtitles` for a language code that is not two letters
|
|
522
|
+
* Upload in chunks of 4 MB, `X::Uploads::MediaUpload::DEFAULT_CHUNK_SIZE`, rather than 1 MB, so a video takes a quarter of the requests
|
|
523
|
+
* A file larger than the 16 GB the API takes raises `X::InvalidMedia` before the upload
|
|
524
|
+
* `chunk_size:` replaces `chunk_size_mb:`, takes bytes, and defaults to nil, which uploads in chunks of 4 MB
|
|
525
|
+
* Upload an animated GIF larger than 5 MB in chunks, which the API takes up to 15 MB of
|
|
526
|
+
* Validate the `alt_text:` of `upload` before uploading, so media is not lost to a refused alt text
|
|
527
|
+
* Post profile images and banners to the API v1.1 through the client's base URL, credentials, and connections
|
|
528
|
+
* Keep the state and helpers of `X::Client` in an internal object, so mixed-in methods never collide with them
|
|
529
|
+
|
|
530
|
+
### Removed
|
|
531
|
+
* Remove `X::MediaUploader.upload_binary`; pass a `StringIO` to `X::Uploads::MediaUpload.upload`
|
|
532
|
+
* Remove `upload_profile_image_binary` and `upload_profile_banner_binary`; pass an IO to `update_profile_image` and `update_profile_banner`
|
|
533
|
+
* Remove `require "x/media_uploader"` and `require "x/account_uploader"`; require `x`, `x/uploads/media_upload`, or `x/uploads/account`
|
|
534
|
+
* Remove the `boundary:` of the upload methods, which each upload now generates for itself
|
|
535
|
+
* Remove `X::AccountUploader::MIME_TYPE_MAP`
|
|
536
|
+
* Remove `X::Uploads::MediaUpload::PROCESSING_INFO_STATES`; use `X::UploadedMedia#processing?`
|
|
537
|
+
* Remove `X::Uploads::MediaUpload::MAX_RETRIES`; a chunk is retried up to the client's `max_retries`
|
|
538
|
+
* Remove `X::HTTPError#code`; use `#status`, an Integer, or `error.http_response.code`
|
|
539
|
+
* Remove `X::HTTPError#error_message` and `#message_from_json_response`, and make `#json?` private
|
|
540
|
+
* Remove `X::RateLimit#retry_after`; read `#reset_in`
|
|
541
|
+
* Remove the setters of `X::Client`; derive a client that differs with `X::Client#with`
|
|
542
|
+
* `api_key=`, `api_key_secret=`, `access_token=`, `access_token_secret=`, `bearer_token=`, `client_id=`, `client_secret=`, `refresh_token=`
|
|
543
|
+
* `base_url=`, `default_array_class=`, `default_object_class=`, `open_timeout=`, `read_timeout=`, `write_timeout=`, `proxy_url=`, `debug_output=`, `max_redirects=`
|
|
544
|
+
* Remove the setters of `X::Connection`, `X::RateLimit`, `X::BearerTokenAuthenticator`, `X::OAuth1Authenticator`, and `X::OAuth2Authenticator`
|
|
545
|
+
* Remove `X::Connection::DEFAULT_HOST` and `DEFAULT_PORT`
|
|
546
|
+
* Remove `X::OAuth1Authenticator#access_token`; `user_id` reads the user the token acts for
|
|
547
|
+
* Remove `X::OAuthAuthenticator::OAUTH_SIGNATURE_ALGORITHM`, `OAUTH_VERSION`, and `OAUTH_SIGNATURE_METHOD`
|
|
548
|
+
* Remove `X::OAuth2Authenticator::REFRESH_GRANT_TYPE`, `TOKEN_HOST`, and `TOKEN_PATH`; tokens are requested at the origin of the `base_url`
|
|
549
|
+
* Remove the `base64` dependency
|
|
550
|
+
|
|
551
|
+
### Fixed
|
|
552
|
+
* Keep the method and body of a `PUT` or `DELETE` that a 301 or 302 redirects, as RFC 9110 has it
|
|
553
|
+
* A `POST` redirected by a 301 or 302, and any request redirected by a 303, is still followed with a `GET`
|
|
554
|
+
* Resolve a relative redirect against the URL of the request, rather than the base URL
|
|
555
|
+
* Raise `X::HTTPError` for a redirect that cannot be followed, instead of `KeyError`, `URI::InvalidURIError`, or `ArgumentError`
|
|
556
|
+
* A 300, 304, or 305, or one whose `Location` is missing, invalid, or not HTTP or HTTPS
|
|
557
|
+
* Send the credentials on every redirected request, not only the first
|
|
558
|
+
* Drop the credentials and any `Authorization`, `Cookie`, or `Proxy-Authorization` header on a redirect to another origin
|
|
559
|
+
* Preserve the headers passed to `get`, `post`, `put`, and `delete` across redirects
|
|
560
|
+
* Send no `Authorization` header from a client without credentials, rather than an empty one
|
|
561
|
+
* Send a `Content-Type` header only with a request that carries a body
|
|
562
|
+
* Build the message of an `X::HTTPError` from a body that is not JSON, or whose errors have no `message`
|
|
563
|
+
* Instead of raising `JSON::ParserError`, `KeyError`, or `TypeError` in place of the error
|
|
564
|
+
* Raise `X::ClientError` or `X::ServerError` for an unnamed 4xx or 5xx status, such as 411 or 501, instead of `X::HTTPError`
|
|
565
|
+
* Raise the errors of `on_response`, a request's block, or the object class as they were raised
|
|
566
|
+
* The request is not retried, rate-limited, or refreshed, and a stream does not reconnect for them
|
|
567
|
+
* Send a query parameter without a value, as the `flag` of `get("users?flag")`, without `=`
|
|
568
|
+
* Connect to a host or proxy named by an IPv6 literal, such as `http://[::1]:8080/`; `x-core` depends on net-http 0.9.1 or later
|
|
569
|
+
* End a `base_url` without a trailing slash with one, so `https://api.x.com/2` requests `/2/users/me`
|
|
570
|
+
* Take the proxy from `https_proxy` for HTTPS requests and `http_proxy` for HTTP, and honor `no_proxy`
|
|
571
|
+
* A proxy there that cannot be parsed, or that is not an http or https URL with a host, as `proxy.example.com:8080` is not, raises `ArgumentError` from each request that opens a connection, and from `get_stream`, naming the variables read for its scheme and leaving out the value
|
|
572
|
+
* Connect to an `https://` proxy over TLS
|
|
573
|
+
* Decode a percent-encoded proxy user and password
|
|
574
|
+
* Leave the proxy user and password out of the message of an invalid proxy URL, and raise `ArgumentError` for one
|
|
575
|
+
* Sign a form-encoded request body with OAuth 1.0a
|
|
576
|
+
* Sign every value of a repeated query parameter
|
|
577
|
+
* Sign the normalized URL, so a request to a host with no path signs `/`
|
|
578
|
+
* Form-encode the client credentials before Basic authentication on token refresh, as RFC 6749 Section 2.3.1 requires
|
|
579
|
+
* Refresh an OAuth 2.0 token through the connection of the client that sends the request, with its proxy and timeouts
|
|
580
|
+
* Declare `X::BadGateway` and `X::GatewayTimeout` as `X::ServerError`s in the signatures
|
|
581
|
+
* Declare the standard libraries each gem's signatures refer to in its `sig/manifest.yaml`
|
|
582
|
+
* Link each gem's `changelog_uri` to the `main` branch rather than `master`
|
|
583
|
+
* Parse uploader responses into Hashes and Arrays whatever the client's `default_object_class` and `default_array_class`
|
|
584
|
+
* Return nil from `update_profile_image` and `update_profile_banner`; look the user up to read what it holds
|
|
585
|
+
* Raise `X::InvalidMedia` from `update_profile_image` and `update_profile_banner` for an empty or oversized file
|
|
586
|
+
* A profile image larger than 700 KB or a banner larger than 5 MB
|
|
587
|
+
* `X::InvalidMediaType` for one that is not a GIF, JPEG, or PNG; `ArgumentError` for a non-integer banner offset or size
|
|
588
|
+
* Accept the `amplify_video` media category, and subtitle such a video with `add_subtitles`
|
|
589
|
+
* Upload every media type the API documents: WebM, QuickTime, and MPEG-TS videos, WebVTT subtitles, and BMP, TIFF, and progressive JPEG images
|
|
590
|
+
* Upload an `.m4v` file as an MP4 video, in chunks
|
|
591
|
+
* Raise `X::InvalidMediaType` before a request for `.glb` and `.usdz` files, and for `.avi` and `.mkv` files unless they begin with the signature of a type the API documents, as WebM does
|
|
592
|
+
* Upload subtitles in chunks as `text/srt`, the type the API names
|
|
593
|
+
* Send the media category in lowercase, as the API documents it
|
|
594
|
+
* Raise `X::InvalidMedia` from the uploaders, before any request, for media that cannot be uploaded
|
|
595
|
+
* An empty file, a directory, a file that cannot be read, or an IO open for writing alone
|
|
596
|
+
* A `Pathname` of a missing file, as for a `String`, instead of `TypeError`
|
|
597
|
+
* An image over 5 MB, a GIF over 15 MB, or subtitles over 1 MB
|
|
598
|
+
* Raise `X::MissingMediaData` before a chunk is uploaded when the initialize response holds no media, instead of `NoMethodError`
|
|
599
|
+
* Give up waiting for media to process after the `processing_timeout:` (default 600 seconds), raising `X::MediaProcessingTimeout`
|
|
600
|
+
* Taken by `upload`, `await_processing`, `await_processing!`, `upload_media`, and `await_media_processing(!)`
|
|
601
|
+
* `processing_timeout:` takes a finite number of seconds of at least 0, or nil for no limit
|
|
602
|
+
* Anything else, `Float::INFINITY` included, raises `ArgumentError` before the first request
|
|
603
|
+
* Checks wait at least a second apart when X asks for no wait
|
|
604
|
+
* Only `succeeded` and `failed` end the wait; any other state, even one X does not document, is still `processing?`
|
|
605
|
+
* Retry a failed chunk, and the finalize request, with the client's `max_retries`, backing off rather than retrying at once
|
|
606
|
+
* Upload the chunks of a video four at a time, or the `concurrency:` of `chunked_upload` (at most 16), instead of a thread per chunk
|
|
607
|
+
* A chunk that fails stops the chunks not yet begun
|
|
608
|
+
* Stop the chunks of a chunked upload when the waiting thread is interrupted, such as by a timeout
|
|
609
|
+
* Including a `ThreadError` or interrupt while the chunk threads are being started
|
|
610
|
+
* A chunk storing the tokens of a refresh it made with `save_tokens` finishes storing them first
|
|
611
|
+
* A chunk reading from the file finishes reading first, so the file is closed rather than left open
|
|
612
|
+
* Raise `ArgumentError` from `chunked_upload`, before any request, for an invalid `chunk_size` or `concurrency`
|
|
613
|
+
* `chunk_size` must be a positive Integer of at most 5,242,880 bytes, within the 10,000 segments the API numbers
|
|
614
|
+
* `concurrency` must be an Integer from 1 to `MAX_CONCURRENCY` (16)
|
|
615
|
+
* Read the size of chunked upload media once, so a growing file uploads the declared `total_bytes`
|
|
616
|
+
* Give a class that includes `X::Uploads::MediaUpload` or `X::Uploads::Account` their public methods and constants alone, so its own methods do not break uploads
|
|
617
|
+
|
|
1
618
|
## [0.19.0] - 2026-03-01
|
|
2
619
|
* Add streaming support for filtered stream and volume stream endpoints
|
|
3
620
|
|
|
@@ -56,7 +673,6 @@
|
|
|
56
673
|
* Add `code` attribute to error classes (b003639)
|
|
57
674
|
|
|
58
675
|
## [0.11.0] - 2023-10-24
|
|
59
|
-
|
|
60
676
|
* Add base Authenticator class (8c66ce2)
|
|
61
677
|
* Consistently use keyword arguments (3beb271)
|
|
62
678
|
* Use patern matching to build request (4d001c7)
|
|
@@ -69,71 +685,88 @@
|
|
|
69
685
|
* Make Connection class threadsafe (d95d285)
|
|
70
686
|
|
|
71
687
|
## [0.10.0] - 2023-10-08
|
|
72
|
-
|
|
73
688
|
* Add media upload helper methods (6c6a267)
|
|
74
689
|
* Add PayloadTooLargeError class (cd61850)
|
|
75
690
|
|
|
76
691
|
## [0.9.1] - 2023-10-06
|
|
77
|
-
|
|
78
692
|
* Allow successful empty responses (06bf7db)
|
|
79
693
|
* Update default User-Agent string (296b36a)
|
|
80
694
|
* Move query parameter escaping into RequestBuilder (56d6bd2)
|
|
81
695
|
|
|
82
696
|
## [0.9.0] - 2023-09-26
|
|
83
|
-
|
|
84
697
|
* Add support for HTTP proxies (3740f4f)
|
|
85
698
|
|
|
86
699
|
## [0.8.1] - 2023-09-20
|
|
87
|
-
|
|
88
700
|
* Fix bug where setting Connection#base_uri= doesn't update the HTTP client (d5a89db)
|
|
89
701
|
|
|
90
702
|
## [0.8.0] - 2023-09-14
|
|
91
|
-
|
|
92
703
|
* Add (back) bearer token authentication (62e141d)
|
|
93
704
|
* Follow redirects (90a8c55)
|
|
94
705
|
* Parse error responses with Content-Type: application/problem+json (0b697d9)
|
|
95
706
|
|
|
96
707
|
## [0.7.1] - 2023-09-02
|
|
97
|
-
|
|
98
708
|
* Fix bug in X::Authenticator#split_uri (ebc9d5f)
|
|
99
709
|
|
|
100
710
|
## [0.7.0] - 2023-09-02
|
|
101
|
-
|
|
102
711
|
* Remove OAuth gem (7c29bb1)
|
|
103
712
|
|
|
104
713
|
## [0.6.0] - 2023-08-30
|
|
105
|
-
|
|
106
714
|
* Add configurable debug output stream for logging (fd2d4b0)
|
|
107
715
|
* Remove bearer token authentication (efff940)
|
|
108
716
|
* Define RBS type signatures (d7f63ba)
|
|
109
717
|
|
|
110
718
|
## [0.5.1] - 2023-08-16
|
|
111
|
-
|
|
112
719
|
* Fix bearer token authentication (1a1ca93)
|
|
113
720
|
|
|
114
721
|
## [0.5.0] - 2023-08-10
|
|
115
|
-
|
|
116
722
|
* Add configurable write timeout (2a31f84)
|
|
117
723
|
* Use built-in Gem::Version class (066e0b6)
|
|
118
724
|
|
|
119
725
|
## [0.4.0] - 2023-08-06
|
|
120
|
-
|
|
121
726
|
* Refactor Client into Authenticator, RequestBuilder, Connection, ResponseHandler (6bee1e9)
|
|
122
727
|
* Add configurable open timeout (1000f9d)
|
|
123
728
|
* Allow configuration of content type (f33a732)
|
|
124
729
|
|
|
125
730
|
## [0.3.0] - 2023-08-04
|
|
126
|
-
|
|
127
731
|
* Add accessors to X::Client (e61fa73)
|
|
128
732
|
* Add configurable read timeout (41502b9)
|
|
129
733
|
* Handle network-related errors (9ed1fb4)
|
|
130
734
|
* Include response body in errors (a203e6a)
|
|
131
735
|
|
|
132
736
|
## [0.2.0] - 2023-08-02
|
|
133
|
-
|
|
134
737
|
* Allow configuration of base URL (4bc0531)
|
|
135
738
|
* Improve error handling (14dc0cd)
|
|
136
739
|
|
|
137
740
|
## [0.1.0] - 2023-08-02
|
|
138
|
-
|
|
139
741
|
* Initial release
|
|
742
|
+
|
|
743
|
+
[1.0.0]: https://github.com/sferik/x-ruby/compare/v0.19.0...v1.0.0
|
|
744
|
+
[0.19.0]: https://github.com/sferik/x-ruby/compare/v0.18.0...v0.19.0
|
|
745
|
+
[0.18.0]: https://github.com/sferik/x-ruby/compare/v0.17.0...v0.18.0
|
|
746
|
+
[0.17.0]: https://github.com/sferik/x-ruby/compare/v0.16.0...v0.17.0
|
|
747
|
+
[0.16.0]: https://github.com/sferik/x-ruby/compare/v0.15.4...v0.16.0
|
|
748
|
+
[0.15.4]: https://github.com/sferik/x-ruby/compare/v0.15.3...v0.15.4
|
|
749
|
+
[0.15.3]: https://github.com/sferik/x-ruby/compare/v0.15.2...v0.15.3
|
|
750
|
+
[0.15.2]: https://github.com/sferik/x-ruby/compare/v0.15.1...v0.15.2
|
|
751
|
+
[0.15.1]: https://github.com/sferik/x-ruby/compare/v0.15.0...v0.15.1
|
|
752
|
+
[0.15.0]: https://github.com/sferik/x-ruby/compare/v0.14.1...v0.15.0
|
|
753
|
+
[0.14.1]: https://github.com/sferik/x-ruby/compare/v0.14.0...v0.14.1
|
|
754
|
+
[0.14.0]: https://github.com/sferik/x-ruby/compare/v0.13.0...v0.14.0
|
|
755
|
+
[0.13.0]: https://github.com/sferik/x-ruby/compare/v0.12.1...v0.13.0
|
|
756
|
+
[0.12.1]: https://github.com/sferik/x-ruby/compare/v0.12.0...v0.12.1
|
|
757
|
+
[0.12.0]: https://github.com/sferik/x-ruby/compare/v0.11.0...v0.12.0
|
|
758
|
+
[0.11.0]: https://github.com/sferik/x-ruby/compare/v0.10.0...v0.11.0
|
|
759
|
+
[0.10.0]: https://github.com/sferik/x-ruby/compare/v0.9.1...v0.10.0
|
|
760
|
+
[0.9.1]: https://github.com/sferik/x-ruby/compare/v0.9.0...v0.9.1
|
|
761
|
+
[0.9.0]: https://github.com/sferik/x-ruby/compare/v0.8.1...v0.9.0
|
|
762
|
+
[0.8.1]: https://github.com/sferik/x-ruby/compare/v0.8.0...v0.8.1
|
|
763
|
+
[0.8.0]: https://github.com/sferik/x-ruby/compare/v0.7.1...v0.8.0
|
|
764
|
+
[0.7.1]: https://github.com/sferik/x-ruby/compare/v0.7.0...v0.7.1
|
|
765
|
+
[0.7.0]: https://github.com/sferik/x-ruby/compare/v0.6.0...v0.7.0
|
|
766
|
+
[0.6.0]: https://github.com/sferik/x-ruby/compare/v0.5.1...v0.6.0
|
|
767
|
+
[0.5.1]: https://github.com/sferik/x-ruby/compare/v0.5.0...v0.5.1
|
|
768
|
+
[0.5.0]: https://github.com/sferik/x-ruby/compare/v0.4.0...v0.5.0
|
|
769
|
+
[0.4.0]: https://github.com/sferik/x-ruby/compare/v0.3.0...v0.4.0
|
|
770
|
+
[0.3.0]: https://github.com/sferik/x-ruby/compare/v0.2.0...v0.3.0
|
|
771
|
+
[0.2.0]: https://github.com/sferik/x-ruby/compare/v0.1.0...v0.2.0
|
|
772
|
+
[0.1.0]: https://github.com/sferik/x-ruby/releases/tag/v0.1.0
|