twitchrb 1.9.0 → 1.10.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.
data/README.md CHANGED
@@ -26,6 +26,28 @@ An access token is required because the Helix API requires authentication.
26
26
  @client = Twitch::Client.new(client_id: "abc123", access_token: "xyz123")
27
27
  ```
28
28
 
29
+ #### User vs. App Access Tokens
30
+
31
+ Most endpoints accept a **user access token** — issued for a specific Twitch user via the
32
+ [authorization code](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow)
33
+ 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.).
35
+
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:
40
+
41
+ ```ruby
42
+ oauth = Twitch::OAuth.new(client_id: "abc123", client_secret: "your-client-secret")
43
+ token = oauth.create(grant_type: "client_credentials")
44
+
45
+ @app_client = Twitch::Client.new(client_id: "abc123", access_token: token.access_token)
46
+ ```
47
+
48
+ App tokens expire (typically after ~60 days). Use `oauth.validate(token: ...)` to check the remaining
49
+ lifetime, and `oauth.create(grant_type: "client_credentials")` to mint a fresh one when needed.
50
+
29
51
  ### Resources
30
52
 
31
53
  The gem maps as closely as we can to the Twitch API so you can easily convert API examples to gem code.
@@ -296,7 +318,7 @@ attributes = {title: "My new title"}
296
318
  ```ruby
297
319
  # Retrieves a list of clips
298
320
  # Available parameters: broadcaster_id or game_id
299
- @client.clips.list(user_id: 12345)
321
+ @client.clips.list(broadcaster_id: 12345)
300
322
  @client.clips.list(game_id: 12345)
301
323
 
302
324
  # Retrieves a clip by its ID.
@@ -305,7 +327,26 @@ attributes = {title: "My new title"}
305
327
 
306
328
  # Create a clip of a given Channel
307
329
  # Required scope: clips:edit
308
- @client.clips.create(broadcaster_id: 1234)
330
+ # title is optional
331
+ # duration is optional and can be from 5 to 60 seconds; defaults to 30 if not set
332
+ @client.clips.create(broadcaster_id: 1234, title: "Best moment", duration: 12.5)
333
+
334
+ # Create a clip from a VOD
335
+ # Required scope: editor:manage:clips or channel:manage:clips
336
+ # title is optional
337
+ # duration is optional and can be from 5 to 60 seconds; defaults to 30 if not set
338
+ @client.clips.create_from_vod(
339
+ editor_id: 1234,
340
+ broadcaster_id: 1234,
341
+ vod_id: 5678,
342
+ vod_offset: 45,
343
+ duration: 15.0,
344
+ title: "VOD highlight"
345
+ )
346
+
347
+ # Get download URLs for one or more clips
348
+ # Required scope: editor:manage:clips or channel:manage:clips
349
+ @client.clips.downloads(editor_id: 1234, broadcaster_id: 1234, clip_ids: ["clip-1", "clip-2"])
309
350
  ```
310
351
 
311
352
  ### Emotes
@@ -355,10 +396,11 @@ These require an application OAuth access token.
355
396
 
356
397
  ```ruby
357
398
  # Retrieves a list of EventSub Subscriptions
358
- # Available parameters: status, type, after
399
+ # Available parameters: status, type, user_id, after, conduit_id, subscription_id
359
400
  @client.eventsub_subscriptions.list
360
401
  @client.eventsub_subscriptions.list(status: "enabled")
361
402
  @client.eventsub_subscriptions.list(type: "channel.follow")
403
+ @client.eventsub_subscriptions.list(conduit_id: "conduit-id")
362
404
 
363
405
  # Create an EventSub Subscription
364
406
  @client.eventsub_subscriptions.create(
@@ -368,11 +410,45 @@ These require an application OAuth access token.
368
410
  transport: {method: "webhook", callback: "webhook_url", secret: "secret"}
369
411
  )
370
412
 
413
+ # If Twitch returns a 409 conflict for an already-existing subscription,
414
+ # the raised error exposes the existing subscription ID
415
+ begin
416
+ @client.eventsub_subscriptions.create(
417
+ type: "channel.follow",
418
+ version: 1,
419
+ condition: {broadcaster_user_id: 123},
420
+ transport: {method: "webhook", callback: "webhook_url", secret: "secret"}
421
+ )
422
+ rescue Twitch::Errors::EventsubSubscriptionConflictError => e
423
+ e.existing_subscription_id
424
+ end
425
+
426
+ # The generic EventSub API also supports beta subscription types such as
427
+ # channel.custom_power_up_redemption.add with version "beta"
428
+ @client.eventsub_subscriptions.create(
429
+ type: "channel.custom_power_up_redemption.add",
430
+ version: "beta",
431
+ condition: {broadcaster_user_id: 123},
432
+ transport: {method: "webhook", callback: "webhook_url", secret: "secret"}
433
+ )
434
+
371
435
  # Delete an EventSub Subscription
372
436
  # IDs are UUIDs
373
437
  @client.eventsub_subscriptions.delete(id: "abc12-abc12-abc12")
374
438
  ```
375
439
 
440
+ ### Custom Power-ups
441
+
442
+ ```ruby
443
+ # Get all custom Power-ups for a broadcaster
444
+ # Required scope: bits:read
445
+ # broadcaster_id must match the currently authenticated user
446
+ @client.custom_power_ups.list(broadcaster_id: 123)
447
+
448
+ # Filter custom Power-ups by ID
449
+ @client.custom_power_ups.list(broadcaster_id: 123, ids: ["power-up-1", "power-up-2"])
450
+ ```
451
+
376
452
  ### EventSub Conduits
377
453
 
378
454
  Conduits provide a way to receive events over multiple transports. These require an application OAuth access token.
@@ -449,6 +525,10 @@ shards = [
449
525
  # moderator_id can be either the currently authenticated moderator or the broadcaster
450
526
  # color can be either blue, green, orange, purple, primary. If left blank, primary is default
451
527
  @client.announcements.create broadcaster_id: 123, moderator_id: 123, message: "test message", color: "purple"
528
+
529
+ # When using an App Access Token during a shared chat session, set for_source_only: false
530
+ # to send the announcement to all channels in the session instead of only the source channel
531
+ @client.announcements.create broadcaster_id: 123, moderator_id: 123, message: "shared announcement", for_source_only: false
452
532
  ```
453
533
 
454
534
  ### Create a Shoutout
@@ -539,12 +619,40 @@ shards = [
539
619
  # reply_to is optional and is the UUID of the message to reply to
540
620
  @client.chat_messages.create broadcaster_id: 123, sender_id: 321, message: "A test message", reply_to: "aabbcc"
541
621
 
622
+ # When using an App Access Token during a shared chat session, control whether the message
623
+ # is only sent to the source channel or shared to all channels in the session
624
+ @client.chat_messages.create broadcaster_id: 123, sender_id: 321, message: "Shared chat message", for_source_only: false
625
+
626
+ # Optionally pin the message immediately after sending it
627
+ # Requires moderator:manage:chat_messages and the sender must be the broadcaster or a moderator
628
+ @client.chat_messages.create broadcaster_id: 123, sender_id: 321, message: "Read the rules", pin: true
629
+
542
630
  # Removes a single chat message from the broadcaster's chat room
543
631
  # Requires moderator:manage:chat_messages
544
632
  # moderator_id can be either the currently authenticated moderator or the broadcaster
545
633
  @client.chat_messages.delete broadcaster_id: 123, moderator_id: 123, message_id: "abc123-abc123"
546
634
  ```
547
635
 
636
+ ### Pinned Chat Messages
637
+
638
+ ```ruby
639
+ # Get the currently pinned chat message
640
+ # Requires moderator:read:chat_messages or moderator:manage:chat_messages
641
+ @client.pinned_chat_messages.retrieve broadcaster_id: 123, moderator_id: 321
642
+
643
+ # Pin a message
644
+ # Requires moderator:manage:chat_messages
645
+ @client.pinned_chat_messages.create broadcaster_id: 123, moderator_id: 321, message_id: "abc123-abc123", duration_seconds: 300
646
+
647
+ # Update a pinned message's duration
648
+ # Requires moderator:manage:chat_messages
649
+ @client.pinned_chat_messages.update broadcaster_id: 123, moderator_id: 321, message_id: "abc123-abc123", duration_seconds: 120
650
+
651
+ # Unpin a message
652
+ # Requires moderator:manage:chat_messages
653
+ @client.pinned_chat_messages.delete broadcaster_id: 123, moderator_id: 321, message_id: "abc123-abc123"
654
+ ```
655
+
548
656
  ### Whispers
549
657
 
550
658
  ```ruby
@@ -638,6 +746,13 @@ messages = [{msg_id: "abc1", msg_text: "is this allowed?"}, {msg_id: "abc2", msg
638
746
  @client.chatters.list broadcaster_id: 123, moderator_id: 123
639
747
  ```
640
748
 
749
+ ### Shared Chat Sessions
750
+
751
+ ```ruby
752
+ # Get the active shared chat session for a broadcaster
753
+ @client.shared_chat_sessions.retrieve broadcaster_id: 123
754
+ ```
755
+
641
756
  ### Channel Points Custom Rewards
642
757
 
643
758
  ```ruby
@@ -700,6 +815,20 @@ messages = [{msg_id: "abc1", msg_text: "is this allowed?"}, {msg_id: "abc2", msg
700
815
  @client.warnings.create broadcaster_id: 123, moderator_id: 123, user_id: 321, reason: "dont do that"
701
816
  ```
702
817
 
818
+ ### Suspicious Users
819
+
820
+ ```ruby
821
+ # Add suspicious status to a chat user
822
+ # Required scope: moderator:manage:suspicious_users
823
+ # moderator_id must match the currently authenticated moderator
824
+ @client.suspicious_users.create broadcaster_id: 123, moderator_id: 321, user_id: 456, status: "RESTRICTED"
825
+
826
+ # Remove suspicious status from a chat user
827
+ # Required scope: moderator:manage:suspicious_users
828
+ # moderator_id must match the currently authenticated moderator
829
+ @client.suspicious_users.delete broadcaster_id: 123, moderator_id: 321, user_id: 456
830
+ ```
831
+
703
832
  ### Streams
704
833
 
705
834
  ```ruby
@@ -856,13 +985,13 @@ tag_ids = ["tag-id-1", "tag-id-2"]
856
985
  @client.tags.replace(broadcaster_id: 123, tag_ids: tag_ids)
857
986
  ```
858
987
 
859
- ### Hype Train Events
988
+ ### Hype Train Status
860
989
 
861
990
  ```ruby
862
- # Get hype train events for a broadcaster
991
+ # Get hype train status for a broadcaster
863
992
  # Required scope: channel:read:hype_train
864
993
  # broadcaster_id must match the currently authenticated user
865
- @client.hype_train_events.list(broadcaster_id: 123)
994
+ @client.hype_train_status.retrieve(broadcaster_id: 123)
866
995
  ```
867
996
 
868
997
  ### Moderator Events
data/bin/smoke ADDED
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env ruby
2
+ # Hits the real Twitch API to verify the gem still matches what Twitch returns.
3
+ # Run before releases, or on a schedule. Unit tests use WebMock and won't catch
4
+ # Twitch-side schema drift; this will.
5
+ #
6
+ # Required env (load via .env):
7
+ # TWITCH_CLIENT_ID
8
+ # TWITCH_ACCESS_TOKEN # user token
9
+ # TWITCH_CLIENT_SECRET # only needed for app-token checks
10
+ #
11
+ # Optional env — enables write checks that actually send messages:
12
+ # TWITCH_SMOKE_BROADCASTER_ID # channel to post into
13
+ # TWITCH_SMOKE_MODERATOR_ID # must match the user/bot in the token
14
+ # # (also used as sender_id for chat messages)
15
+ #
16
+ # Usage:
17
+ # bin/smoke # run all checks
18
+ # bin/smoke users games # run only the named checks
19
+
20
+ require "bundler/setup"
21
+ require "twitch"
22
+ require "dotenv/load"
23
+
24
+ unless ENV["TWITCH_CLIENT_ID"] && ENV["TWITCH_ACCESS_TOKEN"]
25
+ abort "Missing TWITCH_CLIENT_ID / TWITCH_ACCESS_TOKEN — set them in .env"
26
+ end
27
+
28
+ client = Twitch::Client.new(
29
+ client_id: ENV["TWITCH_CLIENT_ID"],
30
+ access_token: ENV["TWITCH_ACCESS_TOKEN"]
31
+ )
32
+
33
+ app_client = nil
34
+ if ENV["TWITCH_CLIENT_SECRET"]
35
+ oauth = Twitch::OAuth.new(
36
+ client_id: ENV["TWITCH_CLIENT_ID"],
37
+ client_secret: ENV["TWITCH_CLIENT_SECRET"]
38
+ )
39
+ token = oauth.create(grant_type: "client_credentials")
40
+ app_client = Twitch::Client.new(client_id: ENV["TWITCH_CLIENT_ID"], access_token: token.access_token) if token
41
+ end
42
+
43
+ broadcaster_id = ENV["TWITCH_SMOKE_BROADCASTER_ID"]
44
+ moderator_id = ENV["TWITCH_SMOKE_MODERATOR_ID"]
45
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
46
+
47
+ CHECKS = {
48
+ "users" => -> { client.users.retrieve(username: "twitchdev") },
49
+ "users_many" => -> { client.users.retrieve(usernames: [ "twitchdev", "twitch" ]) },
50
+ "channels" => -> { client.channels.retrieve(id: 141981764) },
51
+ "games" => -> { client.games.retrieve(name: "Just Chatting") },
52
+ "games_top" => -> { client.games.top(first: 5) },
53
+ "streams" => -> { client.streams.list(first: 5) },
54
+ "clips" => -> { client.clips.list(broadcaster_id: "141981764", first: 3) },
55
+ "videos" => -> { client.videos.list(user_id: "141981764", first: 3) },
56
+ "badges_global" => -> { client.badges.global },
57
+ "badges_channel" => -> { client.badges.channel(broadcaster_id: "141981764") },
58
+ "emotes_global" => -> { client.emotes.global },
59
+ "emotes_channel" => -> { client.emotes.channel(broadcaster_id: "141981764") },
60
+ "eventsub_list" => -> { client.eventsub_subscriptions.list },
61
+
62
+ # app-token only
63
+ "conduits_list" => -> { app_client && app_client.eventsub_conduits.list },
64
+ "app_subs_list" => -> { app_client && app_client.eventsub_subscriptions.list },
65
+
66
+ # write checks — only run when broadcaster + moderator ids are set
67
+ "chat_message" => -> {
68
+ client.chat_messages.create(
69
+ broadcaster_id: broadcaster_id,
70
+ sender_id: moderator_id,
71
+ message: "twitchrb smoke test #{timestamp}"
72
+ )
73
+ },
74
+ "announcement" => -> {
75
+ client.announcements.create(
76
+ broadcaster_id: broadcaster_id,
77
+ moderator_id: moderator_id,
78
+ message: "twitchrb smoke test announcement #{timestamp}",
79
+ color: "primary"
80
+ )
81
+ }
82
+ }.freeze
83
+
84
+ requested = ARGV.empty? ? CHECKS.keys : ARGV
85
+ unknown = requested - CHECKS.keys
86
+ abort "Unknown check(s): #{unknown.join(", ")}\nAvailable: #{CHECKS.keys.join(", ")}" if unknown.any?
87
+
88
+ failures = 0
89
+ write_checks = %w[chat_message announcement]
90
+
91
+ requested.each do |name|
92
+ if name.start_with?("conduits", "app_subs") && app_client.nil?
93
+ puts "- #{name.ljust(18)} skipped (no TWITCH_CLIENT_SECRET)"
94
+ next
95
+ end
96
+
97
+ if write_checks.include?(name) && (broadcaster_id.nil? || moderator_id.nil?)
98
+ puts "- #{name.ljust(18)} skipped (set TWITCH_SMOKE_BROADCASTER_ID + TWITCH_SMOKE_MODERATOR_ID)"
99
+ next
100
+ end
101
+
102
+ begin
103
+ result = CHECKS[name].call
104
+ count = result.respond_to?(:data) ? result.data.size : 1
105
+ puts "✓ #{name.ljust(18)} (#{count} record#{"s" unless count == 1})"
106
+ rescue => e
107
+ failures += 1
108
+ puts "✗ #{name.ljust(18)} #{e.class}: #{e.message}"
109
+ end
110
+ end
111
+
112
+ puts
113
+ if failures.zero?
114
+ puts "All checks passed."
115
+ else
116
+ puts "#{failures} check(s) failed."
117
+ exit 1
118
+ end
data/lib/twitch/client.rb CHANGED
@@ -114,6 +114,10 @@ module Twitch
114
114
  CustomRewardRedemptionsResource.new(self)
115
115
  end
116
116
 
117
+ def custom_power_ups
118
+ CustomPowerUpsResource.new(self)
119
+ end
120
+
117
121
  def goals
118
122
  GoalsResource.new(self)
119
123
  end
@@ -122,6 +126,10 @@ module Twitch
122
126
  HypeTrainEventsResource.new(self)
123
127
  end
124
128
 
129
+ def hype_train_status
130
+ HypeTrainStatusResource.new(self)
131
+ end
132
+
125
133
  def announcements
126
134
  AnnouncementsResource.new(self)
127
135
  end
@@ -134,6 +142,10 @@ module Twitch
134
142
  ChatMessagesResource.new(self)
135
143
  end
136
144
 
145
+ def pinned_chat_messages
146
+ PinnedChatMessagesResource.new(self)
147
+ end
148
+
137
149
  def vips
138
150
  VipsResource.new(self)
139
151
  end
@@ -162,6 +174,10 @@ module Twitch
162
174
  ShoutoutsResource.new(self)
163
175
  end
164
176
 
177
+ def shared_chat_sessions
178
+ SharedChatSessionsResource.new(self)
179
+ end
180
+
165
181
  def unban_requests
166
182
  UnbanRequestsResource.new(self)
167
183
  end
@@ -170,6 +186,10 @@ module Twitch
170
186
  WarningsResource.new(self)
171
187
  end
172
188
 
189
+ def suspicious_users
190
+ SuspiciousUsersResource.new(self)
191
+ end
192
+
173
193
  def connection
174
194
  @connection ||= Faraday.new(BASE_URL) do |conn|
175
195
  conn.request :authorization, :Bearer, access_token
data/lib/twitch/error.rb CHANGED
@@ -1,4 +1,7 @@
1
1
  module Twitch
2
2
  class Error < StandardError
3
3
  end
4
+
5
+ class UnsafeRequestPathError < ArgumentError
6
+ end
4
7
  end
@@ -88,6 +88,15 @@ module Twitch
88
88
  end
89
89
  end
90
90
 
91
+ class EventsubSubscriptionConflictError < ConflictError
92
+ attr_reader :existing_subscription_id
93
+
94
+ def initialize(response_body, http_status_code, existing_subscription_id: nil)
95
+ @existing_subscription_id = existing_subscription_id
96
+ super(response_body, http_status_code)
97
+ end
98
+ end
99
+
91
100
  class TooManyRequestsError < ErrorGenerator
92
101
  private
93
102
 
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class ClipDownload < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class CustomPowerUp < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class HypeTrainStatus < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class PinnedChatMessage < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class SharedChatSession < Object
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Twitch
2
+ class SuspiciousUser < Object
3
+ end
4
+ end
@@ -9,22 +9,27 @@ module Twitch
9
9
  private
10
10
 
11
11
  def get_request(url, params: {}, headers: {})
12
+ validate_request_path!(url)
12
13
  handle_response client.connection.get(url, params, headers)
13
14
  end
14
15
 
15
16
  def post_request(url, body:, headers: {})
17
+ validate_request_path!(url)
16
18
  handle_response client.connection.post(url, body, headers)
17
19
  end
18
20
 
19
21
  def patch_request(url, body:, headers: {})
22
+ validate_request_path!(url)
20
23
  handle_response client.connection.patch(url, body, headers)
21
24
  end
22
25
 
23
26
  def put_request(url, body:, headers: {})
27
+ validate_request_path!(url)
24
28
  handle_response client.connection.put(url, body, headers)
25
29
  end
26
30
 
27
31
  def delete_request(url, params: {}, headers: {})
32
+ validate_request_path!(url)
28
33
  handle_response client.connection.delete(url, params, headers)
29
34
  end
30
35
 
@@ -71,5 +76,13 @@ module Twitch
71
76
  )
72
77
  raise error
73
78
  end
79
+
80
+ def validate_request_path!(url)
81
+ return unless url.start_with?("//") || URI(url).absolute?
82
+
83
+ raise Twitch::UnsafeRequestPathError, "request path must be relative to the Twitch API base URL"
84
+ rescue URI::InvalidURIError
85
+ nil
86
+ end
74
87
  end
75
88
  end
@@ -2,8 +2,8 @@ module Twitch
2
2
  class AnnouncementsResource < Resource
3
3
  # Moderator ID must match the user in the OAuth token
4
4
  # Required scope: moderator:manage:announcements
5
- def create(broadcaster_id:, moderator_id:, message:, color: nil)
6
- attrs = { message: message, color: color }
5
+ def create(broadcaster_id:, moderator_id:, message:, color: nil, for_source_only: nil)
6
+ attrs = { message: message, color: color, for_source_only: for_source_only }
7
7
 
8
8
  post_request("chat/announcements?broadcaster_id=#{broadcaster_id}&moderator_id=#{moderator_id}", body: attrs)
9
9
  end
@@ -1,7 +1,14 @@
1
1
  module Twitch
2
2
  class ChatMessagesResource < Resource
3
- def create(broadcaster_id:, sender_id:, message:, reply_to: nil)
4
- attrs = { broadcaster_id: broadcaster_id, sender_id: sender_id, message: message, reply_parent_message_id: reply_to }
3
+ def create(broadcaster_id:, sender_id:, message:, reply_to: nil, pin: nil, for_source_only: nil)
4
+ attrs = {
5
+ broadcaster_id: broadcaster_id,
6
+ sender_id: sender_id,
7
+ message: message,
8
+ reply_parent_message_id: reply_to,
9
+ pin: pin,
10
+ for_source_only: for_source_only
11
+ }.compact
5
12
 
6
13
  response = post_request("chat/messages", body: attrs)
7
14
  ChatMessage.new(response.body.dig("data")[0])
@@ -13,9 +13,37 @@ module Twitch
13
13
 
14
14
  # Required scope: clips:edit
15
15
  def create(broadcaster_id:, **attributes)
16
- response = post_request("clips", body: attributes.merge(broadcaster_id: broadcaster_id))
16
+ response = post_request(query_path("clips", attributes.merge(broadcaster_id: broadcaster_id)), body: {})
17
17
 
18
18
  Clip.new(response.body.dig("data")[0]) if response.success?
19
19
  end
20
+
21
+ # Required scope: editor:manage:clips or channel:manage:clips
22
+ def create_from_vod(editor_id:, broadcaster_id:, vod_id:, vod_offset:, **attributes)
23
+ params = attributes.merge(
24
+ editor_id: editor_id,
25
+ broadcaster_id: broadcaster_id,
26
+ vod_id: vod_id,
27
+ vod_offset: vod_offset
28
+ )
29
+
30
+ response = post_request(query_path("videos/clips", params), body: {})
31
+
32
+ Clip.new(response.body.dig("data")[0]) if response.success?
33
+ end
34
+
35
+ # Required scope: editor:manage:clips or channel:manage:clips
36
+ def downloads(editor_id:, broadcaster_id:, clip_id: nil, clip_ids: nil)
37
+ ids = clip_ids || Array(clip_id)
38
+ response = get_request("clips/downloads", params: { editor_id: editor_id, broadcaster_id: broadcaster_id, clip_id: ids })
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)}"
47
+ end
20
48
  end
21
49
  end
@@ -0,0 +1,13 @@
1
+ module Twitch
2
+ class CustomPowerUpsResource < Resource
3
+ # Required scope: bits:read
4
+ # broadcaster_id must match the currently authenticated user
5
+ def list(broadcaster_id:, ids: nil)
6
+ params = { broadcaster_id: broadcaster_id }
7
+ params[:id] = ids if ids
8
+
9
+ response = get_request("bits/custom_power_ups", params: params)
10
+ Collection.from_response(response, type: CustomPowerUp)
11
+ end
12
+ end
13
+ end
@@ -7,7 +7,18 @@ module Twitch
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 = post_request("eventsub/subscriptions", body: attributes)
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)
11
22
 
12
23
  EventsubSubscription.new(response.body.dig("data")[0]) if response.success?
13
24
  end
@@ -3,8 +3,8 @@ module Twitch
3
3
  # Required scope: channel:read:hype_train
4
4
  # Broadcaster ID must match the user in the OAuth token
5
5
  def list(broadcaster_id:)
6
- response = get_request("hypetrain/events", params: { broadcaster_id: broadcaster_id })
7
- Collection.from_response(response, type: HypeTrainEvent)
6
+ warn "`hype_train_events.list` is deprecated because Twitch removed GET /helix/hypetrain/events. Use `hype_train_status.retrieve` instead."
7
+ HypeTrainStatusResource.new(client).retrieve(broadcaster_id: broadcaster_id)
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,14 @@
1
+ module Twitch
2
+ class HypeTrainStatusResource < Resource
3
+ # Required scope: channel:read:hype_train
4
+ # Broadcaster ID must match the user in the OAuth token
5
+ def retrieve(broadcaster_id:)
6
+ response = get_request("hypetrain/status", params: { broadcaster_id: broadcaster_id })
7
+ data = response.body.dig("data")
8
+
9
+ return nil if data.nil? || data.empty?
10
+
11
+ HypeTrainStatus.new(data[0])
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ module Twitch
2
+ class PinnedChatMessagesResource < Resource
3
+ def retrieve(broadcaster_id:, moderator_id:)
4
+ response = get_request("chat/pins", params: { broadcaster_id: broadcaster_id, moderator_id: moderator_id })
5
+ data = response.body.dig("data")
6
+
7
+ return nil if data.nil? || data.empty?
8
+
9
+ PinnedChatMessage.new(data[0])
10
+ end
11
+
12
+ # moderator_id must match the user in the OAuth token
13
+ def create(broadcaster_id:, moderator_id:, message_id:, duration_seconds: nil)
14
+ put_request(query_path(broadcaster_id:, moderator_id:, message_id:, duration_seconds:), body: {})
15
+ end
16
+
17
+ # moderator_id must match the user in the OAuth token
18
+ def update(broadcaster_id:, moderator_id:, message_id:, duration_seconds: nil)
19
+ patch_request(query_path(broadcaster_id:, moderator_id:, message_id:, duration_seconds:), body: {})
20
+ end
21
+
22
+ # moderator_id must match the user in the OAuth token
23
+ def delete(broadcaster_id:, moderator_id:, message_id:)
24
+ delete_request("chat/pins", params: { broadcaster_id: broadcaster_id, moderator_id: moderator_id, message_id: message_id })
25
+ end
26
+
27
+ private
28
+
29
+ def query_path(broadcaster_id:, moderator_id:, message_id:, duration_seconds:)
30
+ params = {
31
+ broadcaster_id: broadcaster_id,
32
+ moderator_id: moderator_id,
33
+ message_id: message_id,
34
+ duration_seconds: duration_seconds
35
+ }.compact
36
+
37
+ "chat/pins?#{URI.encode_www_form(params)}"
38
+ end
39
+ end
40
+ end