rito 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/README.md +46 -12
- data/lib/rito/client.rb +36 -7
- data/lib/rito/endpoints/base.rb +8 -3
- data/lib/rito/endpoints/lol/league_v4.rb +2 -2
- data/lib/rito/endpoints/lol/tournament_v5.rb +4 -2
- data/lib/rito/endpoints/lor.rb +0 -10
- data/lib/rito/endpoints/riftbound.rb +15 -0
- data/lib/rito/endpoints/tft.rb +15 -7
- data/lib/rito/endpoints/valorant.rb +8 -3
- data/lib/rito/middleware/rate_limit_observe.rb +4 -4
- data/lib/rito/middleware/rate_limit_throttle.rb +11 -25
- data/lib/rito/middleware/riot_errors.rb +12 -23
- data/lib/rito/models/account.rb +3 -3
- data/lib/rito/models/lol.rb +4 -4
- data/lib/rito/models/match.rb +6 -6
- data/lib/rito/models/summoner.rb +1 -1
- data/lib/rito/models.rb +18 -0
- data/lib/rito/rate_limiting/adaptive_limiter.rb +24 -15
- data/lib/rito/rate_limiting/bucket.rb +37 -22
- data/lib/rito/rate_limiting/header_parser.rb +3 -1
- data/lib/rito/rate_limiting/redis_limiter.rb +51 -27
- data/lib/rito/routing.rb +5 -4
- data/lib/rito/rso/models.rb +2 -2
- data/lib/rito/version.rb +1 -1
- data/lib/rito.rb +3 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ad417d4efb4e93230ccc8e4aba43faaf13b6a6e9f39894e8a0e60ef0666e8ec
|
|
4
|
+
data.tar.gz: f508bc188dbda8b54f8159b9a3382c00cc93145b7188054558a4c9da377ec244
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8dde9a5eac05156304fd1fe8d43e4b9463982eef6e9926b366fb589a17447217b744e19e22dca69151ff268a41f967d1e6e767514cdf08b0a060a46686a4638
|
|
7
|
+
data.tar.gz: cf3993148793c8b5f81b89e27eba30704d63ab74eea357d6ea99ce93eb6a244db483f09ed4b1429f774ec81e969cd5665beffacd32b3d493ddf07257fa32b819
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 (2026-09-05)
|
|
4
|
+
|
|
5
|
+
- **Breaking:** LoL `leagues.entries` now requires `queue` before `tier` and
|
|
6
|
+
`division`, matching the live API path.
|
|
7
|
+
- Tournament and tournament-stub requests route to `americas`; `na1` escalates
|
|
8
|
+
automatically, and unsupported clusters are rejected.
|
|
9
|
+
- Account routing normalizes SEA platform values (including `sg2`) to `asia`.
|
|
10
|
+
VALORANT routing values work as client defaults.
|
|
11
|
+
- **Breaking:** Explicit API-key or bearer credentials no longer inherit the other global
|
|
12
|
+
credential. Explicit `nil` clears credentials; mixed credentials are rejected.
|
|
13
|
+
- Transport failures retry before conversion to Rito errors. Non-JSON HTTP
|
|
14
|
+
error bodies preserve the HTTP error class; 403 hints include routing and access.
|
|
15
|
+
Retried 429 POSTs preserve their original request body.
|
|
16
|
+
- Rate-limit buckets isolate API families. Headerless 429 backoff grows
|
|
17
|
+
exponentially, learned windows survive expiry, deadlines do not slide with
|
|
18
|
+
responses, and older counts do not erase in-flight reservations. Application
|
|
19
|
+
and method quota are acquired together.
|
|
20
|
+
- Redis quota checks reserve all windows atomically, preserve counter TTLs,
|
|
21
|
+
accept zero Retry-After, retain longer blocks, and back off across workers.
|
|
22
|
+
- Request instrumentation emits one completion event for successes and errors,
|
|
23
|
+
including bearer requests, with total network attempts and elapsed time.
|
|
24
|
+
- Model fields and raw payloads are recursively frozen. Documentation identifies
|
|
25
|
+
endpoints returning parsed JSON and replaces removed method examples.
|
|
26
|
+
- Added a schema snapshot contract test, live-path regressions, retry and
|
|
27
|
+
instrumentation tests, and Redis concurrency coverage. Explicitly configured
|
|
28
|
+
Redis must be available for tests.
|
|
29
|
+
|
|
30
|
+
## 0.3.1 (2026-09-05)
|
|
31
|
+
|
|
32
|
+
Fixed endpoint diffs against the current riotapi-schema (last generated
|
|
33
|
+
2026-08-05):
|
|
34
|
+
|
|
35
|
+
- **val-console-match-v1 paths were wrong** (`/val/console/match/v1/...` →
|
|
36
|
+
`/val/match/console/v1/...`) — every console match call would 404.
|
|
37
|
+
The matchlist now sends the required `platformType` query param
|
|
38
|
+
(`ids_by_puuid` gained a required `platform_type:` keyword) and
|
|
39
|
+
`recent_by_queue` was added (GET recent-matches/by-queue).
|
|
40
|
+
- TFT match `ids_by_puuid` gained `start_time:` / `end_time:` params.
|
|
41
|
+
- TFT league `challenger` / `grandmaster` / `master` gained an optional
|
|
42
|
+
`queue:` param.
|
|
43
|
+
- `apac` is now a valid regional (lor-match-v1).
|
|
44
|
+
- The Riftbound endpoint class moved out of the LoR file into its own
|
|
45
|
+
`endpoints/riftbound.rb` (no API change).
|
|
46
|
+
- CI: GitHub releases now use the tagged version's CHANGELOG section as
|
|
47
|
+
the release body instead of auto-generated notes.
|
|
48
|
+
|
|
3
49
|
## 0.3.0 (2026-09-05)
|
|
4
50
|
|
|
5
51
|
Aligned the endpoint surface with the current Riot API reference
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A modern, production-ready Ruby client for the Riot Games API: League of
|
|
|
4
4
|
Legends, TFT, VALORANT, Legends of Runeterra, Riftbound, and the Riot Account API.
|
|
5
5
|
|
|
6
6
|
Full routing-value handling, header-driven adaptive rate limiting, frozen
|
|
7
|
-
`Data` models, and
|
|
7
|
+
`Data` models, and Faraday transport with faraday-retry.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -42,8 +42,14 @@ Rito.rate_limiter # rate limit strategy (default: AdaptiveLimiter)
|
|
|
42
42
|
|
|
43
43
|
# per-client, for multi-key apps:
|
|
44
44
|
client = Rito::Client.new(api_key: "RGAPI-...", region: :kr)
|
|
45
|
+
rso_client = Rito::Client.new(bearer_token: access_token, region: :americas)
|
|
45
46
|
```
|
|
46
47
|
|
|
48
|
+
Explicit credentials select API-key or bearer authentication without inheriting
|
|
49
|
+
the other credential from global config. Use separate clients for API-key and
|
|
50
|
+
RSO requests; configuring both credentials on one client raises `ConfigError`.
|
|
51
|
+
Explicit `nil` clears a credential.
|
|
52
|
+
|
|
47
53
|
## Queues
|
|
48
54
|
|
|
49
55
|
`Rito::Queues` is a frozen, built-in reference for League queue ids as
|
|
@@ -178,9 +184,10 @@ limiter = Rito::RateLimiting::RedisLimiter.new(redis: Redis.new)
|
|
|
178
184
|
client = Rito::Client.new(rate_limiter: limiter)
|
|
179
185
|
```
|
|
180
186
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
`acquire!` blocks the calling thread while throttled, so use background jobs
|
|
188
|
+
for work that must not block a web request. `max_retries: 0` disables retries;
|
|
189
|
+
it does not disable pre-emptive waiting. `NullLimiter` disables limiter-based
|
|
190
|
+
waiting entirely.
|
|
184
191
|
|
|
185
192
|
### Multi-process (Redis)
|
|
186
193
|
|
|
@@ -207,16 +214,25 @@ URL, and Riot's error detail.
|
|
|
207
214
|
|
|
208
215
|
## Models
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
Account, summoner, LoL match, mastery, rotation, and league responses have
|
|
218
|
+
`Data` models. Their mapped fields and `.raw` payloads are recursively frozen;
|
|
219
|
+
unknown fields Riot adds later are preserved in `.raw`. Other endpoints return
|
|
220
|
+
parsed JSON hashes, arrays, or scalar values, including timelines, status,
|
|
221
|
+
Clash, challenges, tournaments, and most TFT, VALORANT, and LoR responses:
|
|
212
222
|
|
|
213
223
|
```ruby
|
|
214
|
-
entry = client.leagues.
|
|
224
|
+
entry = client.leagues.entries_by_puuid(puuid).first
|
|
215
225
|
entry.tier # => "DIAMOND"
|
|
216
226
|
entry.winrate # => 57.14
|
|
217
227
|
entry.raw # => full payload hash
|
|
218
228
|
```
|
|
219
229
|
|
|
230
|
+
League listings require a queue, tier, and division:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
client.leagues.entries("RANKED_SOLO_5x5", "DIAMOND", "I", page: 1)
|
|
234
|
+
```
|
|
235
|
+
|
|
220
236
|
## Coverage
|
|
221
237
|
|
|
222
238
|
| Accessor | API |
|
|
@@ -242,8 +258,9 @@ Note: VALORANT uses its own platform routing values (`na, eu, ap, kr,
|
|
|
242
258
|
latam, br`; console: `na, eu, ap, br, latam`) — the client validates
|
|
243
259
|
against the right set per product. `esports` is a valid platform for
|
|
244
260
|
VALORANT content/match and a valid regional for tft-match-v1 (with
|
|
245
|
-
`esportseu`)
|
|
246
|
-
|
|
261
|
+
`esportseu`); `apac` is a valid regional for lor-match-v1. The
|
|
262
|
+
tournament endpoints only exist on the `americas`
|
|
263
|
+
host — pass `region: :na1` or `:americas`. Other regional clusters are rejected.
|
|
247
264
|
|
|
248
265
|
## Testing your own app
|
|
249
266
|
|
|
@@ -256,8 +273,11 @@ bundle exec rake test
|
|
|
256
273
|
|
|
257
274
|
## Instrumentation
|
|
258
275
|
|
|
259
|
-
If ActiveSupport is present (e.g. in a Rails app),
|
|
260
|
-
`request.rito` notification
|
|
276
|
+
If ActiveSupport is present (e.g. in a Rails app), each `Rito::Client` request
|
|
277
|
+
emits one `request.rito` notification after it completes, including failures
|
|
278
|
+
and bearer requests. `attempts` counts network attempts across transport and
|
|
279
|
+
429 retries; `duration_ms` includes retry and limiter waits. Configuration and
|
|
280
|
+
routing errors that prevent a request do not emit an event:
|
|
261
281
|
|
|
262
282
|
```ruby
|
|
263
283
|
ActiveSupport::Notifications.subscribe("request.rito") do |*, payload|
|
|
@@ -274,10 +294,24 @@ Without ActiveSupport, instrumentation is a no-op.
|
|
|
274
294
|
|
|
275
295
|
```
|
|
276
296
|
bundle install
|
|
297
|
+
bundle exec rake lint
|
|
277
298
|
bundle exec rake test
|
|
278
299
|
```
|
|
279
300
|
|
|
280
|
-
Ruby 3.2+ required (`Data.define`).
|
|
301
|
+
Ruby 3.2+ required (`Data.define`). Redis tests use `REDIS_URL` when set and
|
|
302
|
+
fail if that configured Redis is unavailable; otherwise they try localhost
|
|
303
|
+
and skip if it is unavailable.
|
|
304
|
+
|
|
305
|
+
The endpoint contract test uses a checked-in snapshot of riotapi-schema
|
|
306
|
+
(paths, routing hosts, and query parameters). Refresh it with
|
|
307
|
+
`bundle exec rake api_routes`, review the diff, and run the suite.
|
|
308
|
+
|
|
309
|
+
Live read-only checks use `bundle exec ruby scripts/live_matrix.rb` with
|
|
310
|
+
`RIOT_API_KEY_TEST` and `RIOT_API_KEY_PROD` in `.env`. Access varies by key and
|
|
311
|
+
product: a 403 can indicate missing product access or an incorrect path/host,
|
|
312
|
+
as well as an expired key. API keys cannot validate RSO-only endpoints;
|
|
313
|
+
those need an RSO access token. Synthetic-id 404s establish no successful
|
|
314
|
+
payload coverage.
|
|
281
315
|
|
|
282
316
|
## License
|
|
283
317
|
|
data/lib/rito/client.rb
CHANGED
|
@@ -5,10 +5,13 @@ module Rito
|
|
|
5
5
|
attr_reader :api_key, :bearer_token, :region, :limiter, :max_retries,
|
|
6
6
|
:open_timeout, :timeout, :adapter
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
UNSET = Object.new.freeze
|
|
9
|
+
private_constant :UNSET
|
|
10
|
+
|
|
11
|
+
def initialize(api_key: UNSET, bearer_token: UNSET, region: nil, rate_limiter: nil,
|
|
9
12
|
max_retries: nil, open_timeout: nil, timeout: nil, adapter: nil)
|
|
10
|
-
@api_key = api_key
|
|
11
|
-
@bearer_token = bearer_token
|
|
13
|
+
@api_key = api_key.equal?(UNSET) ? (Rito.api_key if bearer_token.equal?(UNSET)) : api_key
|
|
14
|
+
@bearer_token = bearer_token.equal?(UNSET) ? (Rito.bearer_token if api_key.equal?(UNSET)) : bearer_token
|
|
12
15
|
@region = region || Rito.region
|
|
13
16
|
@limiter = rate_limiter || Rito.rate_limiter || RateLimiting::AdaptiveLimiter.new
|
|
14
17
|
@max_retries = max_retries || Rito.max_retries
|
|
@@ -92,16 +95,38 @@ module Rito
|
|
|
92
95
|
Endpoints::Riftbound.new(self)
|
|
93
96
|
end
|
|
94
97
|
|
|
95
|
-
def request(method, path, routing_kind:, routing_value: nil, params: {}, body: nil)
|
|
98
|
+
def request(method, path, routing_kind:, routing_value: nil, regions: nil, params: {}, body: nil)
|
|
96
99
|
region = Routing.resolve(routing_kind, routing_value || @region)
|
|
100
|
+
raise ArgumentError, "#{region.inspect} is not supported by this endpoint" if regions && !regions.include?(region)
|
|
101
|
+
|
|
97
102
|
connection = @connection.for_host(Routing.host_for(region))
|
|
103
|
+
context = { attempts: 0 }
|
|
104
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
105
|
+
url = connection.build_url(path, params).to_s
|
|
98
106
|
response = connection.run_request(method, path, body, {}) do |req|
|
|
107
|
+
req.options.context = context
|
|
99
108
|
req.params.update(params) unless params.empty?
|
|
100
109
|
end
|
|
101
|
-
#
|
|
102
|
-
# its retries (or the status was not retryable).
|
|
110
|
+
# Transport errors and final 5xx must be converted after Faraday's retries.
|
|
103
111
|
raise_server_error!(response) if response.status >= 500
|
|
104
112
|
response
|
|
113
|
+
rescue Faraday::ConnectionFailed => e
|
|
114
|
+
raise ConnectionError.new('Connection failed', wrapped: e)
|
|
115
|
+
rescue Faraday::TimeoutError, ::Timeout::Error => e
|
|
116
|
+
raise TimeoutError.new('Request timed out', wrapped: e)
|
|
117
|
+
rescue Faraday::SSLError => e
|
|
118
|
+
raise SSLError.new('TLS connection failed', wrapped: e)
|
|
119
|
+
ensure
|
|
120
|
+
if context && context[:attempts].positive?
|
|
121
|
+
error = $ERROR_INFO
|
|
122
|
+
Instrumentation.emit(
|
|
123
|
+
http_method: method, url: url,
|
|
124
|
+
status: response&.status || (error.status if error.respond_to?(:status)),
|
|
125
|
+
attempts: context[:attempts],
|
|
126
|
+
duration_ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round(2),
|
|
127
|
+
error: error&.class&.name
|
|
128
|
+
)
|
|
129
|
+
end
|
|
105
130
|
end
|
|
106
131
|
|
|
107
132
|
private
|
|
@@ -114,7 +139,11 @@ module Rito
|
|
|
114
139
|
|
|
115
140
|
def validate!
|
|
116
141
|
raise ConfigError, 'api_key or bearer_token is required' if @api_key.to_s.empty? && @bearer_token.to_s.empty?
|
|
117
|
-
|
|
142
|
+
if @api_key && @bearer_token
|
|
143
|
+
raise ConfigError, 'configure either api_key or bearer_token; use separate clients for API and RSO requests'
|
|
144
|
+
end
|
|
145
|
+
return if Routing.platform?(@region) || Routing.regional?(@region) ||
|
|
146
|
+
Routing::VALORANT_PLATFORMS.include?(Routing.normalize(@region))
|
|
118
147
|
|
|
119
148
|
raise ConfigError, "unknown region: #{@region.inspect}"
|
|
120
149
|
end
|
data/lib/rito/endpoints/base.rb
CHANGED
|
@@ -10,19 +10,24 @@ module Rito
|
|
|
10
10
|
private
|
|
11
11
|
|
|
12
12
|
def get(path, region = nil, params: {})
|
|
13
|
-
@client.request(:get, path, routing_kind: routing_kind, routing_value: region,
|
|
13
|
+
@client.request(:get, path, routing_kind: routing_kind, routing_value: region, regions: regions,
|
|
14
|
+
params: params).body
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def post(path, region = nil, params: {}, body: nil)
|
|
17
|
-
@client.request(:post, path, routing_kind: routing_kind, routing_value: region,
|
|
18
|
+
@client.request(:post, path, routing_kind: routing_kind, routing_value: region, regions: regions,
|
|
18
19
|
params: params, body: body).body
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def put(path, region = nil, params: {}, body: nil)
|
|
22
|
-
@client.request(:put, path, routing_kind: routing_kind, routing_value: region,
|
|
23
|
+
@client.request(:put, path, routing_kind: routing_kind, routing_value: region, regions: regions,
|
|
23
24
|
params: params, body: body).body
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
def regions
|
|
28
|
+
self.class::REGIONS if self.class.const_defined?(:REGIONS)
|
|
29
|
+
end
|
|
30
|
+
|
|
26
31
|
def routing_kind
|
|
27
32
|
self.class::ROUTING
|
|
28
33
|
end
|
|
@@ -29,10 +29,10 @@ module Rito
|
|
|
29
29
|
.map { |hash| Models::LeagueEntry.from_api(hash) }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def entries(tier, division, page: nil, region: nil)
|
|
32
|
+
def entries(queue, tier, division, page: nil, region: nil)
|
|
33
33
|
params = {}
|
|
34
34
|
params['page'] = page if page
|
|
35
|
-
get("/lol/league/v4/entries/#{escape(tier)}/#{escape(division)}", region, params: params)
|
|
35
|
+
get("/lol/league/v4/entries/#{escape(queue)}/#{escape(tier)}/#{escape(division)}", region, params: params)
|
|
36
36
|
.map { |hash| Models::LeagueEntry.from_api(hash) }
|
|
37
37
|
end
|
|
38
38
|
end
|
|
@@ -4,7 +4,8 @@ module Rito
|
|
|
4
4
|
module Endpoints
|
|
5
5
|
module Lol
|
|
6
6
|
class TournamentV5 < Base
|
|
7
|
-
ROUTING = :
|
|
7
|
+
ROUTING = :regional
|
|
8
|
+
REGIONS = %w[americas].freeze
|
|
8
9
|
PREFIX = '/lol/tournament/v5'
|
|
9
10
|
|
|
10
11
|
def code(tournament_code, region: nil)
|
|
@@ -39,7 +40,8 @@ module Rito
|
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
class TournamentStubV5 < Base
|
|
42
|
-
ROUTING = :
|
|
43
|
+
ROUTING = :regional
|
|
44
|
+
REGIONS = %w[americas].freeze
|
|
43
45
|
PREFIX = '/lol/tournament-stub/v5'
|
|
44
46
|
|
|
45
47
|
def code(tournament_code, region: nil)
|
data/lib/rito/endpoints/lor.rb
CHANGED
|
@@ -54,16 +54,6 @@ module Rito
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
|
-
|
|
58
|
-
class Riftbound < Base
|
|
59
|
-
ROUTING = :regional
|
|
60
|
-
|
|
61
|
-
def contents(locale: nil, region: nil)
|
|
62
|
-
params = {}
|
|
63
|
-
params['locale'] = locale if locale
|
|
64
|
-
get('/riftbound/content/v1/contents', region, params: params)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
57
|
end
|
|
68
58
|
|
|
69
59
|
module Lor
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rito
|
|
4
|
+
module Endpoints
|
|
5
|
+
class Riftbound < Base
|
|
6
|
+
ROUTING = :regional
|
|
7
|
+
|
|
8
|
+
def contents(locale: nil, region: nil)
|
|
9
|
+
params = {}
|
|
10
|
+
params['locale'] = locale if locale
|
|
11
|
+
get('/riftbound/content/v1/contents', region, params: params)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/rito/endpoints/tft.rb
CHANGED
|
@@ -31,16 +31,22 @@ module Rito
|
|
|
31
31
|
get("/tft/league/v1/entries/#{escape(tier)}/#{escape(division)}", region, params: params)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def challenger(region: nil)
|
|
35
|
-
|
|
34
|
+
def challenger(queue: nil, region: nil)
|
|
35
|
+
params = {}
|
|
36
|
+
params['queue'] = queue if queue
|
|
37
|
+
get('/tft/league/v1/challenger', region, params: params)
|
|
36
38
|
end
|
|
37
39
|
|
|
38
|
-
def grandmaster(region: nil)
|
|
39
|
-
|
|
40
|
+
def grandmaster(queue: nil, region: nil)
|
|
41
|
+
params = {}
|
|
42
|
+
params['queue'] = queue if queue
|
|
43
|
+
get('/tft/league/v1/grandmaster', region, params: params)
|
|
40
44
|
end
|
|
41
45
|
|
|
42
|
-
def master(region: nil)
|
|
43
|
-
|
|
46
|
+
def master(queue: nil, region: nil)
|
|
47
|
+
params = {}
|
|
48
|
+
params['queue'] = queue if queue
|
|
49
|
+
get('/tft/league/v1/master', region, params: params)
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
def rated_ladder_top(queue, region: nil)
|
|
@@ -51,10 +57,12 @@ module Rito
|
|
|
51
57
|
class MatchV1 < Base
|
|
52
58
|
ROUTING = :regional
|
|
53
59
|
|
|
54
|
-
def ids_by_puuid(puuid, count: nil, start: nil, region: nil)
|
|
60
|
+
def ids_by_puuid(puuid, count: nil, start: nil, start_time: nil, end_time: nil, region: nil)
|
|
55
61
|
params = {}
|
|
56
62
|
params['count'] = count if count
|
|
57
63
|
params['start'] = start if start
|
|
64
|
+
params['startTime'] = start_time if start_time
|
|
65
|
+
params['endTime'] = end_time if end_time
|
|
58
66
|
get("/tft/match/v1/matches/by-puuid/#{escape(puuid)}/ids", region, params: params)
|
|
59
67
|
end
|
|
60
68
|
|
|
@@ -33,11 +33,16 @@ module Rito
|
|
|
33
33
|
ROUTING = :valorant_console_platform
|
|
34
34
|
|
|
35
35
|
def by_id(match_id, region: nil)
|
|
36
|
-
get("/val/console/
|
|
36
|
+
get("/val/match/console/v1/matches/#{escape(match_id)}", region)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def ids_by_puuid(puuid, region: nil)
|
|
40
|
-
get("/val/console/
|
|
39
|
+
def ids_by_puuid(puuid, platform_type:, region: nil)
|
|
40
|
+
get("/val/match/console/v1/matchlists/by-puuid/#{escape(puuid)}", region,
|
|
41
|
+
params: { 'platformType' => platform_type })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def recent_by_queue(queue, region: nil)
|
|
45
|
+
get("/val/match/console/v1/recent-matches/by-queue/#{escape(queue)}", region)
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
|
|
@@ -12,10 +12,10 @@ module Rito
|
|
|
12
12
|
|
|
13
13
|
def on_complete(env)
|
|
14
14
|
limiter = @client.limiter
|
|
15
|
-
|
|
15
|
+
key = @client.api_key || @client.bearer_token
|
|
16
|
+
return if limiter.nil? || key.nil?
|
|
16
17
|
|
|
17
|
-
limiter.observe!(Faraday::Response.new(env), key:
|
|
18
|
-
region: region_from(env), bucket: bucket_from(env))
|
|
18
|
+
limiter.observe!(Faraday::Response.new(env), key: key, region: region_from(env), bucket: bucket_from(env))
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
private
|
|
@@ -25,7 +25,7 @@ module Rito
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def bucket_from(env)
|
|
28
|
-
env.url.path.split('/').first(2).join('/')
|
|
28
|
+
env.url.path.split('/').reject(&:empty?).first(2).join('/')
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -15,43 +15,29 @@ module Rito
|
|
|
15
15
|
|
|
16
16
|
def call(env)
|
|
17
17
|
limiter = @client.limiter
|
|
18
|
-
key = @client.api_key
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
attempts = 0
|
|
22
|
-
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
18
|
+
key = @client.api_key || @client.bearer_token
|
|
19
|
+
retries = 0
|
|
20
|
+
request_body = env.body
|
|
23
21
|
begin
|
|
24
|
-
limiter
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
rescue Rito::RateLimited
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
retry if attempts <= @client.max_retries
|
|
22
|
+
limiter&.acquire!(key: key, region: region_from(env), bucket: bucket_from(env))
|
|
23
|
+
env.body = request_body
|
|
24
|
+
env.request.context[:attempts] += 1 if env.request.context
|
|
25
|
+
@app.call(env)
|
|
26
|
+
rescue Rito::RateLimited
|
|
27
|
+
retries += 1
|
|
28
|
+
retry if retries <= @client.max_retries
|
|
32
29
|
raise
|
|
33
30
|
end
|
|
34
31
|
end
|
|
35
32
|
|
|
36
33
|
private
|
|
37
34
|
|
|
38
|
-
def emit(env, status, started, attempts, error: nil)
|
|
39
|
-
Instrumentation.emit(
|
|
40
|
-
http_method: env.method,
|
|
41
|
-
url: env.url.to_s,
|
|
42
|
-
status: status,
|
|
43
|
-
attempts: attempts,
|
|
44
|
-
duration_ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round(2),
|
|
45
|
-
error: error
|
|
46
|
-
)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
35
|
def region_from(env)
|
|
50
36
|
env.url.host.to_s.split('.').first
|
|
51
37
|
end
|
|
52
38
|
|
|
53
39
|
def bucket_from(env)
|
|
54
|
-
env.url.path.split('/').first(2).join('/')
|
|
40
|
+
env.url.path.split('/').reject(&:empty?).first(2).join('/')
|
|
55
41
|
end
|
|
56
42
|
end
|
|
57
43
|
end
|
|
@@ -14,31 +14,13 @@ module Rito
|
|
|
14
14
|
415 => Rito::UnsupportedMediaType
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
# expire every 24h — users never guess this from a bare "Forbidden".
|
|
19
|
-
KEY_STALENESS_HINT = ' (check the API key: dev keys expire every 24h)'
|
|
17
|
+
FORBIDDEN_HINT = ' (check routing, product access, and the API key: dev keys expire every 24h)'
|
|
20
18
|
|
|
21
19
|
def initialize(app, client:)
|
|
22
20
|
super(app)
|
|
23
21
|
@client = client
|
|
24
22
|
end
|
|
25
23
|
|
|
26
|
-
def call(env)
|
|
27
|
-
@app.call(env).on_complete { |completed| on_complete(completed) }
|
|
28
|
-
rescue Faraday::RetriableResponse => e
|
|
29
|
-
response = e.response
|
|
30
|
-
raise error_class_for(response.status).new(
|
|
31
|
-
"#{response.status} (after transport retries)",
|
|
32
|
-
response: response
|
|
33
|
-
)
|
|
34
|
-
rescue Faraday::ConnectionFailed => e
|
|
35
|
-
raise Rito::ConnectionError.new("#{e.class}: #{e.message}", wrapped: e)
|
|
36
|
-
rescue Faraday::TimeoutError, Timeout::Error => e
|
|
37
|
-
raise Rito::TimeoutError.new("#{e.class}: #{e.message}", wrapped: e)
|
|
38
|
-
rescue Faraday::SSLError => e
|
|
39
|
-
raise Rito::SSLError.new("#{e.class}: #{e.message}", wrapped: e)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
24
|
def on_complete(env)
|
|
43
25
|
return if env.status.between?(200, 299)
|
|
44
26
|
# 5xx are retried by faraday-retry (registered above this middleware);
|
|
@@ -72,14 +54,21 @@ module Rito
|
|
|
72
54
|
end
|
|
73
55
|
end
|
|
74
56
|
|
|
57
|
+
def parse_body(body)
|
|
58
|
+
body = JSON.parse(body) if body.is_a?(String) && !body.empty?
|
|
59
|
+
body.is_a?(Hash) ? body : {}
|
|
60
|
+
rescue JSON::ParserError
|
|
61
|
+
{}
|
|
62
|
+
end
|
|
63
|
+
|
|
75
64
|
def message(env)
|
|
76
|
-
parsed = env.response_body
|
|
77
|
-
|
|
78
|
-
detail = parsed
|
|
65
|
+
parsed = parse_body(env.response_body)
|
|
66
|
+
status = parsed['status']
|
|
67
|
+
detail = parsed['message'] || (status['message'] if status.is_a?(Hash))
|
|
79
68
|
|
|
80
69
|
base = "#{env.status} #{env.method.to_s.upcase} #{env.url}"
|
|
81
70
|
text = detail ? "#{base}: #{detail}" : base
|
|
82
|
-
text +=
|
|
71
|
+
text += FORBIDDEN_HINT if env.status == 403
|
|
83
72
|
text
|
|
84
73
|
end
|
|
85
74
|
end
|
data/lib/rito/models/account.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Rito
|
|
|
8
8
|
puuid: hash['puuid'],
|
|
9
9
|
game_name: hash['gameName'],
|
|
10
10
|
tag_line: hash['tagLine'],
|
|
11
|
-
raw: hash
|
|
11
|
+
raw: Models.deep_freeze(hash)
|
|
12
12
|
)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -19,7 +19,7 @@ module Rito
|
|
|
19
19
|
puuid: hash['puuid'],
|
|
20
20
|
game: hash['game'],
|
|
21
21
|
active_shard: hash['activeShard'],
|
|
22
|
-
raw: hash
|
|
22
|
+
raw: Models.deep_freeze(hash)
|
|
23
23
|
)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -30,7 +30,7 @@ module Rito
|
|
|
30
30
|
puuid: hash['puuid'],
|
|
31
31
|
game: hash['game'],
|
|
32
32
|
region: hash['region'],
|
|
33
|
-
raw: hash
|
|
33
|
+
raw: Models.deep_freeze(hash)
|
|
34
34
|
)
|
|
35
35
|
end
|
|
36
36
|
end
|
data/lib/rito/models/lol.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Rito
|
|
|
17
17
|
champion_points_since_last_level: hash['championPointsSinceLastLevel'],
|
|
18
18
|
last_play_time: hash['lastPlayTime'],
|
|
19
19
|
tokens_earned: hash['tokensEarned'],
|
|
20
|
-
raw: hash
|
|
20
|
+
raw: Models.deep_freeze(hash)
|
|
21
21
|
)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -27,7 +27,7 @@ module Rito
|
|
|
27
27
|
new(
|
|
28
28
|
sr: hash['sr'],
|
|
29
29
|
newplayer: hash['newplayer'],
|
|
30
|
-
raw: hash
|
|
30
|
+
raw: Models.deep_freeze(hash)
|
|
31
31
|
)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -52,7 +52,7 @@ module Rito
|
|
|
52
52
|
fresh_blood: hash['freshBlood'],
|
|
53
53
|
hot_streak: hash['hotStreak'],
|
|
54
54
|
inactive: hash['inactive'],
|
|
55
|
-
raw: hash
|
|
55
|
+
raw: Models.deep_freeze(hash)
|
|
56
56
|
)
|
|
57
57
|
end
|
|
58
58
|
|
|
@@ -72,7 +72,7 @@ module Rito
|
|
|
72
72
|
name: hash['name'],
|
|
73
73
|
queue: hash['queue'],
|
|
74
74
|
entries: hash['entries'].to_a.freeze,
|
|
75
|
-
raw: hash
|
|
75
|
+
raw: Models.deep_freeze(hash)
|
|
76
76
|
)
|
|
77
77
|
end
|
|
78
78
|
end
|
data/lib/rito/models/match.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Rito
|
|
|
11
11
|
new(
|
|
12
12
|
metadata: MatchMetadata.from_api(hash['metadata']),
|
|
13
13
|
info: MatchInfo.from_api(hash['info']),
|
|
14
|
-
raw: hash
|
|
14
|
+
raw: Models.deep_freeze(hash)
|
|
15
15
|
)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -23,7 +23,7 @@ module Rito
|
|
|
23
23
|
new(
|
|
24
24
|
match_id: hash['matchId'],
|
|
25
25
|
participants: hash['participants'],
|
|
26
|
-
raw: hash
|
|
26
|
+
raw: Models.deep_freeze(hash)
|
|
27
27
|
)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -48,9 +48,9 @@ module Rito
|
|
|
48
48
|
game_type: hash['gameType'],
|
|
49
49
|
game_version: hash['gameVersion'],
|
|
50
50
|
map_id: hash['mapId'],
|
|
51
|
-
participants: hash['participants']&.map { |p| MatchParticipant.from_api(p) },
|
|
51
|
+
participants: hash['participants']&.map { |p| MatchParticipant.from_api(p) }&.freeze,
|
|
52
52
|
queue_id: hash['queueId'],
|
|
53
|
-
raw: hash
|
|
53
|
+
raw: Models.deep_freeze(hash)
|
|
54
54
|
)
|
|
55
55
|
end
|
|
56
56
|
end
|
|
@@ -67,7 +67,7 @@ module Rito
|
|
|
67
67
|
summoner_name: hash['summonerName'],
|
|
68
68
|
team_id: hash['teamId'],
|
|
69
69
|
win: hash['win'],
|
|
70
|
-
raw: hash
|
|
70
|
+
raw: Models.deep_freeze(hash)
|
|
71
71
|
)
|
|
72
72
|
end
|
|
73
73
|
end
|
|
@@ -77,7 +77,7 @@ module Rito
|
|
|
77
77
|
new(
|
|
78
78
|
total: hash['total'],
|
|
79
79
|
match_file_urls: hash['matchFileURLs'],
|
|
80
|
-
raw: hash
|
|
80
|
+
raw: Models.deep_freeze(hash)
|
|
81
81
|
)
|
|
82
82
|
end
|
|
83
83
|
end
|
data/lib/rito/models/summoner.rb
CHANGED
data/lib/rito/models.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rito
|
|
4
|
+
module Models
|
|
5
|
+
def self.deep_freeze(value)
|
|
6
|
+
case value
|
|
7
|
+
when Hash
|
|
8
|
+
value.each do |key, item|
|
|
9
|
+
deep_freeze(key)
|
|
10
|
+
deep_freeze(item)
|
|
11
|
+
end
|
|
12
|
+
when Array
|
|
13
|
+
value.each { |item| deep_freeze(item) }
|
|
14
|
+
end
|
|
15
|
+
value.freeze
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -15,8 +15,21 @@ module Rito
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def acquire!(key:, region:, bucket:)
|
|
18
|
-
app_bucket(key, region)
|
|
19
|
-
method_bucket(key, region, bucket)
|
|
18
|
+
app = app_bucket(key, region)
|
|
19
|
+
method = method_bucket(key, region, bucket)
|
|
20
|
+
loop do
|
|
21
|
+
delay = @mutex.synchronize do
|
|
22
|
+
[app.delay, method.delay].max.tap do |wait|
|
|
23
|
+
if wait.zero?
|
|
24
|
+
app.consume!
|
|
25
|
+
method.consume!
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
return if delay.zero?
|
|
30
|
+
|
|
31
|
+
@sleeper.call(delay + 0.001)
|
|
32
|
+
end
|
|
20
33
|
end
|
|
21
34
|
|
|
22
35
|
def observe!(response, key:, region:, bucket:)
|
|
@@ -26,19 +39,15 @@ module Rito
|
|
|
26
39
|
app = app_bucket(key, region)
|
|
27
40
|
method = method_bucket(key, region, bucket)
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
when :method, :service
|
|
39
|
-
method.limit_block!(retry_after)
|
|
40
|
-
else
|
|
41
|
-
method.limit_block!(retry_after)
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
app.apply!(limits: HeaderParser.app_limits(headers), counts: HeaderParser.app_counts(headers),
|
|
44
|
+
limited: status == 429)
|
|
45
|
+
method.apply!(limits: HeaderParser.method_limits(headers), counts: HeaderParser.method_counts(headers),
|
|
46
|
+
limited: status == 429)
|
|
47
|
+
if status == 429
|
|
48
|
+
target = HeaderParser.limit_type(headers) == :application ? app : method
|
|
49
|
+
target.limit_block!(HeaderParser.retry_after(headers))
|
|
50
|
+
end
|
|
42
51
|
end
|
|
43
52
|
end
|
|
44
53
|
|
|
@@ -18,7 +18,7 @@ module Rito
|
|
|
18
18
|
@limited_streak = 0
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def apply!(limits:, counts:)
|
|
21
|
+
def apply!(limits:, counts:, limited: false)
|
|
22
22
|
@mutex.synchronize do
|
|
23
23
|
now = @clock.call
|
|
24
24
|
if limits.any?
|
|
@@ -28,12 +28,12 @@ module Rito
|
|
|
28
28
|
{
|
|
29
29
|
limit: limit,
|
|
30
30
|
window: window,
|
|
31
|
-
reset_at: now + window,
|
|
32
|
-
count: counts_by_window.fetch(window)
|
|
31
|
+
reset_at: previous&.fetch(:reset_at) || now + window,
|
|
32
|
+
count: [counts_by_window.fetch(window, 0), previous&.fetch(:count) || 0].max
|
|
33
33
|
}
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
-
@limited_streak = 0
|
|
36
|
+
@limited_streak = 0 unless limited
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -43,33 +43,32 @@ module Rito
|
|
|
43
43
|
def limit_block!(retry_after)
|
|
44
44
|
@mutex.synchronize do
|
|
45
45
|
now = @clock.call
|
|
46
|
-
@limited_streak
|
|
47
|
-
delay = retry_after || [@limited_streak * DEFAULT_BACKOFF, MAX_BACKOFF].min
|
|
48
|
-
delay += rand * JITTER
|
|
46
|
+
@limited_streak = [@limited_streak + 1, 6].min
|
|
47
|
+
delay = retry_after || [2**(@limited_streak - 1) * DEFAULT_BACKOFF, MAX_BACKOFF].min
|
|
48
|
+
delay += rand * JITTER unless retry_after
|
|
49
49
|
until_time = now + delay
|
|
50
50
|
@blocked_until = until_time if @blocked_until.nil? || @blocked_until < until_time
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
def delay
|
|
55
|
+
@mutex.synchronize { delay_unlocked }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def consume!
|
|
59
|
+
@mutex.synchronize { @windows.each { |window| window[:count] += 1 } }
|
|
60
|
+
end
|
|
61
|
+
|
|
54
62
|
def wait_until_allowed!
|
|
55
63
|
loop do
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if @blocked_until && @blocked_until > now
|
|
60
|
-
delay = @blocked_until - now
|
|
61
|
-
else
|
|
62
|
-
@windows.reject! { |w| w[:reset_at] <= now }
|
|
63
|
-
violating = @windows.find { |w| w[:count] >= w[:limit] }
|
|
64
|
-
if violating
|
|
65
|
-
delay = violating[:reset_at] - now
|
|
66
|
-
else
|
|
67
|
-
@windows.each { |w| w[:count] += 1 }
|
|
68
|
-
return
|
|
69
|
-
end
|
|
64
|
+
wait = @mutex.synchronize do
|
|
65
|
+
delay_unlocked.tap do |value|
|
|
66
|
+
@windows.each { |window| window[:count] += 1 } if value.zero?
|
|
70
67
|
end
|
|
71
68
|
end
|
|
72
|
-
|
|
69
|
+
return if wait.zero?
|
|
70
|
+
|
|
71
|
+
@sleeper.call(wait + 0.001)
|
|
73
72
|
end
|
|
74
73
|
end
|
|
75
74
|
|
|
@@ -78,6 +77,22 @@ module Rito
|
|
|
78
77
|
@blocked_until && @blocked_until > @clock.call
|
|
79
78
|
end
|
|
80
79
|
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def delay_unlocked
|
|
84
|
+
now = @clock.call
|
|
85
|
+
@windows.each do |window|
|
|
86
|
+
next if window[:reset_at] > now
|
|
87
|
+
|
|
88
|
+
window[:count] = 0
|
|
89
|
+
window[:reset_at] = now + window[:window]
|
|
90
|
+
end
|
|
91
|
+
delays = @windows.filter_map do |window|
|
|
92
|
+
window[:reset_at] - now if window[:count] >= window[:limit]
|
|
93
|
+
end
|
|
94
|
+
[0, (@blocked_until || now) - now, *delays].max
|
|
95
|
+
end
|
|
81
96
|
end
|
|
82
97
|
end
|
|
83
98
|
end
|
|
@@ -12,7 +12,9 @@ module Rito
|
|
|
12
12
|
limit, window = part.strip.split(':', 2)
|
|
13
13
|
next if limit.nil? || window.nil?
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
limit = Integer(limit)
|
|
16
|
+
window = Integer(window)
|
|
17
|
+
[limit, window] if limit >= 0 && window.positive?
|
|
16
18
|
rescue ArgumentError
|
|
17
19
|
next
|
|
18
20
|
end
|
|
@@ -24,23 +24,47 @@ module Rito
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
for i = 3, #KEYS do
|
|
27
|
-
local
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
redis.call('DECR', KEYS[i])
|
|
37
|
-
return 0
|
|
38
|
-
end
|
|
27
|
+
local limit = tonumber(ARGV[(i - 3) * 2 + 1])
|
|
28
|
+
if tonumber(redis.call('GET', KEYS[i]) or '0') >= limit then
|
|
29
|
+
return 0
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
for i = 3, #KEYS do
|
|
33
|
+
redis.call('INCR', KEYS[i])
|
|
34
|
+
if redis.call('PTTL', KEYS[i]) < 0 then
|
|
35
|
+
redis.call('EXPIRE', KEYS[i], ARGV[(i - 3) * 2 + 2])
|
|
39
36
|
end
|
|
40
37
|
end
|
|
41
38
|
return 1
|
|
42
39
|
LUA
|
|
43
40
|
|
|
41
|
+
BLOCK_SCRIPT = <<~LUA
|
|
42
|
+
local streak = math.min(redis.call('INCR', KEYS[2]), 6)
|
|
43
|
+
redis.call('EXPIRE', KEYS[2], 600)
|
|
44
|
+
local delay = tonumber(ARGV[1])
|
|
45
|
+
if delay < 0 then
|
|
46
|
+
delay = math.min(2 ^ (streak - 1), 32) * 1000 + tonumber(ARGV[2])
|
|
47
|
+
end
|
|
48
|
+
if delay > 0 and redis.call('PTTL', KEYS[1]) < delay then
|
|
49
|
+
redis.call('SET', KEYS[1], '1', 'PX', delay)
|
|
50
|
+
end
|
|
51
|
+
LUA
|
|
52
|
+
|
|
53
|
+
SYNC_COUNT_SCRIPT = <<~LUA
|
|
54
|
+
local current = redis.call('GET', KEYS[1])
|
|
55
|
+
local count = tonumber(ARGV[1])
|
|
56
|
+
if not current then
|
|
57
|
+
redis.call('SET', KEYS[1], count, 'EX', ARGV[2])
|
|
58
|
+
elseif count > tonumber(current) then
|
|
59
|
+
local ttl = redis.call('PTTL', KEYS[1])
|
|
60
|
+
if ttl > 0 then
|
|
61
|
+
redis.call('SET', KEYS[1], count, 'PX', ttl)
|
|
62
|
+
else
|
|
63
|
+
redis.call('SET', KEYS[1], count, 'EX', ARGV[2])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
LUA
|
|
67
|
+
|
|
44
68
|
def initialize(redis:, sleeper: Kernel.method(:sleep))
|
|
45
69
|
raise DependencyMissing, 'Add the `redis` gem to your Gemfile to use RedisLimiter' unless defined?(::Redis)
|
|
46
70
|
|
|
@@ -61,11 +85,12 @@ module Rito
|
|
|
61
85
|
status = response.status
|
|
62
86
|
prefix = prefix_for(key, region)
|
|
63
87
|
|
|
88
|
+
sync_limits!(prefix, bucket, headers)
|
|
89
|
+
sync_counts!(prefix, bucket, headers)
|
|
64
90
|
if status == 429
|
|
65
91
|
block!(prefix, bucket, headers)
|
|
66
92
|
else
|
|
67
|
-
|
|
68
|
-
sync_counts!(prefix, bucket, headers)
|
|
93
|
+
@redis.del("#{prefix}:block:streak", "#{prefix}:m:#{bucket}:block:streak")
|
|
69
94
|
end
|
|
70
95
|
end
|
|
71
96
|
|
|
@@ -99,15 +124,14 @@ module Rito
|
|
|
99
124
|
end
|
|
100
125
|
|
|
101
126
|
def block!(prefix, bucket, headers)
|
|
102
|
-
retry_after = HeaderParser.retry_after(headers)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
end
|
|
127
|
+
retry_after = HeaderParser.retry_after(headers)
|
|
128
|
+
block_key = if HeaderParser.limit_type(headers) == :application
|
|
129
|
+
"#{prefix}:block"
|
|
130
|
+
else
|
|
131
|
+
"#{prefix}:m:#{bucket}:block"
|
|
132
|
+
end
|
|
133
|
+
@redis.eval(BLOCK_SCRIPT, keys: [block_key, "#{block_key}:streak"],
|
|
134
|
+
argv: [retry_after ? retry_after * 1000 : -1, rand(500)])
|
|
111
135
|
end
|
|
112
136
|
|
|
113
137
|
def sync_limits!(prefix, bucket, headers)
|
|
@@ -123,13 +147,13 @@ module Rito
|
|
|
123
147
|
end
|
|
124
148
|
|
|
125
149
|
def sync_counts!(prefix, bucket, headers)
|
|
126
|
-
sync_scope_counts!(
|
|
127
|
-
sync_scope_counts!(
|
|
150
|
+
sync_scope_counts!(HeaderParser.app_counts(headers), "#{prefix}:app")
|
|
151
|
+
sync_scope_counts!(HeaderParser.method_counts(headers), "#{prefix}:m:#{bucket}")
|
|
128
152
|
end
|
|
129
153
|
|
|
130
|
-
def sync_scope_counts!(
|
|
154
|
+
def sync_scope_counts!(counts, counter_prefix)
|
|
131
155
|
counts.each do |count, window|
|
|
132
|
-
@redis.
|
|
156
|
+
@redis.eval(SYNC_COUNT_SCRIPT, keys: ["#{counter_prefix}:#{window}"], argv: [count, window])
|
|
133
157
|
end
|
|
134
158
|
end
|
|
135
159
|
|
data/lib/rito/routing.rb
CHANGED
|
@@ -5,7 +5,8 @@ module Rito
|
|
|
5
5
|
PLATFORMS = %w[
|
|
6
6
|
br1 eun1 euw1 jp1 kr la1 la2 me1 na1 oc1 pbe1 ru sg2 tr1 tw2 vn2
|
|
7
7
|
].freeze
|
|
8
|
-
|
|
8
|
+
# esports/esportseu are accepted by tft-match-v1; apac by lor-match-v1.
|
|
9
|
+
REGIONALS = %w[americas apac asia esports esportseu europe sea].freeze
|
|
9
10
|
|
|
10
11
|
CLUSTERS = {
|
|
11
12
|
'americas' => %w[na1 br1 la1 la2],
|
|
@@ -70,11 +71,11 @@ module Rito
|
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
def regional_for_account(value)
|
|
73
|
-
v =
|
|
74
|
+
v = resolve(:regional, value)
|
|
75
|
+
v = 'asia' if v == 'sea'
|
|
74
76
|
return v if ACCOUNT_REGIONALS.include?(v)
|
|
75
|
-
return 'asia' if v == 'sea'
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
raise ArgumentError, "#{value.inspect} is not usable as an account routing value"
|
|
78
79
|
end
|
|
79
80
|
end
|
|
80
81
|
end
|
data/lib/rito/rso/models.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Rito
|
|
|
15
15
|
scope: hash['scope'],
|
|
16
16
|
expires_in: hash['expires_in'],
|
|
17
17
|
sub_sid: hash['sub_sid'],
|
|
18
|
-
raw: hash
|
|
18
|
+
raw: Models.deep_freeze(hash)
|
|
19
19
|
)
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -30,7 +30,7 @@ module Rito
|
|
|
30
30
|
|
|
31
31
|
Userinfo = Data.define(:sub, :cpid, :raw) do
|
|
32
32
|
def self.from_api(hash)
|
|
33
|
-
new(sub: hash['sub'], cpid: hash['cpid'], raw: hash
|
|
33
|
+
new(sub: hash['sub'], cpid: hash['cpid'], raw: Models.deep_freeze(hash))
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
data/lib/rito/version.rb
CHANGED
data/lib/rito.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# stdlib
|
|
4
4
|
require 'json'
|
|
5
|
+
require 'English'
|
|
5
6
|
require 'uri'
|
|
6
7
|
require 'erb/util'
|
|
7
8
|
|
|
@@ -25,6 +26,7 @@ require_relative 'rito/middleware/riot_errors'
|
|
|
25
26
|
require_relative 'rito/connection'
|
|
26
27
|
require_relative 'rito/instrumentation'
|
|
27
28
|
require_relative 'rito/client'
|
|
29
|
+
require_relative 'rito/models'
|
|
28
30
|
require_relative 'rito/rso'
|
|
29
31
|
require_relative 'rito/models/account'
|
|
30
32
|
require_relative 'rito/models/summoner'
|
|
@@ -46,6 +48,7 @@ require_relative 'rito/endpoints/lol/tournament_v5'
|
|
|
46
48
|
require_relative 'rito/endpoints/tft'
|
|
47
49
|
require_relative 'rito/endpoints/valorant'
|
|
48
50
|
require_relative 'rito/endpoints/lor'
|
|
51
|
+
require_relative 'rito/endpoints/riftbound'
|
|
49
52
|
require_relative 'rito/version'
|
|
50
53
|
|
|
51
54
|
module Rito
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rito
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kodbilenadam
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -65,6 +65,7 @@ files:
|
|
|
65
65
|
- lib/rito/endpoints/lol/summoner_v4.rb
|
|
66
66
|
- lib/rito/endpoints/lol/tournament_v5.rb
|
|
67
67
|
- lib/rito/endpoints/lor.rb
|
|
68
|
+
- lib/rito/endpoints/riftbound.rb
|
|
68
69
|
- lib/rito/endpoints/tft.rb
|
|
69
70
|
- lib/rito/endpoints/valorant.rb
|
|
70
71
|
- lib/rito/errors.rb
|
|
@@ -73,6 +74,7 @@ files:
|
|
|
73
74
|
- lib/rito/middleware/rate_limit_observe.rb
|
|
74
75
|
- lib/rito/middleware/rate_limit_throttle.rb
|
|
75
76
|
- lib/rito/middleware/riot_errors.rb
|
|
77
|
+
- lib/rito/models.rb
|
|
76
78
|
- lib/rito/models/account.rb
|
|
77
79
|
- lib/rito/models/lol.rb
|
|
78
80
|
- lib/rito/models/match.rb
|