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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +3 -6
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +237 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +69 -34
- data/README.md +135 -6
- data/bin/smoke +118 -0
- data/lib/twitch/client.rb +20 -0
- data/lib/twitch/error.rb +3 -0
- data/lib/twitch/error_generator.rb +9 -0
- data/lib/twitch/objects/clip_download.rb +4 -0
- data/lib/twitch/objects/custom_power_up.rb +4 -0
- data/lib/twitch/objects/hype_train_status.rb +4 -0
- data/lib/twitch/objects/pinned_chat_message.rb +4 -0
- data/lib/twitch/objects/shared_chat_session.rb +4 -0
- data/lib/twitch/objects/suspicious_user.rb +4 -0
- data/lib/twitch/resource.rb +13 -0
- data/lib/twitch/resources/announcements.rb +2 -2
- data/lib/twitch/resources/chat_messages.rb +9 -2
- data/lib/twitch/resources/clips.rb +29 -1
- data/lib/twitch/resources/custom_power_ups.rb +13 -0
- data/lib/twitch/resources/eventsub_subscriptions.rb +12 -1
- data/lib/twitch/resources/hype_train_events.rb +2 -2
- data/lib/twitch/resources/hype_train_status.rb +14 -0
- data/lib/twitch/resources/pinned_chat_messages.rb +40 -0
- data/lib/twitch/resources/shared_chat_sessions.rb +12 -0
- data/lib/twitch/resources/suspicious_users.rb +25 -0
- data/lib/twitch/version.rb +1 -1
- data/lib/twitch.rb +12 -0
- data/mise.toml +1 -1
- data/twitchrb.gemspec +2 -2
- metadata +26 -6
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(
|
|
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
|
-
|
|
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
|
|
988
|
+
### Hype Train Status
|
|
860
989
|
|
|
861
990
|
```ruby
|
|
862
|
-
# Get hype train
|
|
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.
|
|
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
|
@@ -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
|
|
data/lib/twitch/resource.rb
CHANGED
|
@@ -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 = {
|
|
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",
|
|
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 =
|
|
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
|
-
|
|
7
|
-
|
|
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
|