twitchrb 1.11.0 → 2.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +80 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +360 -45
  6. data/lib/twitch/client.rb +109 -20
  7. data/lib/twitch/collection.rb +54 -8
  8. data/lib/twitch/eventsub_webhook.rb +140 -0
  9. data/lib/twitch/oauth.rb +49 -12
  10. data/lib/twitch/object.rb +28 -0
  11. data/lib/twitch/objects/ad_schedule.rb +4 -0
  12. data/lib/twitch/objects/analytics_report.rb +4 -0
  13. data/lib/twitch/objects/bits_leaderboard_entry.rb +4 -0
  14. data/lib/twitch/objects/charity_donation.rb +4 -0
  15. data/lib/twitch/objects/chat_settings.rb +4 -0
  16. data/lib/twitch/objects/cheermote.rb +4 -0
  17. data/lib/twitch/objects/content_classification_label.rb +4 -0
  18. data/lib/twitch/objects/drops_entitlement.rb +4 -0
  19. data/lib/twitch/objects/drops_entitlement_update.rb +4 -0
  20. data/lib/twitch/objects/extension.rb +4 -0
  21. data/lib/twitch/objects/extension_bits_product.rb +4 -0
  22. data/lib/twitch/objects/extension_configuration_segment.rb +4 -0
  23. data/lib/twitch/objects/extension_live_channel.rb +4 -0
  24. data/lib/twitch/objects/extension_secret.rb +4 -0
  25. data/lib/twitch/objects/extension_transaction.rb +4 -0
  26. data/lib/twitch/objects/guest_star_invite.rb +4 -0
  27. data/lib/twitch/objects/guest_star_session.rb +4 -0
  28. data/lib/twitch/objects/guest_star_settings.rb +4 -0
  29. data/lib/twitch/objects/shield_mode_status.rb +4 -0
  30. data/lib/twitch/objects/{tag.rb → team.rb} +1 -1
  31. data/lib/twitch/objects/user_active_extensions.rb +4 -0
  32. data/lib/twitch/objects/user_extension.rb +4 -0
  33. data/lib/twitch/resource.rb +29 -0
  34. data/lib/twitch/resources/ads.rb +18 -0
  35. data/lib/twitch/resources/analytics.rb +19 -0
  36. data/lib/twitch/resources/announcements.rb +1 -1
  37. data/lib/twitch/resources/automod.rb +7 -7
  38. data/lib/twitch/resources/badges.rb +2 -2
  39. data/lib/twitch/resources/banned_users.rb +3 -3
  40. data/lib/twitch/resources/bits.rb +16 -0
  41. data/lib/twitch/resources/blocked_terms.rb +3 -3
  42. data/lib/twitch/resources/channels.rb +4 -4
  43. data/lib/twitch/resources/charity_campaigns.rb +8 -0
  44. data/lib/twitch/resources/chat_settings.rb +20 -0
  45. data/lib/twitch/resources/chatters.rb +1 -1
  46. data/lib/twitch/resources/clips.rb +3 -9
  47. data/lib/twitch/resources/content_classification_labels.rb +8 -0
  48. data/lib/twitch/resources/custom_power_ups.rb +1 -1
  49. data/lib/twitch/resources/custom_reward_redemptions.rb +3 -4
  50. data/lib/twitch/resources/custom_rewards.rb +5 -5
  51. data/lib/twitch/resources/drops_entitlements.rb +17 -0
  52. data/lib/twitch/resources/emotes.rb +3 -3
  53. data/lib/twitch/resources/eventsub_conduits.rb +14 -4
  54. data/lib/twitch/resources/eventsub_subscriptions.rb +1 -1
  55. data/lib/twitch/resources/extensions.rb +99 -0
  56. data/lib/twitch/resources/games.rb +3 -3
  57. data/lib/twitch/resources/goals.rb +1 -1
  58. data/lib/twitch/resources/guest_star.rb +102 -0
  59. data/lib/twitch/resources/moderators.rb +3 -3
  60. data/lib/twitch/resources/pinned_chat_messages.rb +4 -11
  61. data/lib/twitch/resources/polls.rb +1 -1
  62. data/lib/twitch/resources/predictions.rb +1 -1
  63. data/lib/twitch/resources/raids.rb +2 -3
  64. data/lib/twitch/resources/search.rb +2 -2
  65. data/lib/twitch/resources/shield_mode.rb +18 -0
  66. data/lib/twitch/resources/shoutouts.rb +1 -1
  67. data/lib/twitch/resources/stream_markers.rb +1 -1
  68. data/lib/twitch/resources/stream_schedule.rb +4 -5
  69. data/lib/twitch/resources/streams.rb +2 -2
  70. data/lib/twitch/resources/subscriptions.rb +13 -2
  71. data/lib/twitch/resources/suspicious_users.rb +1 -1
  72. data/lib/twitch/resources/teams.rb +20 -0
  73. data/lib/twitch/resources/unban_requests.rb +2 -2
  74. data/lib/twitch/resources/users.rb +30 -34
  75. data/lib/twitch/resources/videos.rb +2 -2
  76. data/lib/twitch/resources/vips.rb +2 -2
  77. data/lib/twitch/resources/warnings.rb +2 -2
  78. data/lib/twitch/resources/whispers.rb +1 -1
  79. data/lib/twitch/version.rb +1 -1
  80. data/lib/twitch.rb +33 -9
  81. data/twitchrb.gemspec +4 -4
  82. metadata +40 -16
  83. data/lib/twitch/objects/banned_event.rb +0 -4
  84. data/lib/twitch/objects/followed_user.rb +0 -4
  85. data/lib/twitch/objects/hype_train_event.rb +0 -4
  86. data/lib/twitch/objects/moderator_event.rb +0 -4
  87. data/lib/twitch/resources/banned_events.rb +0 -8
  88. data/lib/twitch/resources/hype_train_events.rb +0 -10
  89. data/lib/twitch/resources/moderator_events.rb +0 -9
  90. data/lib/twitch/resources/tags.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a4de14d63208a337630d3530f24d90679645c2b4ad02d4f31beb96f687a6ff1
4
- data.tar.gz: 2e49411b20156a55d93fa60e1dba87bf25121c228f601e10f6861e01aa9b0094
3
+ metadata.gz: 0641bf783f69ad1e15ce425ab0a04932376a98403eb996cae29e44cb04509869
4
+ data.tar.gz: c50b205b48bc9671724f848e81329ff980873f598fb278da24c344c753ff1768
5
5
  SHA512:
6
- metadata.gz: 81bcbe6402c4cdf5f3cc90ac181aec22d8d46990053846f1bce3b92aead9f71462857c6b92e1b5dcbd4c34d1e635e057ce86516c856db1944252d01dc1e90b56
7
- data.tar.gz: cfff782da54f1ffde515c8911febf8d251c1df677d0ba481399cfc86d2bd95c9885e0d5f6682aadbb96be98dc4cecf0ceda8aa72b073abdda842b4dc8e7ce120
6
+ metadata.gz: f5f5d1850b6595dce58b11f9b2cd4756f3ae00043ce22c6643d20fd133232270abf6d24360b4d2a2ef7c3220b1b1cf888745ac418ce7582c3cf220c022bfca4a
7
+ data.tar.gz: 32e637838de8ea70e207655e3d870a7f7f80a021f4fb3afe4211f74293f6fcfce54e525d7f79ea859908bc2cf7f26277d3109812132610e7b48b4fe0659c0322
data/.rubocop.yml CHANGED
@@ -4,7 +4,7 @@ inherit_gem: { rubocop-rails-omakase: rubocop.yml }
4
4
  AllCops:
5
5
  Exclude:
6
6
  - 'test/**/*'
7
- TargetRubyVersion: 4.0
7
+ TargetRubyVersion: 3.3
8
8
 
9
9
  # Overwrite or add rules to create your own house style
10
10
  #
data/CHANGELOG.md CHANGED
@@ -4,6 +4,86 @@ All notable changes to `twitchrb` are documented in this file.
4
4
 
5
5
  Published release notes were sourced from GitHub releases where available. Older tag-only versions and the current unreleased work were reconstructed from local git history.
6
6
 
7
+ ## [2.0.0] - 2026-09-23
8
+
9
+ This release adds every remaining Helix endpoint, automatic pagination, automatic token refresh, and EventSub
10
+ webhook verification, and fixes many requests that didn't match the Twitch API reference. It contains breaking
11
+ changes, listed below.
12
+
13
+ ### Upgrading from 1.x
14
+
15
+ - **Ruby 3.3 or newer is required.**
16
+ - **Methods for endpoints Twitch has shut down were removed:**
17
+ - `hype_train_events.list` → use `hype_train_status.retrieve(broadcaster_id:)`.
18
+ - `users.follows` and `users.following?` → use `channels.followers(broadcaster_id:, user_id:)` to check whether a
19
+ user follows a channel, or `channels.followed(user_id:)` to list the channels a user follows.
20
+ - `tags.list`, `tags.stream`, and `tags.replace` → channel tags are returned by `channels.retrieve` and set with
21
+ `channels.update(broadcaster_id:, tags: [...])`.
22
+ - `banned_events.list` and `moderator_events.list` → no API replacement; use the `channel.ban`, `channel.unban`,
23
+ and `channel.moderate` EventSub subscriptions.
24
+ - **`oauth.create`, `oauth.refresh`, and `oauth.device` raise errors instead of returning `false`.** Replace checks
25
+ like `if token = oauth.refresh(...)` with a `rescue`:
26
+
27
+ ```ruby
28
+ begin
29
+ token = oauth.refresh(refresh_token: refresh_token)
30
+ rescue Twitch::Error => e
31
+ e.twitch_error_message #=> "Invalid refresh token"
32
+ end
33
+ ```
34
+
35
+ `oauth.validate` and `oauth.revoke` still return `false`.
36
+ - **Missing required arguments raise `ArgumentError`** instead of `RuntimeError`. Update any `rescue RuntimeError`.
37
+ - **`to_h` on response objects now converts nested objects to hashes too.** If you called methods on nested values
38
+ from `to_h` (e.g. `object.to_h[:current].id`), use hash access instead (`object.to_h[:current][:id]`), or call the
39
+ method on the object itself (`object.current.id`).
40
+
41
+ ### Changed
42
+ - Missing required arguments (e.g. calling `clips.list` without `broadcaster_id`, `game_id` or `id`) now raise `ArgumentError` instead of `RuntimeError`.
43
+ - **Breaking:** `oauth.create`, `oauth.refresh`, and `oauth.device` now raise errors (e.g. `Twitch::Errors::BadRequestError`, with Twitch's message in `twitch_error_message`) instead of returning `false`. `oauth.validate` and `oauth.revoke` still return `false`.
44
+ - The minimum supported Ruby version is now 3.3 (previously declared as 2.3, though the gem already required 3.1+). This matches the versions tested in CI.
45
+
46
+ ### Added
47
+ - `subscriptions.subscribed?(broadcaster_id:, user_id:)`, which returns `true` or `false` instead of raising when the user isn't subscribed.
48
+ - Automatic token refresh: pass `refresh_token:` (and `client_secret:`, unless your app is a public client) to `Twitch::Client.new`, and requests that fail because the access token expired are retried once with a refreshed token. `on_token_refresh:` is called with the new token so you can store it, and `client.refresh_access_token!` refreshes manually.
49
+ - Automatic pagination: collections from paginated endpoints now have `next_page?`, `next_page`, `each_page`, and `auto_paginate`, which lazily iterates over every item across pages (e.g. `auto_paginate.first(250)` only fetches the pages it needs).
50
+ - `Twitch::EventsubWebhook` to verify and parse EventSub webhook requests. It checks the HMAC signature in constant time, rejects messages older than 10 minutes, and gives access to the message type, challenge, subscription, and event.
51
+ - `oauth.exchange_code(code:, redirect_uri:)` for the authorization code grant flow, and `oauth.device_token(device_code:, scopes:)` to finish the device code grant flow. Previously neither flow could be completed.
52
+ - `oauth.create` sends extra keyword arguments (such as `code` and `redirect_uri`) with the request, and `scope`/`scopes` accept arrays.
53
+ - `Twitch::OAuth.new` no longer requires a `client_secret`, for public clients using the device code flow.
54
+ - `ads.schedule` and `ads.snooze` (Get Ad Schedule, Snooze Next Ad).
55
+ - `analytics.extensions` and `analytics.games`.
56
+ - `bits.leaderboard` and `bits.cheermotes`.
57
+ - `charity_campaigns.donations`.
58
+ - `chat_settings.retrieve` and `chat_settings.update`.
59
+ - `shield_mode.retrieve` and `shield_mode.update`.
60
+ - `content_classification_labels.list`.
61
+ - `teams.retrieve` and `teams.channel`.
62
+ - `drops_entitlements.list` and `drops_entitlements.update`.
63
+ - `users.extensions`, `users.active_extensions`, and `users.update_extensions`.
64
+ - An `extensions` resource covering the Extensions API: `retrieve`, `released`, `live_channels`, `configuration`, `set_configuration`, `set_required_configuration`, `send_pubsub_message`, `send_chat_message`, `secrets`, `create_secret`, `bits_products`, `update_bits_product`, and `transactions`.
65
+ - A `guest_star` resource covering the Guest Star beta API.
66
+
67
+ ### Removed
68
+ - Removed methods for endpoints Twitch has shut down, which could only return errors:
69
+ - `hype_train_events.list`, which forwarded to `hype_train_status.retrieve` with a deprecation warning since 1.10.0.
70
+ - `banned_events.list` and `moderator_events.list` (Get Banned Events / Get Moderator Events).
71
+ - `users.follows` and `users.following?` (`GET /users/follows`). Use `channels.followers` or `channels.followed` instead.
72
+ - `tags.list`, `tags.stream`, and `tags.replace` (the old Twitch-defined stream tags). Channel tags are now read with `channels.retrieve` and set with `channels.update(tags: [...])`.
73
+ - Removed the now-unused `Twitch::BannedEvent`, `Twitch::ModeratorEvent`, `Twitch::FollowedUser`, `Twitch::HypeTrainEvent`, and `Twitch::Tag` classes.
74
+
75
+ ### Fixed
76
+ - Fixed multi-value query params (e.g. `users.retrieve(ids:)`, `games.retrieve(names:)`, `streams.list(user_id: [...])`, `clips.downloads(clip_ids:)`) being sent as `id[]=1&id[]=2`. They are now sent as repeated keys (`id=1&id=2`), which is the format Helix expects.
77
+ - Fixed write endpoints sending query string parameters in the JSON body instead of the query string, contrary to the Twitch API reference. Affects `channels.update`, `custom_rewards.create`/`update`, `automod.check_status`/`check_status_multiple`/`update_settings`, `banned_users.create`, `blocked_terms.create`, `unban_requests.resolve`, `moderators.create`, `vips.create`, `raids.create`, `whispers.create`, `stream_schedule.update`/`create_segment`/`update_segment`, `users.update`, and `users.block_user` (whose `source_context` and `reason` options were being sent in the body).
78
+ - Fixed `clips.list` and `videos.list` rejecting calls that only pass `id`, which both endpoints accept.
79
+ - Fixed `eventsub_conduits.update_shards` discarding the `errors` Twitch returns for shards that failed to update. They are now available as `result.errors`, and `Collection#errors` is available for any endpoint that reports partial failures.
80
+ - `eventsub_conduits.update_shards` now sends lists of more than 100 shards in batches of 100, the most Twitch accepts per request.
81
+ - Fixed `oauth.device` sending `scope` instead of `scopes`, which Twitch requires.
82
+ - Fixed `to_h` and `to_json` on response objects leaving nested objects as `OpenStruct`s, which serialized as strings like `"#<OpenStruct ...>"`. Nested objects and arrays are now converted to plain hashes, and `as_json` is defined for Rails.
83
+ - `Collection#cursor` now works for endpoints that return `pagination` as a bare string (such as Get Extension Live Channels) instead of raising.
84
+ - `banned_users.create` no longer sends `"duration": null` for permanent bans.
85
+ - Helix and OAuth requests now have default timeouts (30s read, 10s open) instead of waiting indefinitely. Configure with the `timeout:` and `open_timeout:` options on `Twitch::Client.new` and `Twitch::OAuth.new`.
86
+
7
87
  ## [1.11.0] - 2026-08-11
8
88
 
9
89
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- twitchrb (1.10.0)
4
+ twitchrb (2.0.0)
5
5
  faraday (>= 2.14.3, < 3)
6
6
  ostruct (~> 0.6.0)
7
7
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TwitchRB
2
2
 
3
- [![CI](https://github.com/deanpcmad/twitchrb/actions/workflows/ci.yml/badge.svg)](https://github.com/deanpcmad/twitchrb/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/d34ndev/twitchrb/actions/workflows/ci.yml/badge.svg)](https://github.com/d34ndev/twitchrb/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/twitchrb.svg)](https://badge.fury.io/rb/twitchrb)
5
5
  [![Downloads](https://img.shields.io/gem/dt/twitchrb.svg)](https://rubygems.org/gems/twitchrb)
6
6
 
@@ -14,6 +14,8 @@ Add this line to your application's Gemfile:
14
14
  gem "twitchrb"
15
15
  ```
16
16
 
17
+ Upgrading from 1.x? Version 2.0 has breaking changes. See [Upgrading from 1.x](CHANGELOG.md#upgrading-from-1x) in the changelog.
18
+
17
19
  ## Usage
18
20
 
19
21
  ### Set Client Details
@@ -26,17 +28,46 @@ An access token is required because the Helix API requires authentication.
26
28
  @client = Twitch::Client.new(client_id: "abc123", access_token: "xyz123")
27
29
  ```
28
30
 
31
+ Requests time out after 30 seconds (10 seconds to open the connection) by default, raising
32
+ `Faraday::TimeoutError` or `Faraday::ConnectionFailed`. Both can be changed on `Twitch::Client` and `Twitch::OAuth`:
33
+
34
+ ```ruby
35
+ @client = Twitch::Client.new(client_id: "abc123", access_token: "xyz123", timeout: 10, open_timeout: 5)
36
+ ```
37
+
38
+ #### Refreshing User Access Tokens
39
+
40
+ User access tokens expire after a few hours. Pass the `refresh_token` and your `client_secret` and the client
41
+ will refresh an expired token automatically, then retry the request. Twitch may issue a new refresh token each
42
+ time, so use `on_token_refresh` to store the new tokens:
43
+
44
+ ```ruby
45
+ @client = Twitch::Client.new(
46
+ client_id: "abc123",
47
+ client_secret: "your-client-secret", # omit for public clients
48
+ access_token: user.twitch_access_token,
49
+ refresh_token: user.twitch_refresh_token,
50
+ on_token_refresh: ->(token) {
51
+ user.update!(twitch_access_token: token.access_token, twitch_refresh_token: token.refresh_token)
52
+ }
53
+ )
54
+
55
+ # Or refresh manually
56
+ @client.refresh_access_token!
57
+ ```
58
+
59
+ The client refreshes once per expired token, even across threads. If the refresh token itself is invalid (e.g.
60
+ the user disconnected your app), the error from Twitch is raised, e.g. `Twitch::Errors::BadRequestError`.
61
+
29
62
  #### User vs. App Access Tokens
30
63
 
31
64
  Most endpoints accept a **user access token** — issued for a specific Twitch user via the
32
65
  [authorization code](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow)
33
66
  or [device code](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-code-grant-flow) flows,
34
- and required for anything that acts on behalf of a user (sending chat, managing channels, etc.).
67
+ and required for anything that acts on behalf of a user (managing channels, polls, rewards, etc.).
35
68
 
36
- Some endpoints require an **app access token** instead — issued to your application, not a user. These
37
- are the EventSub APIs (subscriptions over webhooks, conduits, and shards), plus a few others noted in
38
- each section below. App tokens are obtained with the `client_credentials` grant and need only your
39
- Client ID and Client Secret:
69
+ An **app access token** is issued to your application, not a user. App tokens are obtained with the
70
+ `client_credentials` grant and need only your Client ID and Client Secret:
40
71
 
41
72
  ```ruby
42
73
  oauth = Twitch::OAuth.new(client_id: "abc123", client_secret: "your-client-secret")
@@ -48,6 +79,25 @@ token = oauth.create(grant_type: "client_credentials")
48
79
  App tokens expire (typically after ~60 days). Use `oauth.validate(token: ...)` to check the remaining
49
80
  lifetime, and `oauth.create(grant_type: "client_credentials")` to mint a fresh one when needed.
50
81
 
82
+ **These require an app access token:**
83
+
84
+ - EventSub over webhooks or conduits: `eventsub_subscriptions` and `eventsub_conduits` (including shards).
85
+ EventSub over WebSockets requires a user access token instead.
86
+ - `users.authorization`
87
+ - `extensions.transactions`, and `extensions.bits_products`/`update_bits_product` (the token's Client ID must be the extension's)
88
+
89
+ **These also accept an app access token**, as long as the user in the request (e.g. `sender_id` or
90
+ `moderator_id`) has previously authorized your app with the required scope through one of the user token flows.
91
+ This is how chatbots can act without storing each user's token. Chat endpoints need the bot account to have
92
+ granted `user:bot`, and the broadcaster to have granted `channel:bot` or made the bot a moderator — see
93
+ [Chatbots](https://dev.twitch.tv/docs/chat/authenticating/) for details.
94
+
95
+ - Chat: `chat_messages.create`, `chat_messages.delete`, `announcements.create`, `shoutouts.create`,
96
+ `pinned_chat_messages` (all methods), `chatters.list`, `chat_settings.update`
97
+ - Moderation: `automod` (all methods), `banned_users` (all methods), `blocked_terms` (all methods),
98
+ `moderators.channels`, `shield_mode` (all methods), `warnings.create`, `suspicious_users` (all methods)
99
+ - Ads: `channels.commercial`, `ads.schedule`, `ads.snooze`
100
+
51
101
  ### Resources
52
102
 
53
103
  The gem maps as closely as we can to the Twitch API so you can easily convert API examples to gem code.
@@ -85,11 +135,38 @@ results.last
85
135
  results.cursor
86
136
  #=> "abc123"
87
137
 
88
- # Retrieve the next page
138
+ # Retrieve the next page manually
89
139
  @client.clips.list(broadcaster_id: 123, after: results.cursor)
90
140
  #=> Twitch::Collection
91
141
  ```
92
142
 
143
+ #### Automatic Pagination
144
+
145
+ Collections from paginated endpoints can fetch their following pages for you. Each extra page is a separate
146
+ API request (and counts towards your rate limit), so use `first:` to request up to 100 items per page.
147
+
148
+ ```ruby
149
+ followers = @client.channels.followers(broadcaster_id: 123, first: 100)
150
+
151
+ # Fetch the next page, or nil on the last page
152
+ followers.next_page? #=> true
153
+ followers.next_page #=> Twitch::Collection
154
+
155
+ # Iterate over every item across all pages. Pages are fetched lazily, only as needed.
156
+ followers.auto_paginate.each { |follower| puts follower.user_name }
157
+
158
+ # Stop after 250 items, without fetching any more pages than needed
159
+ followers.auto_paginate.first(250)
160
+
161
+ # Get every item as an array
162
+ followers.auto_paginate.to_a
163
+
164
+ # Iterate page by page
165
+ followers.each_page do |page|
166
+ puts "#{page.data.size} followers on this page"
167
+ end
168
+ ```
169
+
93
170
  ### Rate Limiting
94
171
 
95
172
  The Twitch API has rate limits to ensure fair usage. TwitchRB automatically tracks rate limit information from API responses and can warn you when approaching limits.
@@ -168,33 +245,59 @@ The `rate_limiter` object has several useful methods:
168
245
 
169
246
  ### OAuth
170
247
 
171
- This library includes the ability to create, refresh and revoke OAuth tokens.
248
+ This library includes the ability to create, refresh, validate and revoke OAuth tokens.
249
+
250
+ Failed token requests raise the same errors as API requests (e.g. `Twitch::Errors::BadRequestError`),
251
+ with Twitch's message available as `error.twitch_error_message`.
172
252
 
173
253
  ```ruby
174
254
  # Firstly, set the client details
255
+ # client_secret can be omitted for public clients using the device code flow
175
256
  @oauth = Twitch::OAuth.new(client_id: "", client_secret: "")
176
257
 
177
- # Create a Token
178
- # grant_type can be either "authorization_code" or "client_credentials"
179
- # scope is a space-delimited list of scopes. This is optional depending on the grant_type
180
- @oauth.create(grant_type: "", scope: "")
258
+ # Create an app access token (client credentials grant flow)
259
+ @oauth.create(grant_type: "client_credentials")
260
+
261
+ # Exchange the code from the authorization code grant flow for a user access token
262
+ # https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow
263
+ token = @oauth.exchange_code(code: params[:code], redirect_uri: "http://localhost:3000/callback")
264
+ token.access_token
265
+ token.refresh_token
181
266
 
182
267
  # Refresh a Token
183
268
  @oauth.refresh(refresh_token: "")
184
269
 
185
- # Device Code Grant Flow
186
- # scopes is required and is a space-delimited list of scopes
187
- # https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-code-grant-flow
188
- @oauth.device(scopes: "bits:read channel:read:subscriptions")
189
-
190
270
  # Validate an Access Token
191
271
  # Returns false if the token is invalid
192
272
  @oauth.validate(token: "")
193
273
 
194
274
  # Revoke a Token
275
+ # Returns false if the token couldn't be revoked
195
276
  @oauth.revoke(token: "")
196
277
  ```
197
278
 
279
+ #### Device Code Grant Flow
280
+
281
+ For apps with limited input, such as CLIs, set-top boxes or games.
282
+ See [the Twitch docs](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-code-grant-flow).
283
+
284
+ ```ruby
285
+ # scopes can be an array or a space-delimited string
286
+ scopes = ["user:read:email", "chat:read"]
287
+ device = @oauth.device(scopes: scopes)
288
+
289
+ puts "Go to #{device.verification_uri} and enter #{device.user_code}"
290
+
291
+ # Poll until the user has authorized the app
292
+ token = begin
293
+ sleep device.interval
294
+ @oauth.device_token(device_code: device.device_code, scopes: scopes)
295
+ rescue Twitch::Errors::BadRequestError => e
296
+ retry if e.twitch_error_message == "authorization_pending"
297
+ raise
298
+ end
299
+ ```
300
+
198
301
  ### Users
199
302
 
200
303
  ```ruby
@@ -254,6 +357,19 @@ This library includes the ability to create, refresh and revoke OAuth tokens.
254
357
  # #<Twitch::UserAuthorization user_id="72938118", user_name="deanpcmad", user_login="deanpcmad", scopes=["user:read:email"], has_authorized=true>
255
358
  @client.users.authorization(id: 123)
256
359
  @client.users.authorization(ids: [123, 321])
360
+
361
+ # Gets all extensions the authenticated user has installed, active or not
362
+ # Required scope: user:read:broadcast or user:edit:broadcast (needed to include inactive extensions)
363
+ @client.users.extensions
364
+
365
+ # Gets the active extensions for a user, or the authenticated user if user_id is omitted
366
+ @client.users.active_extensions(user_id: 123)
367
+
368
+ # Updates the authenticated user's active extensions
369
+ # Required scope: user:edit:broadcast
370
+ @client.users.update_extensions(data: {
371
+ panel: { "1" => { active: true, id: "abc123", version: "1.0.0" } }
372
+ })
257
373
  ```
258
374
 
259
375
  ### Channels
@@ -437,6 +553,54 @@ end
437
553
  @client.eventsub_subscriptions.delete(id: "abc12-abc12-abc12")
438
554
  ```
439
555
 
556
+ ### EventSub Webhooks
557
+
558
+ When Twitch sends EventSub notifications to your webhook callback, you must verify each request came from Twitch
559
+ before trusting it. `Twitch::EventsubWebhook` checks the HMAC signature using the `secret` you passed when creating
560
+ the subscription, and rejects messages older than 10 minutes to guard against replay attacks.
561
+
562
+ Pass the **raw** request body, as the signature covers the exact bytes Twitch sent. `headers` can be a Hash,
563
+ a Rack env, or Rails' `request.headers`.
564
+
565
+ ```ruby
566
+ # Rails example
567
+ class TwitchWebhooksController < ApplicationController
568
+ skip_forgery_protection
569
+
570
+ def create
571
+ webhook = Twitch::EventsubWebhook.new(
572
+ secret: ENV["TWITCH_EVENTSUB_SECRET"],
573
+ headers: request.headers,
574
+ body: request.raw_post
575
+ )
576
+
577
+ return head :forbidden unless webhook.valid?
578
+
579
+ if webhook.verification?
580
+ # Twitch confirms you own the callback when you create a subscription
581
+ render plain: webhook.challenge
582
+ elsif webhook.notification?
583
+ # Twitch may send a message more than once, so skip message IDs you've already processed
584
+ # webhook.subscription_type #=> "channel.follow"
585
+ # webhook.event #=> #<Twitch::Object user_id="1234", user_login="cool_user", ...>
586
+ head :no_content
587
+ elsif webhook.revocation?
588
+ # webhook.subscription.status #=> "authorization_revoked"
589
+ head :no_content
590
+ end
591
+ end
592
+ end
593
+
594
+ # Or just check the signature and timestamp
595
+ Twitch::EventsubWebhook.verify(secret: "...", headers: request.headers, body: request.raw_post)
596
+
597
+ # Allow a different maximum message age, in seconds (default 600)
598
+ Twitch::EventsubWebhook.new(secret: "...", headers: request.headers, body: request.raw_post, max_age: 300)
599
+ ```
600
+
601
+ Other readers: `message_id`, `message_type`, `timestamp`, `retry?`, `subscription_version`, `payload` (the parsed body),
602
+ `signature_valid?`, and `expired?`.
603
+
440
604
  ### Custom Power-ups
441
605
 
442
606
  ```ruby
@@ -484,15 +648,13 @@ shards = [
484
648
  }
485
649
  }
486
650
  ]
487
- @client.eventsub_conduits.update_shards(id: "abc123-def456", shards: shards)
488
- ```
651
+ result = @client.eventsub_conduits.update_shards(id: "abc123-def456", shards: shards)
489
652
 
490
- ### Banned Events
491
-
492
- ```ruby
493
- # Retrieves all ban and un-ban events for a channel
494
- # Available parameters: user_id
495
- @client.banned_events.list(broadcaster_id: 123)
653
+ # Twitch accepts up to 100 shards per request, so larger lists are sent in batches automatically.
654
+ # Twitch applies the valid shards even if others fail, so check errors for any that didn't update:
655
+ result.errors.each do |error|
656
+ puts "Shard #{error.id} failed: #{error.message} (#{error.code})"
657
+ end
496
658
  ```
497
659
 
498
660
  ### Banned Users
@@ -735,6 +897,11 @@ messages = [{msg_id: "abc1", msg_text: "is this allowed?"}, {msg_id: "abc2", msg
735
897
  # Required scope: channel:read:charity
736
898
  # broadcaster_id must match the currently authenticated user
737
899
  @client.charity_campaigns.list broadcaster_id: 123
900
+
901
+ # Gets the donations made to the broadcaster's active charity campaign
902
+ # Required scope: channel:read:charity
903
+ # broadcaster_id must match the currently authenticated user
904
+ @client.charity_campaigns.donations(broadcaster_id: 123)
738
905
  ```
739
906
 
740
907
  ### Chatters
@@ -900,8 +1067,12 @@ outcomes = [
900
1067
  # Check if a user is subscribed to a broadcaster
901
1068
  # Required scope: user:read:subscriptions
902
1069
  # user_id must match the currently authenticated user
1070
+ # Returns a collection with the subscription, or raises Twitch::Errors::EntityNotFoundError if not subscribed
903
1071
  @client.subscriptions.is_subscribed(broadcaster_id: 123, user_id: 456)
904
1072
 
1073
+ # Or get true/false
1074
+ @client.subscriptions.subscribed?(broadcaster_id: 123, user_id: 456)
1075
+
905
1076
  # Get subscription counts and points for a broadcaster
906
1077
  # Required scope: channel:read:subscriptions
907
1078
  # broadcaster_id must match the currently authenticated user
@@ -969,44 +1140,188 @@ outcomes = [
969
1140
  @client.stream_markers.list(video_id: "video-id")
970
1141
  ```
971
1142
 
972
- ### Tags
1143
+ ### Hype Train Status
973
1144
 
974
1145
  ```ruby
975
- # Get all stream tags
976
- @client.tags.list
1146
+ # Get hype train status for a broadcaster
1147
+ # Required scope: channel:read:hype_train
1148
+ # broadcaster_id must match the currently authenticated user
1149
+ @client.hype_train_status.retrieve(broadcaster_id: 123)
1150
+ ```
977
1151
 
978
- # Get stream tags for a specific broadcaster
979
- @client.tags.stream(broadcaster_id: 123)
980
1152
 
981
- # Replace stream tags for a broadcaster
982
- # Required scope: channel:manage:broadcast
1153
+ ### Ads
1154
+
1155
+ ```ruby
1156
+ # Gets the broadcaster's ad schedule and snooze details
1157
+ # Required scope: channel:read:ads
983
1158
  # broadcaster_id must match the currently authenticated user
984
- tag_ids = ["tag-id-1", "tag-id-2"]
985
- @client.tags.replace(broadcaster_id: 123, tag_ids: tag_ids)
1159
+ @client.ads.schedule(broadcaster_id: 123)
1160
+
1161
+ # Pushes back the next scheduled ad by 5 minutes
1162
+ # Required scope: channel:manage:ads
1163
+ # broadcaster_id must match the currently authenticated user
1164
+ @client.ads.snooze(broadcaster_id: 123)
986
1165
  ```
987
1166
 
988
- ### Hype Train Status
1167
+ ### Analytics
989
1168
 
990
1169
  ```ruby
991
- # Get hype train status for a broadcaster
992
- # Required scope: channel:read:hype_train
993
- # broadcaster_id must match the currently authenticated user
994
- @client.hype_train_status.retrieve(broadcaster_id: 123)
1170
+ # Gets URLs for downloadable CSV reports about the authenticated user's extensions
1171
+ # Required scope: analytics:read:extensions
1172
+ # Available parameters: extension_id, type, started_at, ended_at, first, after
1173
+ @client.analytics.extensions(extension_id: "abc123")
1174
+
1175
+ # Gets URLs for downloadable CSV reports about the authenticated user's games
1176
+ # Required scope: analytics:read:games
1177
+ # Available parameters: game_id, type, started_at, ended_at, first, after
1178
+ @client.analytics.games(game_id: 123)
995
1179
  ```
996
1180
 
997
- ### Moderator Events
1181
+ ### Bits
998
1182
 
999
1183
  ```ruby
1000
- # Get moderator events for a broadcaster
1001
- # Required scope: moderation:read
1002
- # broadcaster_id must match the currently authenticated user
1003
- @client.moderator_events.list(broadcaster_id: 123)
1184
+ # Gets the Bits leaderboard for the authenticated broadcaster
1185
+ # Required scope: bits:read
1186
+ # Available parameters: count, period, started_at, user_id
1187
+ @client.bits.leaderboard(count: 10, period: "week")
1188
+
1189
+ # Gets the global Cheermotes, plus a broadcaster's custom Cheermotes if broadcaster_id is given
1190
+ @client.bits.cheermotes
1191
+ @client.bits.cheermotes(broadcaster_id: 123)
1192
+ ```
1193
+
1194
+ ### Chat Settings
1195
+
1196
+ ```ruby
1197
+ # Gets a broadcaster's chat settings
1198
+ # Pass moderator_id (matching the authenticated user) to include non_moderator_chat_delay settings
1199
+ @client.chat_settings.retrieve(broadcaster_id: 123)
1200
+ @client.chat_settings.retrieve(broadcaster_id: 123, moderator_id: 321)
1201
+
1202
+ # Updates a broadcaster's chat settings
1203
+ # Required scope: moderator:manage:chat_settings
1204
+ # moderator_id must match the currently authenticated user
1205
+ # Available attributes: emote_mode, follower_mode, follower_mode_duration, non_moderator_chat_delay,
1206
+ # non_moderator_chat_delay_duration, slow_mode, slow_mode_wait_time, subscriber_mode, unique_chat_mode
1207
+ @client.chat_settings.update(broadcaster_id: 123, moderator_id: 321, slow_mode: true, slow_mode_wait_time: 10)
1208
+ ```
1209
+
1210
+ ### Shield Mode
1211
+
1212
+ ```ruby
1213
+ # Gets a broadcaster's Shield Mode status
1214
+ # Required scope: moderator:read:shield_mode or moderator:manage:shield_mode
1215
+ # moderator_id must match the currently authenticated user
1216
+ @client.shield_mode.retrieve(broadcaster_id: 123, moderator_id: 321)
1217
+
1218
+ # Turns Shield Mode on or off
1219
+ # Required scope: moderator:manage:shield_mode
1220
+ # moderator_id must match the currently authenticated user
1221
+ @client.shield_mode.update(broadcaster_id: 123, moderator_id: 321, is_active: true)
1222
+ ```
1223
+
1224
+ ### Content Classification Labels
1225
+
1226
+ ```ruby
1227
+ # Gets the content classification labels that can be applied to a channel with channels.update
1228
+ @client.content_classification_labels.list
1229
+ @client.content_classification_labels.list(locale: "en-US")
1230
+ ```
1231
+
1232
+ ### Teams
1233
+
1234
+ ```ruby
1235
+ # Gets a team by ID or name
1236
+ @client.teams.retrieve(id: 123)
1237
+ @client.teams.retrieve(name: "staff")
1238
+
1239
+ # Gets the teams a broadcaster is a member of
1240
+ @client.teams.channel(broadcaster_id: 123)
1241
+ ```
1242
+
1243
+ ### Drops Entitlements
1244
+
1245
+ ```ruby
1246
+ # Gets Drops entitlements
1247
+ # The Client ID must be owned by a member of the organization that owns the game
1248
+ # Available parameters: id, user_id, game_id, fulfillment_status, first, after
1249
+ @client.drops_entitlements.list(user_id: 123)
1250
+
1251
+ # Updates the fulfillment status of Drops entitlements (CLAIMED or FULFILLED)
1252
+ @client.drops_entitlements.update(entitlement_ids: ["abc", "def"], fulfillment_status: "FULFILLED")
1253
+ ```
1254
+
1255
+ ### Extensions
1256
+
1257
+ Most Extensions endpoints require a signed JWT created by your Extension Backend Service rather than an
1258
+ OAuth token. Pass the JWT as the client's `access_token`. See
1259
+ [Signing the JWT](https://dev.twitch.tv/docs/extensions/building/#signing-the-jwt).
1260
+
1261
+ ```ruby
1262
+ @ext_client = Twitch::Client.new(client_id: "extension-client-id", access_token: signed_jwt)
1263
+
1264
+ # Gets an extension (requires a JWT), or a released extension (app or user token)
1265
+ @ext_client.extensions.retrieve(extension_id: "abc123")
1266
+ @client.extensions.released(extension_id: "abc123", extension_version: "1.0.0")
1267
+
1268
+ # Gets live channels that have the extension installed or activated
1269
+ @client.extensions.live_channels(extension_id: "abc123")
1270
+
1271
+ # Gets and sets configuration segments (requires a JWT)
1272
+ # segment: broadcaster, developer or global. Pass an array to get more than one.
1273
+ @ext_client.extensions.configuration(extension_id: "abc123", segment: "broadcaster", broadcaster_id: 123)
1274
+ @ext_client.extensions.set_configuration(extension_id: "abc123", segment: "broadcaster", broadcaster_id: 123, content: "{}", version: "1")
1275
+ @ext_client.extensions.set_required_configuration(broadcaster_id: 123, extension_id: "abc123", extension_version: "1.0.0", required_configuration: "RCS")
1276
+
1277
+ # Sends a PubSub message or chat message (requires a JWT)
1278
+ @ext_client.extensions.send_pubsub_message(broadcaster_id: 123, target: ["broadcast"], message: "hello")
1279
+ @ext_client.extensions.send_chat_message(broadcaster_id: 123, text: "hello", extension_id: "abc123", extension_version: "1.0.0")
1280
+
1281
+ # Gets or creates the extension's JWT secrets (requires a JWT)
1282
+ @ext_client.extensions.secrets(extension_id: "abc123")
1283
+ @ext_client.extensions.create_secret(extension_id: "abc123", delay: 300)
1284
+
1285
+ # Gets and updates Bits products
1286
+ # Requires an app access token whose Client ID matches the extension's
1287
+ @app_client.extensions.bits_products(should_include_all: true)
1288
+ @app_client.extensions.update_bits_product(sku: "sku-1", cost: { amount: 100, type: "bits" }, display_name: "Thing")
1289
+
1290
+ # Gets Bits transactions for the extension
1291
+ # Requires an app access token
1292
+ @app_client.extensions.transactions(extension_id: "abc123")
1293
+ ```
1294
+
1295
+ ### Guest Star (Beta)
1296
+
1297
+ ```ruby
1298
+ # Gets and updates a channel's Guest Star settings
1299
+ @client.guest_star.settings(broadcaster_id: 123, moderator_id: 321)
1300
+ @client.guest_star.update_settings(broadcaster_id: 123, slot_count: 4)
1301
+
1302
+ # Gets, creates and ends a Guest Star session
1303
+ @client.guest_star.session(broadcaster_id: 123, moderator_id: 321)
1304
+ @client.guest_star.create_session(broadcaster_id: 123)
1305
+ @client.guest_star.end_session(broadcaster_id: 123, session_id: "abc")
1306
+
1307
+ # Gets, sends and deletes invites
1308
+ @client.guest_star.invites(broadcaster_id: 123, moderator_id: 321, session_id: "abc")
1309
+ @client.guest_star.send_invite(broadcaster_id: 123, moderator_id: 321, session_id: "abc", guest_id: 456)
1310
+ @client.guest_star.delete_invite(broadcaster_id: 123, moderator_id: 321, session_id: "abc", guest_id: 456)
1311
+
1312
+ # Assigns, moves and removes guests in slots
1313
+ @client.guest_star.assign_slot(broadcaster_id: 123, moderator_id: 321, session_id: "abc", guest_id: 456, slot_id: "1")
1314
+ @client.guest_star.update_slot(broadcaster_id: 123, moderator_id: 321, session_id: "abc", source_slot_id: "1", destination_slot_id: "2")
1315
+ @client.guest_star.delete_slot(broadcaster_id: 123, moderator_id: 321, session_id: "abc", guest_id: 456, slot_id: "1")
1316
+
1317
+ # Updates a slot's audio, video, live and volume settings
1318
+ @client.guest_star.update_slot_settings(broadcaster_id: 123, moderator_id: 321, session_id: "abc", slot_id: "1", volume: 50)
1004
1319
  ```
1005
1320
 
1006
1321
 
1007
1322
  ## Contributing
1008
1323
 
1009
- Bug reports and pull requests are welcome on GitHub at https://github.com/deanpcmad/twitchrb.
1324
+ Bug reports and pull requests are welcome on GitHub at https://github.com/d34ndev/twitchrb.
1010
1325
 
1011
1326
  ## License
1012
1327