nicklaw5 / helix Goto Github PK
View Code? Open in Web Editor NEWA Twitch Helix API client written in Go.
License: MIT License
A Twitch Helix API client written in Go.
License: MIT License
Webhook support was added in #22, however no documentation on how to use these features was introduced.
This is because the vod_offset is added to the GetClip Api response.
I have an app that runs while I'm streaming, using this library for Twitch integration (thanks!). I'd like to check if the access token is valid before making API calls, since it's quite likely that my token may expire so I can use the refresh token to update it in that case.
How can I get my current access token from the client in order to check if it is valid? Or is there a way to ask the method itself to use the current token rather than a provided one? Is this a feature that would be useful to others?
Is there reason we use context on client struct instead of methods?
I propose rewrite it for next major if there no special things behind this.
I'm familar with go and can do PR.
Twitch Supports OIDC Authorization with custom claims
https://dev.twitch.tv/docs/authentication/getting-tokens-oidc/
It would be nice to support the OIDC auth method.
I suggest to implement https://github.com/coreos/go-oidc, to instead do :
MISC:
there are multiple new api endpoints
for example moderation ban/unban user
or
moderation add/remove blocked term
also get banned users missing some fields in response (reason)
We should update the library to automatically grab oauth tokens. Just ran into this
Error:Unauthorized ErrorStatus:401 ErrorMessage:OAuth token is missing
Twitch didn't document this properly in their docs, but it is possible to specify multiple broadcaster_id
parameters (up to 100) and you will get back multiple objects for these, quite similarly to how it works in GetClips
function.
I would need to fetch channel information for ~400 channels and doing that in bulks of 100 seems way more efficient than making separate requests for each of these, but I can't find a way to specify more than 1 BroadcasterID
in GetChannelInformationParams
.
I had an idea to change GetChannelInformationParams.BroadcasterID string
to GetChannelInformationParams.BroadcasterIDs []string
(also just like it's done in the GetClips
function), but this would be a breaking change and existing applications would need to update, would you be okay with me PRing this change or should a different solution be considered instead?
Update to the above: I just decided to open a PR for that.
Hey Hey,
is it a Bug, that you return everytime a nil as error. It will be a fatal NullPointer exception and the catch of errors will be triggert.
Greeting
Marcel
Line 110 in a74e7ff
Steps to reproduce:
What happens:
It gets into an endless loop calling the function passed to OnUserAccessTokenRefreshed.
What should happen:
It should be able to distiguish between unauthorized because of missing scopes and unauthorized because of an expired access token.
if r, err := api.GetBlockedTerms(&helix.BlockedTermsParams{
BroadcasterID: channelID,
ModeratorID: selfID,
First: 10,
}); err != nil {
panic(err)
} else {
fmt.Printf("%+v\n", r)
}
&{ResponseCommon:{StatusCode:400 Header:map[Content-Length:[92] Content-Type:[application/json; charset=utf-8] Date:[Sat, 30 Dec 2023 13:22:47 GMT] Strict-Transport-Security:[max-age=300] Timing-Allow-Origin:[https://www.twitch.tv] Vary:[Accept-Encoding, Origin] X-Cache:[MISS, MISS] X-Cache-Hits:[0, 0] X-Served-By:[cache-bfi-kbfi7400051-BFI, cache-mxp6971-MXP] X-Timer:[S1703942568.737446,VS0,VS0,VE174]] Error:Bad Request ErrorStatus:400 ErrorMessage:Missing required parameter "moderator_id"} Data:{Terms:[] Pagination:{Cursor:}}}
Allow users to pass in their own logger.
As of February 28, 2023 the attribute tag_ids
attribute is deprecated. In its place, the attribute tags
should be used. See the documentation for the Get Streams endpoint.
The list of endpoint that are supported needs updating.
The following API endpoints have been added to Twitch's Helix API docs, but have yet to be implemented in this package:
Any help implementing these endpoints is very much appreciated. All PRs should be accompanied by tests.
It appears Twitch has added both from_name
and to_name
as part of the response the Get User Follows API endpoint.
I ran into an issue where I was calling GetUsers(...)
and twitch was returning an error, but the helix library returned an empty error, so I didn't handle it right away.
In my program, I'm wrapping all error responses before I check for an error
func GetHelixError(err error, resp helix.ResponseCommon) error {
if resp.ErrorStatus == 0 && err == nil {
return nil
}
if err != nil {
return err
}
return fmt.Errorf("error %s with status %d: %s", resp.Error, resp.ErrorStatus, resp.ErrorMessage)
}
// Example call
userResp, err := tokenClient.GetUsers(&helix.UsersParams{Logins: []string{strings.ToLower(botName)}})
err = service.GetHelixError(err, userResp.ResponseCommon)
if err != nil {
return fmt.Errorf("could not get user info for bot %s: %w", botName, err)
}
Is the expected way to always check resp.ErrorStatus for it being set. This feels more a limitation of twitch not using proper status codes... but it's what we have to work with.
One thing that could be done, but it would require a lot of refactoring, is add a wrap function. Wrapping it on TwitchResponseError
would allow a check with errors.Is(err, helix.TwitchResponseError)
var TwitchResponseError = errors.New("twitch returned an error")
func pullErrorFromResponse(resp ResponseCommon) error {
if resp.ErrorStatus == 0 {
return nil
}
return fmt.Errorf("error %s occured with status %d and message %s: %w", resp.Error, resp.ErrorStatus, resp.ErrorMessage, err)
}
// elsewhere
func (c *Client) GetStreams(params *StreamsParams) (*StreamsResponse, error) {
resp, err := c.get("/streams", &ManyStreams{}, params)
if err != nil {
return nil, err
}
streams := &StreamsResponse{}
resp.HydrateResponseCommon(&streams.ResponseCommon)
streams.Data.Streams = resp.Data.(*ManyStreams).Streams
streams.Data.Pagination = resp.Data.(*ManyStreams).Pagination
// refactor is here \/
return streams, pullErrorFromResponse(resp.ResponseCommon)
}
It was brought to my attention by @jscoys via email communication that we don't provide access to the
WWW-Authenticate
response header. This response header is sent when an access token has expired and needs to be refreshed. See the Twitch docs for information.
Rather than limiting what headers are providing with each response from the client, it may be a better idea to provide all headers that are returned in a response as a slice.
For example:
type Response struct {
ResponseCommon
Data interface{}
}
type ResponseCommon struct {
StatusCode int
Error string
ErrorStatus int
ErrorMessage string
Headers map[string]string
}
https://discuss.dev.twitch.tv/t/requiring-oauth-for-helix-twitch-api-endpoints/23916
Can we add the app access token easily to the headers?
Hi,
EventSubChannelPredictionEndEvent has a typo in json marshaller :
EndedAt Time `json:"eneded_at"`
Thanks,
Add support for all Helix API endpoints as documented here.
Since Twitch is going to require OAuth Authentication starting from 1st of May 2020 (More here) it would be great to add an Authentication example in order to speed up initial configuration using this API wrapper.
It seems that some of the API endpoint have changed since their original inception in this package. In most circumstances, either additional request parameters have been added, or additional response parameters have been added on Twitch's end.
For example, the Get Clips endpoint has the following request parameters: id
, broadcaster_id
, game_id
, after
, before
, ended_at
, started_at
and first
. Of which only id
is catered for in this package as it currently stands.
We need to review all implemented endpoints to verify they are to current spec.
this does return no data with no error
&{{200 map[Content-Length:[27] Content-Type:[application/json; charset=utf-8] Date:[Mon, 19 Feb 2024 10:07:10 GMT] Ratelimit-Limit:[800] Ratelimit-Remaining:[799] Ratelimit-Reset:[1708337231] Strict-Transport-Security:[max-age=300] Timing-Allow-Origin:[https://www.twitch.tv] Vary:[Accept-Encoding, Origin] X-Cache:[MISS, MISS] X-Cache-Hits:[0, 0] X-Served-By:[cache-bfi-kbfi7400061-BFI, cache-mxp6943-MXP] X-Timer:[S1708337230.992733,VS0,VS0,VE167]] 0 } {[] {}}}
code to test
just check if you get the correct scopes
package main
import (
"fmt"
"io"
"net/http"
"time"
"github.com/nicklaw5/helix/v2"
)
const (
clientid string = ""
usertoken string = ""
)
func main() {
api, err := helix.NewClient(&helix.Options{
ClientID: clientid,
UserAccessToken: usertoken,
RateLimitFunc: func(r *helix.Response) error {
if r.GetRateLimitRemaining() > 0 {
return nil
}
var reset int64 = int64(r.GetRateLimitReset())
var now int64 = time.Now().Unix()
if now < reset {
var sleep time.Duration = time.Duration(reset - now)
if sleep > 0 {
time.Sleep(sleep * time.Second)
}
}
return nil
},
})
if err != nil {
panic(err)
}
valid, self, err := api.ValidateToken(api.GetUserAccessToken())
if err != nil {
panic(err)
} else if !valid {
panic("user access token not valid")
}
fmt.Println(self.Data.Scopes)
bans, err := api.GetBannedUsers(&helix.BannedUsersParams{
BroadcasterID: self.Data.UserID,
UserID: self.Data.UserID,
})
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", bans)
body, err := httpGet(fmt.Sprintf("https://api.twitch.tv/helix/moderation/banned?first=%d&broadcaster_id=%s", 100, self.Data.UserID), map[string]string{
"Client-Id": self.Data.ClientID,
"Authorization": "Bearer " + api.GetUserAccessToken(),
})
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
func httpGet(link string, header map[string]string) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, link, nil)
if err != nil {
return nil, err
}
for k, v := range header {
req.Header.Add(k, v)
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return body, nil
}
Currently in the documentation, a proposed rate limit function is provided as such:
func rateLimitCallback(lastResponse *helix.Response) error {
if lastResponse.GetRateLimitRemaining() > 0 {
return nil
}
var reset64 int64
reset64 = int64(lastResponse.GetRateLimitReset())
currentTime := time.Now().Unix()
if currentTime < reset64 {
timeDiff := time.Duration(reset64 - currentTime)
if timeDiff > 0 {
fmt.Printf("Waiting on rate limit to pass before sending next request (%d seconds)\n", timeDiff)
time.Sleep(timeDiff * time.Second)
}
}
return nil
}
I propose adding this function, sans the print statement, to the core helix library. The user would still have to specify the use of this function, but it would simplify the usage of this library and reduce boiler plate for the end user. As such, a client might instantiate the client like so:
client, err := helix.NewClient(&helix.Options{
ClientID: "your-client-id",
RateLimitFunc: helix.DefaultRateLimit,
})
where helix.DefaultRateLimit
is the example function above.
Thoughts?
(Reposting because may be urgent). Hey, this doesn't seem to work as you say. I have this code:
if time.Now().After(authExpiry) {
authExpiry = time.Now()
res, err := twitch.GetAppAccessToken()
exitIfError(err)
authExpiry = authExpiry.Add(time.Duration(res.Data.ExpiresIn) * time.Second)
log.Insta <- fmt.Sprintf("< | token generated: %s (for %ds)", res.Data.AccessToken, res.Data.ExpiresIn)
}
res, err := twitch.GetStreams(&getStreamsParams)
if err == nil && res.StatusCode != 200 { // reinterpret HTTP error as actual error
err = fmt.Errorf("HTTP %d: %s", res.StatusCode, res.ErrorMessage)
}
if err != nil {
log.Insta <- fmt.Sprintf("x | < : %s", err)
}
and my program output
< | token generated: pfzn88vavmt9hlgy6c33aonf2imako (for 5444223s)
x | < : HTTP 401: OAuth token is missing
during the migration test window that just ended (is back to normal now). Any ideas?
Originally posted by @pyorot in #42 (comment)
r, err := api.AddBlockedTerm(&helix.AddBlockedTermParams{
BroadcasterID: self.Data.UserID,
ModeratorID: self.Data.UserID,
Text: "stream-rise",
})
if err != nil {
panic(err)
}
log.Printf("%+v\n", r)
&{ResponseCommon:{StatusCode:400 Header:map[Content-Length:[92] Content-Type:[application/json; charset=utf-8] Date:[Sat, 30 Dec 2023 13:32:14 GMT] Strict-Transport-Security:[max-age=300] Timing-Allow-Origin:[https://www.twitch.tv] Vary:[Accept-Encoding, Origin] X-Cache:[MISS, MISS] X-Cache-Hits:[0, 0] X-Served-By:[cache-bfi-kbfi7400075-BFI, cache-mxp6937-MXP] X-Timer:[S1703943134.032044,VS0,VS0,VE164]] Error:Bad Request ErrorStatus:400 ErrorMessage:Missing required parameter "moderator_id"} Data:{Terms:[]}}
For one of my projects I would have to change the APIBaseURL
. Unfortunately, this is not possible because it is a constant. And I haven't found any other way to achieve this behavior.
I would appreciate an option for this in the Options
struct
Kind regards David
Twitch are deprecating their websub-based webhook functionality, in favour of their EventSub API.
need to work towards refactoring webhooks.go to support EventSub
When creating a client with both an AppAccessToken
and a UserAccessToken
, the client favors the UserAccessToken
over the AppAccessToken
. However, certain Helix API calls require the use of an AppAccessToken
, i.e. CreateEventSubSubscription
.
The client should automatically use the AppAccessToken field for calls that require AppAccessToken, or otherwise allow AppAccessToken to be passed in per call as in #128.
Old follow endpoints not working https://discuss.dev.twitch.tv/t/legacy-follows-api-and-eventsub-shutdown-timeline-updated/46769
I'll try do PR.
Hi,
Maybe I’m not using it correctly, but I’m getting an app token (I checked), then I pass it to my client and then I’m calling the GetStream endpoint the same way that in the documentation. The weird thing is that I’m getting a rate limit of 30requests per second instead of 120.
When I try to use GetClips with a specified start time and end time, I get a segfault:
startTime := time.Now().Add(time.Duration(-1) * time.Hour)
endTime := time.Now()
resp, err := twitchClient.GetClips(&helix.ClipsParams {
BroadcasterID: accountID,
First: 5,
StartedAt: helix.Time{ Time: startTime },
EndedAt: helix.Time{ Time: endTime },
})
The same code works if I remove StartedAt
and EndedAt
.
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x98 pc=0x67753f]
goroutine 1 [running]:
github.com/nicklaw5/helix.(*Client).getBaseURL(0x94f500, 0x7061c4, 0x6, 0x6bf460, 0xc000169d90)
/home/fish/go/pkg/mod/github.com/nicklaw5/[email protected]/helix.go:307 +0xff
github.com/nicklaw5/helix.(*Client).newRequest(0x94f500, 0x705c51, 0x3, 0x7061c4, 0x6, 0x695bc0, 0xc000102e70, 0x40e100, 0x20, 0x6bf460, ...)
/home/fish/go/pkg/mod/github.com/nicklaw5/[email protected]/helix.go:253 +0x46
github.com/nicklaw5/helix.(*Client).sendRequest(0x94f500, 0x705c51, 0x3, 0x7061c4, 0x6, 0x695a80, 0xc0001103a0, 0x695bc0, 0xc000102e70, 0x40e100,
...)
/home/fish/go/pkg/mod/github.com/nicklaw5/[email protected]/helix.go:158 +0xca
github.com/nicklaw5/helix.(*Client).get(...)
/home/fish/go/pkg/mod/github.com/nicklaw5/[email protected]/helix.go:133
github.com/nicklaw5/helix.(*Client).GetUsers(0x94f500, 0xc000102e70, 0x2a, 0xc000169ed8, 0x40e198)
/home/fish/go/pkg/mod/github.com/nicklaw5/[email protected]/users.go:42 +0xa0
main.get_user_id_from_username(0x706a7a, 0x8, 0x7083f0, 0xe)
/home/fish/twitch-discord-bot/main.go:17 +0xa9
main.main()
/home/fish/twitch-discord-bot/main.go:38 +0x145
exit status 2
Context here: pajbot/pajbot2#1188
Not really looking for any action, but would just be nice if this would've been a minor version bump instead of a patch version bump
Hi !
The last release broke my project :
I use url := AuthBaseURL + "/authorize?response_type=code"
because it return an access_token and a refresh_token
with the response_type=token
, it doesn't return an refresh_token
Did i miss something ?
I think it would be better to pass this as a param of the function
The Twith API Get Streams documentation shows a response that includes tag_ids
, but this interface only exposes a community_ids
array. I think community ids have been deprecated and replaced with tag ids?
Hi there, thanks for excellent API library!
There are new API endpoints for New Twitch API, Get Channel Editors
, Delete Videos
, Get User Block List
, Block User
, andUnblockUser
.
You can see https://dev.twitch.tv/docs/change-log for detail.
Thanks!
According to the docs the GetGameAnalytics
method doesn't support a number of optional parameters (eg. after
, type
, etc.). It also doesn't return a number of response fields (eg. date_range
, type
).
This comment also suggest that the URL is valid for 1 minute, but according to the docs it should be 5 mins.
I assume these changes were introduced after I implemented that endpoint in this library, as I can't remember them being there 🤔.
First of all, thank you for your amazing work on this library, it's great!
The library in general is written with single-user use cases in mind and doesn't work well in a multi-tenant system, i.e. a microservice which handles API calls for multiple users, bot id's, etc. This is because the state of all authentication mechanisms is stored within the instantiated object it self.
One option would be to create a new object per user, but this creates overhead, eliminates the possibility of some design patterns (i.e. singleton instantiation), and would require a tighter coupling of the driver to the authentication source.
Ideally, all authentication would be stateless and be passed in per call. At the very minimum, user and application tokens being passed in per API call would provide sufficient abstraction insofar that multiple users can be used on the same bot, concurrently, without having to create a new object for every user.
I'm curious to hear what you think @nicklaw5 about this proposal.
Thanks!
The Get Emote Sets function is missing the OwnerID
value returned from the API.
The simple solution is to add OwnerID
to the Emote
type, but that'd mean an empty value in the other calls using the type (i.e. Get Channel Emotes and Get Global Emotes).
The complex solution is to return a different type in the response of Get Emote Sets.
I'm happy to make the change with a PR but I'd like to hear your preferred solution first.
Implement functionality to update/get/delete stream schedule events:
https://dev.twitch.tv/docs/api/reference/#get-channel-stream-schedule
This is mostly a todo for a potential PR.
I got my application to work with refreshing tokens on a 401 like twitch suggests in https://dev.twitch.tv/docs/authentication/refresh-tokens/. I originally tried making it work by adding to the library, but I had no idea if it's needed or wanted, so instead I created an HTTPClient
that I pass in instead of a token. It is similar to how the spotify library I use does with oauth2 tokens.
If this is helpful for you adding auto token refreshing, feel free to use it. If not feel free to close the issue.
TokenHandler
is an wrapper of token storage (used to be database now a json file). handler.GetTwitchToken
gets the twitch token from memory, and handler.write
saves the file.
old NewClient method
token, err := c.tokens.GetTwitchToken()
if err != nil {
return fmt.Errorf("could not get twitch token: %w", err)
}
client, err := helix.NewClientWithContext(ctx, &helix.Options{
ClientID: c.settings.ClientID(),
UserAccessToken: token.AccessToken,
})
new NewClient method
client, err := helix.NewClientWithContext(ctx, &helix.Options{
ClientID: c.settings.ClientID(),
HTTPClient: c.tokens.NewTwitchClient(ctx, c.settings),
})
func (handler *TokenHandler) NewTwitchClient(ctx context.Context, settings *SettingsHandler) *TokenHTTPClient {
client := &TokenHTTPClient{
ctx: ctx,
logger: twitchClientTokenLogger,
settings: settings,
save: handler.write,
token: handler.GetTwitchToken,
}
return client
}
type TokenHTTPClient struct {
ctx context.Context
logger *slog.Logger
settings *SettingsHandler
save func() error
token func() (*Token, error)
}
func (client *TokenHTTPClient) Do(req *http.Request) (*http.Response, error) {
err := client.replaceAuth(req)
if err != nil {
return nil, fmt.Errorf("could not replace auth header: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusUnauthorized {
err := client.refreshToken(req)
if err != nil {
return nil, fmt.Errorf("got unauthorized and could not refresh token: %w", err)
}
// Don't defer the body close and do it here
// because the caller will close the body,
// but if we're getting a new response, then
// this body must be closed
resp.Body.Close()
err = client.replaceAuth(req)
if err != nil {
return nil, fmt.Errorf("could not replace auth header: %w", err)
}
return http.DefaultClient.Do(req)
}
return resp, nil
}
func (client *TokenHTTPClient) replaceAuth(req *http.Request) error {
token, err := client.token()
if err != nil {
return fmt.Errorf("could not get token: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("%s %s", "Bearer", token.AccessToken))
return nil
}
func (client *TokenHTTPClient) refreshToken(req *http.Request) error {
client.logger.Info("refreshing tokens")
refreshClient, err := helix.NewClientWithContext(client.ctx, &helix.Options{
ClientID: client.settings.ClientID(),
ClientSecret: client.settings.ClientSecret(),
})
if err != nil {
return fmt.Errorf("could not create client to refresh token: %w", err)
}
token, _ := client.token()
isValid, validateResp, err := refreshClient.ValidateToken(token.RefreshToken)
if err == nil && isValid {
return nil
}
defer func() {
err := client.save()
if err != nil {
client.logger.Error("could not save token change", err)
}
}()
token.AccessToken = ""
if err != nil {
return fmt.Errorf("could not validate token: %w", err)
}
if validateResp.StatusCode != http.StatusUnauthorized {
return fmt.Errorf("could not validate token: %s", validateResp.ErrorMessage)
}
resp, err := refreshClient.RefreshUserAccessToken(token.RefreshToken)
if err != nil || resp.ErrorMessage != "" {
token.RefreshToken = ""
if err != nil {
return fmt.Errorf("could not refresh token: %w", err)
}
return fmt.Errorf("could not refresh token: %s", resp.ErrorMessage)
}
token.AccessToken = resp.Data.AccessToken
token.RefreshToken = resp.Data.RefreshToken
token.TokenExpires = time.Now().UTC().Add(time.Second * time.Duration(resp.Data.ExpiresIn))
return nil
}
It's come to my attention that Twitch has incorrectly documented their endpoints...
their newer endpoints do actually support pagination although the docs do not r
eally state the fact they do, as identified by community devs.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.