twitchrb 1.11.0 → 2.1.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +373 -46
  5. data/lib/twitch/client.rb +109 -20
  6. data/lib/twitch/collection.rb +54 -8
  7. data/lib/twitch/eventsub_webhook.rb +140 -0
  8. data/lib/twitch/oauth.rb +51 -14
  9. data/lib/twitch/object.rb +93 -14
  10. data/lib/twitch/objects/ad_schedule.rb +4 -0
  11. data/lib/twitch/objects/analytics_report.rb +4 -0
  12. data/lib/twitch/objects/bits_leaderboard_entry.rb +4 -0
  13. data/lib/twitch/objects/charity_donation.rb +4 -0
  14. data/lib/twitch/objects/chat_settings.rb +4 -0
  15. data/lib/twitch/objects/cheermote.rb +4 -0
  16. data/lib/twitch/objects/content_classification_label.rb +4 -0
  17. data/lib/twitch/objects/drops_entitlement.rb +4 -0
  18. data/lib/twitch/objects/drops_entitlement_update.rb +4 -0
  19. data/lib/twitch/objects/extension.rb +4 -0
  20. data/lib/twitch/objects/extension_bits_product.rb +4 -0
  21. data/lib/twitch/objects/extension_configuration_segment.rb +4 -0
  22. data/lib/twitch/objects/extension_live_channel.rb +4 -0
  23. data/lib/twitch/objects/extension_secret.rb +4 -0
  24. data/lib/twitch/objects/extension_transaction.rb +4 -0
  25. data/lib/twitch/objects/guest_star_invite.rb +4 -0
  26. data/lib/twitch/objects/guest_star_session.rb +4 -0
  27. data/lib/twitch/objects/guest_star_settings.rb +4 -0
  28. data/lib/twitch/objects/shield_mode_status.rb +4 -0
  29. data/lib/twitch/objects/{tag.rb → team.rb} +1 -1
  30. data/lib/twitch/objects/user_active_extensions.rb +4 -0
  31. data/lib/twitch/objects/user_extension.rb +4 -0
  32. data/lib/twitch/resource.rb +29 -0
  33. data/lib/twitch/resources/ads.rb +18 -0
  34. data/lib/twitch/resources/analytics.rb +19 -0
  35. data/lib/twitch/resources/announcements.rb +1 -1
  36. data/lib/twitch/resources/automod.rb +7 -7
  37. data/lib/twitch/resources/badges.rb +2 -2
  38. data/lib/twitch/resources/banned_users.rb +3 -3
  39. data/lib/twitch/resources/bits.rb +16 -0
  40. data/lib/twitch/resources/blocked_terms.rb +3 -3
  41. data/lib/twitch/resources/channels.rb +4 -4
  42. data/lib/twitch/resources/charity_campaigns.rb +8 -0
  43. data/lib/twitch/resources/chat_settings.rb +20 -0
  44. data/lib/twitch/resources/chatters.rb +1 -1
  45. data/lib/twitch/resources/clips.rb +3 -9
  46. data/lib/twitch/resources/content_classification_labels.rb +8 -0
  47. data/lib/twitch/resources/custom_power_ups.rb +1 -1
  48. data/lib/twitch/resources/custom_reward_redemptions.rb +3 -4
  49. data/lib/twitch/resources/custom_rewards.rb +5 -5
  50. data/lib/twitch/resources/drops_entitlements.rb +17 -0
  51. data/lib/twitch/resources/emotes.rb +3 -3
  52. data/lib/twitch/resources/eventsub_conduits.rb +14 -4
  53. data/lib/twitch/resources/eventsub_subscriptions.rb +1 -1
  54. data/lib/twitch/resources/extensions.rb +99 -0
  55. data/lib/twitch/resources/games.rb +3 -3
  56. data/lib/twitch/resources/goals.rb +1 -1
  57. data/lib/twitch/resources/guest_star.rb +102 -0
  58. data/lib/twitch/resources/moderators.rb +3 -3
  59. data/lib/twitch/resources/pinned_chat_messages.rb +4 -11
  60. data/lib/twitch/resources/polls.rb +1 -1
  61. data/lib/twitch/resources/predictions.rb +1 -1
  62. data/lib/twitch/resources/raids.rb +2 -3
  63. data/lib/twitch/resources/search.rb +2 -2
  64. data/lib/twitch/resources/shield_mode.rb +18 -0
  65. data/lib/twitch/resources/shoutouts.rb +1 -1
  66. data/lib/twitch/resources/stream_markers.rb +1 -1
  67. data/lib/twitch/resources/stream_schedule.rb +4 -5
  68. data/lib/twitch/resources/streams.rb +2 -2
  69. data/lib/twitch/resources/subscriptions.rb +13 -2
  70. data/lib/twitch/resources/suspicious_users.rb +1 -1
  71. data/lib/twitch/resources/teams.rb +20 -0
  72. data/lib/twitch/resources/unban_requests.rb +2 -2
  73. data/lib/twitch/resources/users.rb +30 -34
  74. data/lib/twitch/resources/videos.rb +2 -2
  75. data/lib/twitch/resources/vips.rb +2 -2
  76. data/lib/twitch/resources/warnings.rb +2 -2
  77. data/lib/twitch/resources/whispers.rb +1 -1
  78. data/lib/twitch/version.rb +1 -1
  79. data/lib/twitch.rb +33 -10
  80. metadata +42 -46
  81. data/.env.example +0 -3
  82. data/.github/FUNDING.yml +0 -5
  83. data/.github/dependabot.yml +0 -11
  84. data/.github/workflows/ci.yml +0 -39
  85. data/.gitignore +0 -9
  86. data/.rubocop.yml +0 -16
  87. data/Gemfile +0 -13
  88. data/Gemfile.lock +0 -144
  89. data/Rakefile +0 -10
  90. data/bin/console +0 -23
  91. data/bin/setup +0 -8
  92. data/bin/smoke +0 -118
  93. data/lib/twitch/objects/banned_event.rb +0 -4
  94. data/lib/twitch/objects/followed_user.rb +0 -4
  95. data/lib/twitch/objects/hype_train_event.rb +0 -4
  96. data/lib/twitch/objects/moderator_event.rb +0 -4
  97. data/lib/twitch/resources/banned_events.rb +0 -8
  98. data/lib/twitch/resources/hype_train_events.rb +0 -10
  99. data/lib/twitch/resources/moderator_events.rb +0 -9
  100. data/lib/twitch/resources/tags.rb +0 -18
  101. data/mise.toml +0 -2
  102. data/twitchrb.gemspec +0 -29
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class DropsEntitlementUpdate < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class Extension < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class ExtensionBitsProduct < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class ExtensionConfigurationSegment < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class ExtensionLiveChannel < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class ExtensionSecret < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class ExtensionTransaction < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class GuestStarInvite < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class GuestStarSession < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class GuestStarSettings < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class ShieldModeStatus < Object
3
+ end
4
+ end
@@ -1,4 +1,4 @@
1
1
  module Twitch
2
- class Tag < Object
2
+ class Team < Object
3
3
  end
4
4
  end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class UserActiveExtensions < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class UserExtension < Object
3
+ end
4
+ end
@@ -33,9 +33,38 @@ module Twitch
33
33
  execute_request { client.connection.delete(url, params, headers) }
34
34
  end
35
35
 
36
+ # Builds a Collection that can fetch its following pages by repeating the
37
+ # original GET request with the cursor as the after param
38
+ def collection(response, type:)
39
+ Collection.from_response(response, type: type, next_page: next_page_fetcher(response, type))
40
+ end
41
+
42
+ def next_page_fetcher(response, type)
43
+ return unless response.respond_to?(:env) && response.env.method == :get
44
+
45
+ url = response.env.url
46
+ path = url.path.delete_prefix("#{URI(Client::BASE_URL).path}/")
47
+ params = Faraday::FlatParamsEncoder.decode(url.query) || {}
48
+ params.delete("before")
49
+
50
+ ->(cursor) { collection(get_request(path, params: params.merge("after" => cursor)), type: type) }
51
+ end
52
+
53
+ # Builds a path with a query string, for write endpoints that take some parameters
54
+ # in the query string rather than the body. nil values are dropped.
55
+ def query_path(path, params)
56
+ "#{path}?#{URI.encode_www_form(params.compact)}"
57
+ end
58
+
36
59
  def execute_request
60
+ access_token = client.access_token
37
61
  response = yield
38
62
 
63
+ # Retry once with a refreshed token if the access token has expired
64
+ if response.status == 401 && client.refresh_after_unauthorized(response, access_token)
65
+ response = yield
66
+ end
67
+
39
68
  # Retry once after a 429, waiting until the rate limit window resets
40
69
  if response.status == 429 && client.auto_retry_rate_limit
41
70
  update_rate_limit(response)
@@ -0,0 +1,18 @@
1
+ module Twitch
2
+ class AdsResource < Resource
3
+ # Required scope: channel:read:ads
4
+ # broadcaster_id must match the user in the OAuth token
5
+ def schedule(broadcaster_id:)
6
+ response = get_request("channels/ads", params: { broadcaster_id: broadcaster_id })
7
+ AdSchedule.new(response.body.dig("data")[0])
8
+ end
9
+
10
+ # Pushes back the next scheduled ad by 5 minutes
11
+ # Required scope: channel:manage:ads
12
+ # broadcaster_id must match the user in the OAuth token
13
+ def snooze(broadcaster_id:)
14
+ response = post_request(query_path("channels/ads/schedule/snooze", broadcaster_id: broadcaster_id), body: {})
15
+ AdSchedule.new(response.body.dig("data")[0])
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module Twitch
2
+ class AnalyticsResource < Resource
3
+ # Gets URLs for downloadable CSV reports about the user's extensions
4
+ # Required scope: analytics:read:extensions
5
+ # Available parameters: extension_id, type, started_at, ended_at, first, after
6
+ def extensions(**params)
7
+ response = get_request("analytics/extensions", params: params)
8
+ collection(response, type: AnalyticsReport)
9
+ end
10
+
11
+ # Gets URLs for downloadable CSV reports about the user's games
12
+ # Required scope: analytics:read:games
13
+ # Available parameters: game_id, type, started_at, ended_at, first, after
14
+ def games(**params)
15
+ response = get_request("analytics/games", params: params)
16
+ collection(response, type: AnalyticsReport)
17
+ end
18
+ end
19
+ end
@@ -5,7 +5,7 @@ module Twitch
5
5
  def create(broadcaster_id:, moderator_id:, message:, color: nil, for_source_only: nil)
6
6
  attrs = { message: message, color: color, for_source_only: for_source_only }
7
7
 
8
- post_request("chat/announcements?#{URI.encode_www_form(broadcaster_id: broadcaster_id, moderator_id: moderator_id)}", body: attrs)
8
+ post_request(query_path("chat/announcements", broadcaster_id: broadcaster_id, moderator_id: moderator_id), body: attrs)
9
9
  end
10
10
  end
11
11
  end
@@ -3,17 +3,17 @@ module Twitch
3
3
  # Checks if a supplied message meets the channel's AutoMod requirements
4
4
  # Required scope: moderation:read
5
5
  def check_status(broadcaster_id:, id:, text:)
6
- attrs = { broadcaster_id: broadcaster_id, data: [ { msg_id: id, msg_text: text } ] }
7
- response = post_request("moderation/enforcements/status", body: attrs)
6
+ attrs = { data: [ { msg_id: id, msg_text: text } ] }
7
+ response = post_request(query_path("moderation/enforcements/status", broadcaster_id: broadcaster_id), body: attrs)
8
8
  AutomodStatus.new response.body.dig("data")[0]
9
9
  end
10
10
 
11
11
  # Checks if multiple supplied messages meet the channel's AutoMod requirements
12
12
  # Required scope: moderation:read
13
13
  def check_status_multiple(broadcaster_id:, messages:)
14
- attrs = { broadcaster_id: broadcaster_id, data: messages }
15
- response = post_request("moderation/enforcements/status", body: attrs)
16
- Collection.from_response(response, type: AutomodStatus)
14
+ attrs = { data: messages }
15
+ response = post_request(query_path("moderation/enforcements/status", broadcaster_id: broadcaster_id), body: attrs)
16
+ collection(response, type: AutomodStatus)
17
17
  end
18
18
 
19
19
  def manage_message(user_id:, msg_id:, action:)
@@ -27,8 +27,8 @@ module Twitch
27
27
  end
28
28
 
29
29
  def update_settings(broadcaster_id:, moderator_id:, **params)
30
- attrs = { broadcaster_id: broadcaster_id, moderator_id: moderator_id }
31
- response = put_request("moderation/automod/settings", body: attrs.merge(params))
30
+ path = query_path("moderation/automod/settings", broadcaster_id: broadcaster_id, moderator_id: moderator_id)
31
+ response = put_request(path, body: params)
32
32
  AutomodSetting.new response.body.dig("data")[0]
33
33
  end
34
34
  end
@@ -2,12 +2,12 @@ module Twitch
2
2
  class BadgesResource < Resource
3
3
  def channel(broadcaster_id:)
4
4
  response = get_request("chat/badges", params: { broadcaster_id: broadcaster_id })
5
- Collection.from_response(response, type: Badge)
5
+ collection(response, type: Badge)
6
6
  end
7
7
 
8
8
  def global
9
9
  response = get_request("chat/badges/global")
10
- Collection.from_response(response, type: Badge)
10
+ collection(response, type: Badge)
11
11
  end
12
12
  end
13
13
  end
@@ -3,14 +3,14 @@ module Twitch
3
3
  # Broadcaster ID must match the user in the OAuth token
4
4
  def list(broadcaster_id:, **params)
5
5
  response = get_request("moderation/banned", params: params.merge(broadcaster_id: broadcaster_id))
6
- Collection.from_response(response, type: BannedUser)
6
+ collection(response, type: BannedUser)
7
7
  end
8
8
 
9
9
  # Required scope: moderator:manage:banned_users
10
10
  # moderator_id must match the currently authenticated user. Can be either the broadcaster ID or moderator ID
11
11
  def create(broadcaster_id:, moderator_id:, user_id:, reason:, duration: nil)
12
- attrs = { broadcaster_id: broadcaster_id, moderator_id: moderator_id, data: { user_id: user_id, reason: reason, duration: duration } }
13
- response = post_request("moderation/bans", body: attrs)
12
+ path = query_path("moderation/bans", broadcaster_id: broadcaster_id, moderator_id: moderator_id)
13
+ response = post_request(path, body: { data: { user_id: user_id, reason: reason, duration: duration }.compact })
14
14
  BannedUser.new response.body.dig("data")[0]
15
15
  end
16
16
 
@@ -0,0 +1,16 @@
1
+ module Twitch
2
+ class BitsResource < Resource
3
+ # Required scope: bits:read
4
+ # Available parameters: count, period, started_at, user_id
5
+ def leaderboard(**params)
6
+ response = get_request("bits/leaderboard", params: params)
7
+ collection(response, type: BitsLeaderboardEntry)
8
+ end
9
+
10
+ # Pass broadcaster_id to include the broadcaster's custom Cheermotes
11
+ def cheermotes(broadcaster_id: nil)
12
+ response = get_request("bits/cheermotes", params: { broadcaster_id: broadcaster_id }.compact)
13
+ collection(response, type: Cheermote)
14
+ end
15
+ end
16
+ end
@@ -5,14 +5,14 @@ module Twitch
5
5
  def list(broadcaster_id:, moderator_id:, **params)
6
6
  attrs = { broadcaster_id: broadcaster_id, moderator_id: moderator_id }
7
7
  response = get_request("moderation/blocked_terms", params: attrs.merge(params))
8
- Collection.from_response(response, type: BlockedTerm)
8
+ collection(response, type: BlockedTerm)
9
9
  end
10
10
 
11
11
  # Required scope: moderator:manage:blocked_terms
12
12
  # moderator_id must match the currently authenticated user. Can be either the broadcaster ID or moderator ID
13
13
  def create(broadcaster_id:, moderator_id:, text:)
14
- attrs = { broadcaster_id: broadcaster_id, moderator_id: moderator_id, text: text }
15
- response = post_request("moderation/blocked_terms", body: attrs)
14
+ path = query_path("moderation/blocked_terms", broadcaster_id: broadcaster_id, moderator_id: moderator_id)
15
+ response = post_request(path, body: { text: text })
16
16
  BlockedTerm.new response.body.dig("data")[0]
17
17
  end
18
18
 
@@ -9,7 +9,7 @@ module Twitch
9
9
  # user_id must match the authenticated user
10
10
  def followed(user_id:, **params)
11
11
  response = get_request("channels/followed", params: params.merge(user_id: user_id))
12
- Collection.from_response(response, type: User)
12
+ collection(response, type: User)
13
13
  end
14
14
 
15
15
  # Retrieve a list of users that follow a specified broadcaster
@@ -17,7 +17,7 @@ module Twitch
17
17
  # broadcaster_id must match the authenticated user
18
18
  def followers(broadcaster_id:, **params)
19
19
  response = get_request("channels/followers", params: params.merge(broadcaster_id: broadcaster_id))
20
- Collection.from_response(response, type: User)
20
+ collection(response, type: User)
21
21
  end
22
22
 
23
23
  # Grabs the number of Followers a broadcaster has
@@ -37,13 +37,13 @@ module Twitch
37
37
 
38
38
  # Requires scope: channel:manage:broadcast
39
39
  def update(broadcaster_id:, **attributes)
40
- patch_request("channels", body: attributes.merge(broadcaster_id: broadcaster_id))
40
+ patch_request(query_path("channels", broadcaster_id: broadcaster_id), body: attributes)
41
41
  end
42
42
 
43
43
  # Requires scope: channel:read:editors
44
44
  def editors(broadcaster_id:)
45
45
  response = get_request("channels/editors", params: { broadcaster_id: broadcaster_id })
46
- Collection.from_response(response, type: ChannelEditor)
46
+ collection(response, type: ChannelEditor)
47
47
  end
48
48
 
49
49
  def stream_key(broadcaster_id:)
@@ -10,5 +10,13 @@ module Twitch
10
10
  nil
11
11
  end
12
12
  end
13
+
14
+ # Required scope: channel:read:charity
15
+ # Broadcaster ID must match the user in the OAuth token
16
+ # Available parameters: first, after
17
+ def donations(broadcaster_id:, **params)
18
+ response = get_request("charity/donations", params: params.merge(broadcaster_id: broadcaster_id))
19
+ collection(response, type: CharityDonation)
20
+ end
13
21
  end
14
22
  end
@@ -0,0 +1,20 @@
1
+ module Twitch
2
+ class ChatSettingsResource < Resource
3
+ # moderator_id is only needed to include non_moderator_chat_delay settings,
4
+ # and must match the user in the OAuth token
5
+ def retrieve(broadcaster_id:, moderator_id: nil)
6
+ response = get_request("chat/settings", params: { broadcaster_id: broadcaster_id, moderator_id: moderator_id }.compact)
7
+ ChatSettings.new(response.body.dig("data")[0])
8
+ end
9
+
10
+ # Required scope: moderator:manage:chat_settings
11
+ # moderator_id must match the user in the OAuth token
12
+ # Available attributes: emote_mode, follower_mode, follower_mode_duration, non_moderator_chat_delay,
13
+ # non_moderator_chat_delay_duration, slow_mode, slow_mode_wait_time, subscriber_mode, unique_chat_mode
14
+ def update(broadcaster_id:, moderator_id:, **attributes)
15
+ path = query_path("chat/settings", broadcaster_id: broadcaster_id, moderator_id: moderator_id)
16
+ response = patch_request(path, body: attributes)
17
+ ChatSettings.new(response.body.dig("data")[0])
18
+ end
19
+ end
20
+ end
@@ -7,7 +7,7 @@ module Twitch
7
7
  attrs = { broadcaster_id: broadcaster_id, moderator_id: moderator_id }
8
8
  response = get_request("chat/chatters", params: attrs.merge(params))
9
9
 
10
- Collection.from_response(response, type: Chatter)
10
+ collection(response, type: Chatter)
11
11
  end
12
12
  end
13
13
  end
@@ -1,10 +1,10 @@
1
1
  module Twitch
2
2
  class ClipsResource < Resource
3
3
  def list(**params)
4
- raise "broadcaster_id or game_id is required" unless !params[:broadcaster_id].nil? || !params[:game_id].nil?
4
+ raise ArgumentError, "broadcaster_id, game_id or id is required" if params.values_at(:broadcaster_id, :game_id, :id).all?(&:nil?)
5
5
 
6
6
  response = get_request("clips", params: params)
7
- Collection.from_response(response, type: Clip)
7
+ collection(response, type: Clip)
8
8
  end
9
9
 
10
10
  def retrieve(id:)
@@ -37,13 +37,7 @@ module Twitch
37
37
  ids = clip_ids || Array(clip_id)
38
38
  response = get_request("clips/downloads", params: { editor_id: editor_id, broadcaster_id: broadcaster_id, clip_id: ids })
39
39
 
40
- Collection.from_response(response, type: ClipDownload)
41
- end
42
-
43
- private
44
-
45
- def query_path(path, params)
46
- "#{path}?#{URI.encode_www_form(params)}"
40
+ collection(response, type: ClipDownload)
47
41
  end
48
42
  end
49
43
  end
@@ -0,0 +1,8 @@
1
+ module Twitch
2
+ class ContentClassificationLabelsResource < Resource
3
+ def list(locale: nil)
4
+ response = get_request("content_classification_labels", params: { locale: locale }.compact)
5
+ collection(response, type: ContentClassificationLabel)
6
+ end
7
+ end
8
+ end
@@ -7,7 +7,7 @@ module Twitch
7
7
  params[:id] = ids if ids
8
8
 
9
9
  response = get_request("bits/custom_power_ups", params: params)
10
- Collection.from_response(response, type: CustomPowerUp)
10
+ collection(response, type: CustomPowerUp)
11
11
  end
12
12
  end
13
13
  end
@@ -5,15 +5,14 @@ module Twitch
5
5
  def list(broadcaster_id:, reward_id:, status:, **params)
6
6
  attributes = { broadcaster_id: broadcaster_id, reward_id: reward_id, status: status.upcase }
7
7
  response = get_request("channel_points/custom_rewards/redemptions", params: attributes.merge(params))
8
- Collection.from_response(response, type: CustomRewardRedemption)
8
+ collection(response, type: CustomRewardRedemption)
9
9
  end
10
10
 
11
11
  # Required scope: channel:manage:redemptions
12
12
  # Broadcaster ID must match the user in the OAuth token
13
13
  def update(broadcaster_id:, reward_id:, redemption_id:, status:)
14
- attributes = { status: status.upcase }
15
- query = URI.encode_www_form(broadcaster_id: broadcaster_id, reward_id: reward_id, id: redemption_id, status: status.upcase)
16
- response = patch_request("channel_points/custom_rewards/redemptions?#{query}", body: attributes)
14
+ path = query_path("channel_points/custom_rewards/redemptions", broadcaster_id: broadcaster_id, reward_id: reward_id, id: redemption_id)
15
+ response = patch_request(path, body: { status: status.upcase })
17
16
 
18
17
  CustomRewardRedemption.new(response.body.dig("data")[0]) if response.success?
19
18
  end
@@ -4,19 +4,19 @@ module Twitch
4
4
  # Broadcaster ID must match the user in the OAuth token
5
5
  def list(broadcaster_id:, **params)
6
6
  response = get_request("channel_points/custom_rewards", params: params.merge(broadcaster_id: broadcaster_id))
7
- Collection.from_response(response, type: CustomReward)
7
+ collection(response, type: CustomReward)
8
8
  end
9
9
 
10
10
  def create(broadcaster_id:, title:, cost:, **params)
11
- attributes = { broadcaster_id: broadcaster_id, title: title, cost: cost }
12
- response = post_request("channel_points/custom_rewards", body: attributes.merge(params))
11
+ attributes = { title: title, cost: cost }
12
+ response = post_request(query_path("channel_points/custom_rewards", broadcaster_id: broadcaster_id), body: attributes.merge(params))
13
13
 
14
14
  CustomReward.new(response.body.dig("data")[0]) if response.success?
15
15
  end
16
16
 
17
17
  def update(broadcaster_id:, reward_id:, **params)
18
- attributes = { broadcaster_id: broadcaster_id, id: reward_id }
19
- response = patch_request("channel_points/custom_rewards", body: attributes.merge(params))
18
+ path = query_path("channel_points/custom_rewards", broadcaster_id: broadcaster_id, id: reward_id)
19
+ response = patch_request(path, body: params)
20
20
 
21
21
  CustomReward.new(response.body.dig("data")[0]) if response.success?
22
22
  end
@@ -0,0 +1,17 @@
1
+ module Twitch
2
+ class DropsEntitlementsResource < Resource
3
+ # The Client ID must be owned by a member of the organization that owns the game
4
+ # Available parameters: id, user_id, game_id, fulfillment_status, first, after
5
+ def list(**params)
6
+ response = get_request("entitlements/drops", params: params)
7
+ collection(response, type: DropsEntitlement)
8
+ end
9
+
10
+ # fulfillment_status: CLAIMED or FULFILLED
11
+ # Returns a collection of results grouped by status, each with the affected ids
12
+ def update(entitlement_ids:, fulfillment_status:)
13
+ response = patch_request("entitlements/drops", body: { entitlement_ids: Array(entitlement_ids), fulfillment_status: fulfillment_status })
14
+ collection(response, type: DropsEntitlementUpdate)
15
+ end
16
+ end
17
+ end
@@ -2,17 +2,17 @@ module Twitch
2
2
  class EmotesResource < Resource
3
3
  def channel(broadcaster_id:)
4
4
  response = get_request("chat/emotes", params: { broadcaster_id: broadcaster_id })
5
- Collection.from_response(response, type: Emote)
5
+ collection(response, type: Emote)
6
6
  end
7
7
 
8
8
  def global
9
9
  response = get_request("chat/emotes/global")
10
- Collection.from_response(response, type: Emote)
10
+ collection(response, type: Emote)
11
11
  end
12
12
 
13
13
  def sets(emote_set_id:)
14
14
  response = get_request("chat/emotes/set", params: { emote_set_id: emote_set_id })
15
- Collection.from_response(response, type: Emote)
15
+ collection(response, type: Emote)
16
16
  end
17
17
  end
18
18
  end
@@ -1,8 +1,10 @@
1
1
  module Twitch
2
2
  class EventsubConduitsResource < Resource
3
+ MAX_SHARDS_PER_UPDATE = 100
4
+
3
5
  def list(**params)
4
6
  response = get_request("eventsub/conduits", params: params)
5
- Collection.from_response(response, type: EventsubConduit)
7
+ collection(response, type: EventsubConduit)
6
8
  end
7
9
 
8
10
  def create(shard_count:)
@@ -23,12 +25,20 @@ module Twitch
23
25
 
24
26
  def shards(id:, **params)
25
27
  response = get_request("eventsub/conduits/shards", params: { conduit_id: id }.merge(params))
26
- Collection.from_response(response, type: EventsubConduitShard)
28
+ collection(response, type: EventsubConduitShard)
27
29
  end
28
30
 
31
+ # Twitch accepts up to 100 shards per request, so larger lists are sent in batches.
32
+ # Returns the updated shards. Shards that failed to update are listed in #errors
33
+ # (each with id, message and code), as Twitch applies the rest of the update anyway.
29
34
  def update_shards(id:, shards:)
30
- response = patch_request("eventsub/conduits/shards", body: { conduit_id: id, shards: shards })
31
- Collection.from_response(response, type: EventsubConduitShard)
35
+ results = shards.each_slice(MAX_SHARDS_PER_UPDATE).map do |batch|
36
+ response = patch_request("eventsub/conduits/shards", body: { conduit_id: id, shards: batch })
37
+ collection(response, type: EventsubConduitShard)
38
+ end
39
+
40
+ data = results.flat_map(&:data)
41
+ Collection.new(data: data, total: data.size, cursor: nil, errors: results.flat_map(&:errors))
32
42
  end
33
43
  end
34
44
  end
@@ -2,7 +2,7 @@ module Twitch
2
2
  class EventsubSubscriptionsResource < Resource
3
3
  def list(**params)
4
4
  response = get_request("eventsub/subscriptions", params: params)
5
- Collection.from_response(response, type: EventsubSubscription)
5
+ collection(response, type: EventsubSubscription)
6
6
  end
7
7
 
8
8
  def create(type:, version:, condition:, transport:, **params)
@@ -0,0 +1,99 @@
1
+ module Twitch
2
+ # Most of these endpoints require a signed JWT created by your Extension Backend Service,
3
+ # rather than an OAuth token. Pass the JWT as the client's access_token.
4
+ # See https://dev.twitch.tv/docs/extensions/building/#signing-the-jwt
5
+ class ExtensionsResource < Resource
6
+ # Requires a signed JWT
7
+ def retrieve(extension_id:, extension_version: nil)
8
+ response = get_request("extensions", params: { extension_id: extension_id, extension_version: extension_version }.compact)
9
+ data = response.body.dig("data")
10
+
11
+ return nil if data.nil? || data.empty?
12
+
13
+ Extension.new(data[0])
14
+ end
15
+
16
+ def released(extension_id:, extension_version: nil)
17
+ response = get_request("extensions/released", params: { extension_id: extension_id, extension_version: extension_version }.compact)
18
+ data = response.body.dig("data")
19
+
20
+ return nil if data.nil? || data.empty?
21
+
22
+ Extension.new(data[0])
23
+ end
24
+
25
+ # Available parameters: first, after
26
+ def live_channels(extension_id:, **params)
27
+ response = get_request("extensions/live", params: params.merge(extension_id: extension_id))
28
+ collection(response, type: ExtensionLiveChannel)
29
+ end
30
+
31
+ # Requires a signed JWT
32
+ # segment: broadcaster, developer or global. Pass an array to get more than one.
33
+ def configuration(extension_id:, segment:, broadcaster_id: nil)
34
+ params = { extension_id: extension_id, segment: segment, broadcaster_id: broadcaster_id }.compact
35
+ response = get_request("extensions/configurations", params: params)
36
+ collection(response, type: ExtensionConfigurationSegment)
37
+ end
38
+
39
+ # Requires a signed JWT
40
+ # Available attributes: broadcaster_id, content, version
41
+ def set_configuration(extension_id:, segment:, **attributes)
42
+ put_request("extensions/configurations", body: attributes.merge(extension_id: extension_id, segment: segment))
43
+ end
44
+
45
+ # Requires a signed JWT
46
+ def set_required_configuration(broadcaster_id:, extension_id:, extension_version:, required_configuration:)
47
+ attrs = { extension_id: extension_id, extension_version: extension_version, required_configuration: required_configuration }
48
+ put_request(query_path("extensions/required_configuration", broadcaster_id: broadcaster_id), body: attrs)
49
+ end
50
+
51
+ # Requires a signed JWT
52
+ # target: an array containing broadcast, global, or whisper-<user-id>
53
+ def send_pubsub_message(broadcaster_id:, target:, message:, is_global_broadcast: nil)
54
+ attrs = { broadcaster_id: broadcaster_id, target: Array(target), message: message, is_global_broadcast: is_global_broadcast }.compact
55
+ post_request("extensions/pubsub", body: attrs)
56
+ end
57
+
58
+ # Requires a signed JWT
59
+ def send_chat_message(broadcaster_id:, text:, extension_id:, extension_version:)
60
+ attrs = { text: text, extension_id: extension_id, extension_version: extension_version }
61
+ post_request(query_path("extensions/chat", broadcaster_id: broadcaster_id), body: attrs)
62
+ end
63
+
64
+ # Requires a signed JWT
65
+ def secrets(extension_id:)
66
+ response = get_request("extensions/jwt/secrets", params: { extension_id: extension_id })
67
+ collection(response, type: ExtensionSecret)
68
+ end
69
+
70
+ # Requires a signed JWT
71
+ # delay: seconds before the new secret becomes active (minimum 300)
72
+ def create_secret(extension_id:, delay: nil)
73
+ response = post_request(query_path("extensions/jwt/secrets", extension_id: extension_id, delay: delay), body: {})
74
+ collection(response, type: ExtensionSecret)
75
+ end
76
+
77
+ # Requires an app access token whose Client ID matches the extension's
78
+ def bits_products(should_include_all: nil)
79
+ response = get_request("bits/extensions", params: { should_include_all: should_include_all }.compact)
80
+ collection(response, type: ExtensionBitsProduct)
81
+ end
82
+
83
+ # Requires an app access token whose Client ID matches the extension's
84
+ # cost: { amount: 100, type: "bits" }
85
+ # Available attributes: in_development, expiration, is_broadcast
86
+ def update_bits_product(sku:, cost:, display_name:, **attributes)
87
+ attrs = attributes.merge(sku: sku, cost: cost, display_name: display_name)
88
+ response = put_request("bits/extensions", body: attrs)
89
+ ExtensionBitsProduct.new(response.body.dig("data")[0])
90
+ end
91
+
92
+ # Requires an app access token
93
+ # Available parameters: id, first, after
94
+ def transactions(extension_id:, **params)
95
+ response = get_request("extensions/transactions", params: params.merge(extension_id: extension_id))
96
+ collection(response, type: ExtensionTransaction)
97
+ end
98
+ end
99
+ end