twitchrb 1.10.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +104 -1
  5. data/Gemfile.lock +5 -5
  6. data/README.md +360 -45
  7. data/lib/twitch/client.rb +109 -20
  8. data/lib/twitch/collection.rb +58 -11
  9. data/lib/twitch/error_generator.rb +6 -4
  10. data/lib/twitch/eventsub_webhook.rb +140 -0
  11. data/lib/twitch/oauth.rb +50 -15
  12. data/lib/twitch/object.rb +28 -0
  13. data/lib/twitch/objects/ad_schedule.rb +4 -0
  14. data/lib/twitch/objects/analytics_report.rb +4 -0
  15. data/lib/twitch/objects/bits_leaderboard_entry.rb +4 -0
  16. data/lib/twitch/objects/charity_donation.rb +4 -0
  17. data/lib/twitch/objects/chat_settings.rb +4 -0
  18. data/lib/twitch/objects/cheermote.rb +4 -0
  19. data/lib/twitch/objects/content_classification_label.rb +4 -0
  20. data/lib/twitch/objects/drops_entitlement.rb +4 -0
  21. data/lib/twitch/objects/drops_entitlement_update.rb +4 -0
  22. data/lib/twitch/objects/extension.rb +4 -0
  23. data/lib/twitch/objects/extension_bits_product.rb +4 -0
  24. data/lib/twitch/objects/extension_configuration_segment.rb +4 -0
  25. data/lib/twitch/objects/extension_live_channel.rb +4 -0
  26. data/lib/twitch/objects/extension_secret.rb +4 -0
  27. data/lib/twitch/objects/extension_transaction.rb +4 -0
  28. data/lib/twitch/objects/guest_star_invite.rb +4 -0
  29. data/lib/twitch/objects/guest_star_session.rb +4 -0
  30. data/lib/twitch/objects/guest_star_settings.rb +4 -0
  31. data/lib/twitch/objects/shield_mode_status.rb +4 -0
  32. data/lib/twitch/objects/{tag.rb → team.rb} +1 -1
  33. data/lib/twitch/objects/user_active_extensions.rb +4 -0
  34. data/lib/twitch/objects/user_extension.rb +4 -0
  35. data/lib/twitch/resource.rb +49 -9
  36. data/lib/twitch/resources/ads.rb +18 -0
  37. data/lib/twitch/resources/analytics.rb +19 -0
  38. data/lib/twitch/resources/announcements.rb +1 -1
  39. data/lib/twitch/resources/automod.rb +8 -8
  40. data/lib/twitch/resources/badges.rb +3 -3
  41. data/lib/twitch/resources/banned_users.rb +4 -4
  42. data/lib/twitch/resources/bits.rb +16 -0
  43. data/lib/twitch/resources/blocked_terms.rb +5 -5
  44. data/lib/twitch/resources/channels.rb +7 -7
  45. data/lib/twitch/resources/charity_campaigns.rb +9 -1
  46. data/lib/twitch/resources/chat_messages.rb +1 -1
  47. data/lib/twitch/resources/chat_settings.rb +20 -0
  48. data/lib/twitch/resources/chatters.rb +1 -1
  49. data/lib/twitch/resources/clips.rb +3 -9
  50. data/lib/twitch/resources/content_classification_labels.rb +8 -0
  51. data/lib/twitch/resources/custom_power_ups.rb +1 -1
  52. data/lib/twitch/resources/custom_reward_redemptions.rb +3 -4
  53. data/lib/twitch/resources/custom_rewards.rb +5 -5
  54. data/lib/twitch/resources/drops_entitlements.rb +17 -0
  55. data/lib/twitch/resources/emotes.rb +5 -5
  56. data/lib/twitch/resources/eventsub_conduits.rb +14 -4
  57. data/lib/twitch/resources/eventsub_subscriptions.rb +9 -13
  58. data/lib/twitch/resources/extensions.rb +99 -0
  59. data/lib/twitch/resources/games.rb +5 -7
  60. data/lib/twitch/resources/goals.rb +1 -1
  61. data/lib/twitch/resources/guest_star.rb +102 -0
  62. data/lib/twitch/resources/moderators.rb +4 -4
  63. data/lib/twitch/resources/pinned_chat_messages.rb +4 -11
  64. data/lib/twitch/resources/polls.rb +1 -1
  65. data/lib/twitch/resources/predictions.rb +1 -1
  66. data/lib/twitch/resources/raids.rb +3 -4
  67. data/lib/twitch/resources/search.rb +2 -2
  68. data/lib/twitch/resources/shield_mode.rb +18 -0
  69. data/lib/twitch/resources/shoutouts.rb +1 -1
  70. data/lib/twitch/resources/stream_markers.rb +1 -1
  71. data/lib/twitch/resources/stream_schedule.rb +4 -5
  72. data/lib/twitch/resources/streams.rb +2 -2
  73. data/lib/twitch/resources/subscriptions.rb +13 -2
  74. data/lib/twitch/resources/suspicious_users.rb +6 -2
  75. data/lib/twitch/resources/teams.rb +20 -0
  76. data/lib/twitch/resources/unban_requests.rb +2 -2
  77. data/lib/twitch/resources/users.rb +37 -43
  78. data/lib/twitch/resources/videos.rb +3 -3
  79. data/lib/twitch/resources/vips.rb +3 -3
  80. data/lib/twitch/resources/warnings.rb +2 -2
  81. data/lib/twitch/resources/whispers.rb +1 -1
  82. data/lib/twitch/version.rb +1 -1
  83. data/lib/twitch.rb +33 -9
  84. data/twitchrb.gemspec +5 -5
  85. metadata +42 -18
  86. data/lib/twitch/objects/banned_event.rb +0 -4
  87. data/lib/twitch/objects/followed_user.rb +0 -4
  88. data/lib/twitch/objects/hype_train_event.rb +0 -4
  89. data/lib/twitch/objects/moderator_event.rb +0 -4
  90. data/lib/twitch/resources/banned_events.rb +0 -8
  91. data/lib/twitch/resources/hype_train_events.rb +0 -10
  92. data/lib/twitch/resources/moderator_events.rb +0 -9
  93. data/lib/twitch/resources/tags.rb +0 -18
@@ -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
@@ -10,27 +10,69 @@ module Twitch
10
10
 
11
11
  def get_request(url, params: {}, headers: {})
12
12
  validate_request_path!(url)
13
- handle_response client.connection.get(url, params, headers)
13
+ execute_request { client.connection.get(url, params, headers) }
14
14
  end
15
15
 
16
16
  def post_request(url, body:, headers: {})
17
17
  validate_request_path!(url)
18
- handle_response client.connection.post(url, body, headers)
18
+ execute_request { client.connection.post(url, body, headers) }
19
19
  end
20
20
 
21
21
  def patch_request(url, body:, headers: {})
22
22
  validate_request_path!(url)
23
- handle_response client.connection.patch(url, body, headers)
23
+ execute_request { client.connection.patch(url, body, headers) }
24
24
  end
25
25
 
26
26
  def put_request(url, body:, headers: {})
27
27
  validate_request_path!(url)
28
- handle_response client.connection.put(url, body, headers)
28
+ execute_request { client.connection.put(url, body, headers) }
29
29
  end
30
30
 
31
31
  def delete_request(url, params: {}, headers: {})
32
32
  validate_request_path!(url)
33
- handle_response client.connection.delete(url, params, headers)
33
+ execute_request { client.connection.delete(url, params, headers) }
34
+ end
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
+
59
+ def execute_request
60
+ access_token = client.access_token
61
+ response = yield
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
+
68
+ # Retry once after a 429, waiting until the rate limit window resets
69
+ if response.status == 429 && client.auto_retry_rate_limit
70
+ update_rate_limit(response)
71
+ client.rate_limiter.wait_if_rate_limited
72
+ response = yield
73
+ end
74
+
75
+ handle_response(response)
34
76
  end
35
77
 
36
78
  def handle_response(response)
@@ -44,8 +86,8 @@ module Twitch
44
86
  end
45
87
 
46
88
  def error?(response)
47
- [ 400, 401, 403, 404, 409, 429, 500, 501, 503 ].include?(response.status) ||
48
- response.body&.key?("error")
89
+ response.status >= 400 ||
90
+ (response.body.is_a?(Hash) && response.body.key?("error"))
49
91
  end
50
92
 
51
93
  def raise_error(response)
@@ -58,8 +100,6 @@ module Twitch
58
100
  raise error if error
59
101
  end
60
102
 
61
- private
62
-
63
103
  def update_rate_limit(response)
64
104
  client.rate_limiter.update(response.headers)
65
105
  client.rate_limiter.warn_if_approaching(threshold: client.rate_limit_threshold)
@@ -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?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:)
@@ -22,13 +22,13 @@ module Twitch
22
22
  end
23
23
 
24
24
  def settings(broadcaster_id:, moderator_id:)
25
- response = get_request("moderation/automod/settings?broadcaster_id=#{broadcaster_id}&moderator_id=#{moderator_id}")
25
+ response = get_request("moderation/automod/settings", params: { broadcaster_id: broadcaster_id, moderator_id: moderator_id })
26
26
  AutomodSetting.new response.body.dig("data")[0]
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
@@ -1,13 +1,13 @@
1
1
  module Twitch
2
2
  class BadgesResource < Resource
3
3
  def channel(broadcaster_id:)
4
- response = get_request("chat/badges?broadcaster_id=#{broadcaster_id}")
5
- Collection.from_response(response, type: Badge)
4
+ response = get_request("chat/badges", params: { broadcaster_id: broadcaster_id })
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,21 +3,21 @@ 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
 
17
17
  # Required scope: moderator:manage:banned_users
18
18
  # moderator_id must match the currently authenticated user. Can be either the broadcaster ID or moderator ID
19
19
  def delete(broadcaster_id:, moderator_id:, user_id:)
20
- delete_request("moderation/bans?broadcaster_id=#{broadcaster_id}&moderator_id=#{moderator_id}&user_id=#{user_id}")
20
+ delete_request("moderation/bans", params: { broadcaster_id: broadcaster_id, moderator_id: moderator_id, user_id: user_id })
21
21
  end
22
22
  end
23
23
  end
@@ -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,21 +5,21 @@ 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)
16
- BannedUser.new response.body.dig("data")[0]
14
+ path = query_path("moderation/blocked_terms", broadcaster_id: broadcaster_id, moderator_id: moderator_id)
15
+ response = post_request(path, body: { text: text })
16
+ BlockedTerm.new response.body.dig("data")[0]
17
17
  end
18
18
 
19
19
  # Required scope: moderator:manage:blocked_terms
20
20
  # moderator_id must match the currently authenticated user. Can be either the broadcaster ID or moderator ID
21
21
  def delete(broadcaster_id:, moderator_id:, id:)
22
- delete_request("moderation/blocked_terms?broadcaster_id=#{broadcaster_id}&moderator_id=#{moderator_id}&id=#{id}")
22
+ delete_request("moderation/blocked_terms", params: { broadcaster_id: broadcaster_id, moderator_id: moderator_id, id: id })
23
23
  end
24
24
  end
25
25
  end
@@ -1,7 +1,7 @@
1
1
  module Twitch
2
2
  class ChannelsResource < Resource
3
3
  def retrieve(id:)
4
- Channel.new get_request("channels?broadcaster_id=#{id}").body.dig("data")[0]
4
+ Channel.new get_request("channels", params: { broadcaster_id: id }).body.dig("data")[0]
5
5
  end
6
6
 
7
7
  # Retrieve a list of broadcasters a specified user follows
@@ -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,17 +37,17 @@ 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
- response = get_request("channels/editors?broadcaster_id=#{broadcaster_id}")
46
- Collection.from_response(response, type: ChannelEditor)
45
+ response = get_request("channels/editors", params: { broadcaster_id: broadcaster_id })
46
+ collection(response, type: ChannelEditor)
47
47
  end
48
48
 
49
49
  def stream_key(broadcaster_id:)
50
- response = get_request("streams/key?broadcaster_id=#{broadcaster_id}")
50
+ response = get_request("streams/key", params: { broadcaster_id: broadcaster_id })
51
51
  StreamKey.new(response.body.dig("data")[0])
52
52
  end
53
53
 
@@ -3,12 +3,20 @@ module Twitch
3
3
  # Required scope: channel:read:charity
4
4
  # Broadcaster ID must match the user in the OAuth token
5
5
  def list(broadcaster_id:)
6
- response = get_request("charity/campaigns?broadcaster_id=#{broadcaster_id}")
6
+ response = get_request("charity/campaigns", params: { broadcaster_id: broadcaster_id })
7
7
  if response.body.dig("data")[0]
8
8
  CharityCampaign.new(response.body.dig("data")[0])
9
9
  else
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
@@ -16,7 +16,7 @@ module Twitch
16
16
 
17
17
  # moderator_id must match the user in the OAuth token
18
18
  def delete(broadcaster_id:, moderator_id:, message_id:)
19
- delete_request("moderation/chat?broadcaster_id=#{broadcaster_id}&moderator_id=#{moderator_id}&message_id=#{message_id}")
19
+ delete_request("moderation/chat", params: { broadcaster_id: broadcaster_id, moderator_id: moderator_id, message_id: message_id })
20
20
  end
21
21
  end
22
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
- url = "channel_points/custom_rewards/redemptions?broadcaster_id=#{broadcaster_id}&reward_id=#{reward_id}&id=#{redemption_id}&status=#{status.upcase}"
16
- response = patch_request(url, 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
@@ -1,18 +1,18 @@
1
1
  module Twitch
2
2
  class EmotesResource < Resource
3
3
  def channel(broadcaster_id:)
4
- response = get_request("chat/emotes?broadcaster_id=#{broadcaster_id}")
5
- Collection.from_response(response, type: Emote)
4
+ response = get_request("chat/emotes", params: { broadcaster_id: broadcaster_id })
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
- response = get_request("chat/emotes/set?emote_set_id=#{emote_set_id}")
15
- Collection.from_response(response, type: Emote)
14
+ response = get_request("chat/emotes/set", params: { emote_set_id: emote_set_id })
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,25 +2,21 @@ 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)
9
9
  attributes = { type: type, version: version, condition: condition, transport: transport }.merge(params)
10
- response = client.connection.post("eventsub/subscriptions", attributes)
11
- update_rate_limit(response)
12
-
13
- if response.status == 409
14
- raise Twitch::Errors::EventsubSubscriptionConflictError.new(
15
- response.body,
16
- response.status,
17
- existing_subscription_id: response.body.dig("data", 0, "id")
18
- )
19
- end
20
-
21
- return raise_error(response) if error?(response)
10
+ response = post_request("eventsub/subscriptions", body: attributes)
22
11
 
23
12
  EventsubSubscription.new(response.body.dig("data")[0]) if response.success?
13
+ rescue Twitch::Errors::ConflictError => error
14
+ existing_id = error.response_body.dig("data", 0, "id") if error.response_body.is_a?(Hash)
15
+ raise Twitch::Errors::EventsubSubscriptionConflictError.new(
16
+ error.response_body,
17
+ error.http_status_code,
18
+ existing_subscription_id: existing_id
19
+ )
24
20
  end
25
21
 
26
22
  def delete(id:)