Add context to all implemented endpoints

This commit is contained in:
Evan Fiordeliso 2024-02-27 23:03:40 -05:00
parent 4b44064fe5
commit 616cb935a0
29 changed files with 534 additions and 244 deletions

View File

@ -1,7 +1,9 @@
package ads
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
)
@ -36,18 +38,23 @@ type GetAdScheduleData struct {
//
// Requires a user access token that includes the channel:read:ads scope.
// The user_id in the user access token must match the broadcaster_id.
func (e *Ads) GetAdSchedule(broadcasterID string) (*GetAdScheduleResponse, error) {
func (e *Ads) GetAdSchedule(ctx context.Context, broadcasterID string) (*GetAdScheduleResponse, error) {
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "channels/ads", RawQuery: "broadcaster_id=" + broadcasterID})
resp, err := e.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := e.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetAdScheduleResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package ads
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
)
@ -27,18 +29,23 @@ type SnoozeNextAdData struct {
//
// Requires a user access token that includes the channel:manage:ads scope.
// The user_id in the user access token must match the broadcaster_id.
func (e *Ads) SnoozeNextAd(broadcasterID string) (*SnoozeNextAdResponse, error) {
func (e *Ads) SnoozeNextAd(ctx context.Context, broadcasterID string) (*SnoozeNextAdResponse, error) {
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "channels/ads/schedule/snooze", RawQuery: "broadcaster_id=" + broadcasterID})
resp, err := e.client.Post(endpoint.String(), "application/json", nil)
req, err := http.NewRequest(http.MethodPost, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := e.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data SnoozeNextAdResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,8 +1,10 @@
package ads
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
)
@ -39,28 +41,33 @@ type StartCommercialData struct {
// NOTE: Only the broadcaster may start a commercial; the broadcasters editors and moderators may not start commercials on behalf of the broadcaster.
//
// Requires a user access token that includes the channel:edit:commercial scope.
func (e *Ads) StartCommercial(req *StartCommercialRequest) (*StartCommercialResponse, error) {
func (e *Ads) StartCommercial(ctx context.Context, body *StartCommercialRequest) (*StartCommercialResponse, error) {
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "channels/commercial"})
r, w := io.Pipe()
go func() {
if err := json.NewEncoder(w).Encode(req); err != nil {
if err := json.NewEncoder(w).Encode(body); err != nil {
w.CloseWithError(err)
} else {
w.Close()
}
}()
resp, err := e.client.Post(endpoint.String(), "application/json", r)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := e.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data StartCommercialResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package analytics
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
@ -78,19 +80,24 @@ type ExtensionAnalyticsReport struct {
// Learn More: https://dev.twitch.tv/docs/insights
//
// Requires a user access token that includes the analytics:read:extensions scope.
func (e *Analytics) GetExtensionAnalytics(params GetExtensionAnalyticsParams) (*GetExtensionAnalyticsResponse, error) {
func (e *Analytics) GetExtensionAnalytics(ctx context.Context, params GetExtensionAnalyticsParams) (*GetExtensionAnalyticsResponse, error) {
v, _ := query.Values(params)
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "analytics/extensions", RawQuery: v.Encode()})
resp, err := e.client.Get(endpoint.String())
req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := e.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetExtensionAnalyticsResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package analytics
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
@ -79,19 +81,24 @@ type GameAnalyticsReport struct {
// Learn more: https://dev.twitch.tv/docs/insights
//
// Requires a user access token that includes the analytics:read:games scope.
func (e *Analytics) GetGameAnalytics(params GetGameAnalyticsParams) (*GetGameAnalyticsResponse, error) {
func (e *Analytics) GetGameAnalytics(ctx context.Context, params GetGameAnalyticsParams) (*GetGameAnalyticsResponse, error) {
v, _ := query.Values(params)
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "analytics/games", RawQuery: v.Encode()})
resp, err := e.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := e.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetGameAnalyticsResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package bits
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
@ -66,19 +68,24 @@ type LeaderboardEntry struct {
// Gets the Bits leaderboard for the authenticated broadcaster.
//
// Requires a user access token that includes the bits:read scope.
func (b *Bits) GetBitsLeaderboard(params *GetBitsLeaderboardParams) (*GetBitsLeaderboardResponse, error) {
func (b *Bits) GetBitsLeaderboard(ctx context.Context, params *GetBitsLeaderboardParams) (*GetBitsLeaderboardResponse, error) {
v, _ := query.Values(params)
endpoint := b.baseUrl.ResolveReference(&url.URL{Path: "bits/leaderboard", RawQuery: v.Encode()})
resp, err := b.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := b.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetBitsLeaderboardResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package bits
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
)
@ -79,18 +81,23 @@ type CheermoteImageSizes struct {
// Gets a list of Cheermotes that users can use to cheer Bits in any Bits-enabled channels chat room. Cheermotes are animated emotes that viewers can assign Bits to.
//
// Requires an app access token or user access token.
func (b *Bits) GetCheermotes(broadcasterID string) (*GetCheermotesResponse, error) {
func (b *Bits) GetCheermotes(ctx context.Context, broadcasterID string) (*GetCheermotesResponse, error) {
endpoint := b.baseUrl.ResolveReference(&url.URL{Path: "bits/cheermotes", RawQuery: "broadcaster_id=" + broadcasterID})
resp, err := b.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := b.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetCheermotesResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package bits
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
@ -93,19 +95,24 @@ type ProductDataCost struct {
// Gets an extensions list of transactions. A transaction records the exchange of a currency (for example, Bits) for a digital product.
//
// Requires an app access token.
func (b *Bits) GetExtensionTransactions(params *GetExtensionTransactionsParams) (*GetExtensionTransactionsResponse, error) {
func (b *Bits) GetExtensionTransactions(ctx context.Context, params *GetExtensionTransactionsParams) (*GetExtensionTransactionsResponse, error) {
v, _ := query.Values(params)
endpoint := b.baseUrl.ResolveReference(&url.URL{Path: "extensions/transactions", RawQuery: v.Encode()})
resp, err := b.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := b.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetExtensionTransactionsResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,8 +1,10 @@
package channelpoints
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
)
@ -67,28 +69,33 @@ type CreateCustomRewardsResponse struct {
// Creates a Custom Reward in the broadcasters channel. The maximum number of custom rewards per channel is 50, which includes both enabled and disabled rewards.
//
// Requires a user access token that includes the channel:manage:redemptions scope.
func (c *ChannelPoints) CreateCustomRewards(broadcastID string, req *CreateCustomRewardsRequest) (*CreateCustomRewardsResponse, error) {
func (c *ChannelPoints) CreateCustomRewards(ctx context.Context, broadcastID string, body *CreateCustomRewardsRequest) (*CreateCustomRewardsResponse, error) {
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channel_points/custom_rewards", RawQuery: "broadcaster_id=" + broadcastID})
r, w := io.Pipe()
go func() {
if err := json.NewEncoder(w).Encode(req); err != nil {
if err := json.NewEncoder(w).Encode(body); err != nil {
w.CloseWithError(err)
} else {
w.Close()
}
}()
resp, err := c.client.Post(endpoint.String(), "application/json", r)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data CreateCustomRewardsResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package channelpoints
import (
"context"
"net/http"
"net/url"
@ -20,20 +21,22 @@ type DeleteCustomRewardParams struct {
// The app used to create the reward is the only app that may delete it.
// If the rewards redemption status is UNFULFILLED at the time the reward is deleted, its redemption status is marked as FULFILLED.
//
// / Requires a user access token that includes the channel:manage:redemptions scope.
func (c *ChannelPoints) DeleteCustomReward(params *DeleteCustomRewardParams) error {
// Requires a user access token that includes the channel:manage:redemptions scope.
func (c *ChannelPoints) DeleteCustomReward(ctx context.Context, params *DeleteCustomRewardParams) error {
v, _ := query.Values(params)
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channel_points/custom_rewards", RawQuery: v.Encode()})
resp, err := c.client.Do(&http.Request{
Method: http.MethodDelete,
URL: endpoint,
})
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint.String(), nil)
if err != nil {
return err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
return nil
}

View File

@ -1,7 +1,9 @@
package channelpoints
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/google/go-querystring/query"
@ -33,19 +35,24 @@ type GetCustomRewardResponse struct {
// NOTE: A channel may offer a maximum of 50 rewards, which includes both enabled and disabled rewards.
//
// Requires a user access token that includes the channel:read:redemptions or channel:manage:redemptions scope.
func (c *ChannelPoints) GetCustomReward(params *GetCustomRewardParams) (*GetCustomRewardResponse, error) {
func (c *ChannelPoints) GetCustomReward(ctx context.Context, params *GetCustomRewardParams) (*GetCustomRewardResponse, error) {
v, _ := query.Values(params)
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channel_points/custom_rewards", RawQuery: v.Encode()})
resp, err := c.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetCustomRewardResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package channelpoints
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/google/go-querystring/query"
@ -49,19 +51,24 @@ type GetCustomRewardRedemptionResponse struct {
// Gets a list of redemptions for the specified custom reward. The app used to create the reward is the only app that may get the redemptions.
//
// Requires a user access token that includes the channel:read:redemptions or channel:manage:redemptions scope.
func (c *ChannelPoints) GetCustomRewardRedemption(params *GetCustomRewardRedemptionParams) (*GetCustomRewardRedemptionResponse, error) {
func (c *ChannelPoints) GetCustomRewardRedemption(ctx context.Context, params *GetCustomRewardRedemptionParams) (*GetCustomRewardRedemptionResponse, error) {
v, _ := query.Values(params)
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channel_points/custom_rewards/redemptions", RawQuery: v.Encode()})
resp, err := c.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetCustomRewardRedemptionResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package channelpoints
import (
"context"
"encoding/json"
"io"
"net/http"
@ -79,33 +80,34 @@ type UpdateCustomRewardResponse struct {
// Updates a custom reward. The app used to create the reward is the only app that may update the reward.
//
// Requires a user access token that includes the channel:manage:redemptions scope.
func (c *ChannelPoints) UpdateCustomReward(params *UpdateCustomRewardParams, req *UpdateCustomRewardRequest) (*UpdateCustomRewardResponse, error) {
v, _ := query.Values(req)
func (c *ChannelPoints) UpdateCustomReward(ctx context.Context, params *UpdateCustomRewardParams, body *UpdateCustomRewardRequest) (*UpdateCustomRewardResponse, error) {
v, _ := query.Values(body)
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channel_points/custom_rewards", RawQuery: v.Encode()})
r, w := io.Pipe()
go func() {
if err := json.NewEncoder(w).Encode(req); err != nil {
if err := json.NewEncoder(w).Encode(body); err != nil {
w.CloseWithError(err)
} else {
w.Close()
}
}()
resp, err := c.client.Do(&http.Request{
Method: http.MethodPatch,
URL: endpoint,
Body: r,
})
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint.String(), r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data UpdateCustomRewardResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package channelpoints
import (
"context"
"encoding/json"
"io"
"net/http"
@ -39,33 +40,34 @@ type UpdateRedemptionStatusResponse struct {
// The app used to create the reward is the only app that may update the redemption.
//
// Requires a user access token that includes the channel:manage:redemptions scope.
func (c *ChannelPoints) UpdateRedemptionStatus(params *UpdateRedemptionStatusParams, req *UpdateRedemptionStatusRequest) (*UpdateRedemptionStatusResponse, error) {
v, _ := query.Values(req)
func (c *ChannelPoints) UpdateRedemptionStatus(ctx context.Context, params *UpdateRedemptionStatusParams, body *UpdateRedemptionStatusRequest) (*UpdateRedemptionStatusResponse, error) {
v, _ := query.Values(body)
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channel_points/custom_rewards/redemptions", RawQuery: v.Encode()})
r, w := io.Pipe()
go func() {
if err := json.NewEncoder(w).Encode(req); err != nil {
if err := json.NewEncoder(w).Encode(body); err != nil {
w.CloseWithError(err)
} else {
w.Close()
}
}()
resp, err := c.client.Do(&http.Request{
Method: http.MethodPatch,
URL: endpoint,
Body: r,
})
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint.String(), r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data UpdateRedemptionStatusResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package channels
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
)
@ -25,18 +27,23 @@ type ChannelEditor struct {
// Gets the broadcasters list editors.
//
// Requires a user access token that includes the channel:read:editors scope.
func (c *Channels) GetChannelEditors(broadcasterID string) (*GetChannelEditorsResponse, error) {
func (c *Channels) GetChannelEditors(ctx context.Context, broadcasterID string) (*GetChannelEditorsResponse, error) {
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channels/editors", RawQuery: "broadcaster_id=" + broadcasterID})
resp, err := c.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetChannelEditorsResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package channels
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
@ -67,19 +69,24 @@ type ChannelFollower struct {
// This endpoint will return specific follower information only if both of the above are true.
// If a scope is not provided or the user isnt the broadcaster or a moderator for the specified channel,
// only the total follower count will be included in the response.
func (c *Channels) GetChannelFollowers(params *GetChannelFollowersParams) (*GetChannelFollowersResponse, error) {
func (c *Channels) GetChannelFollowers(ctx context.Context, params *GetChannelFollowersParams) (*GetChannelFollowersResponse, error) {
v, _ := query.Values(params)
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channels/followers", RawQuery: v.Encode()})
resp, err := c.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetChannelFollowersResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package channels
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/google/go-querystring/query"
@ -66,19 +68,24 @@ type ChannelInformation struct {
// Gets information about one or more channels.
//
// Requires an app access token or user access token.
func (c *Channels) GetChannelInformation(params *GetChannelInformationParams) (*GetChannelInformdationResponse, error) {
func (c *Channels) GetChannelInformation(ctx context.Context, params *GetChannelInformationParams) (*GetChannelInformdationResponse, error) {
v, _ := query.Values(params)
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channels", RawQuery: v.Encode()})
resp, err := c.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetChannelInformdationResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package channels
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
@ -58,19 +60,24 @@ type FollowedChannel struct {
// Gets a list of broadcasters that the specified user follows. You can also use this endpoint to see whether a user follows a specific broadcaster.
//
// Requires a user access token that includes the user:read:follows scope.
func (c *Channels) GetFollowedChannels(params *GetFollowedChannelsParams) (*GetFollowedChannelsResponse, error) {
func (c *Channels) GetFollowedChannels(ctx context.Context, params *GetFollowedChannelsParams) (*GetFollowedChannelsResponse, error) {
v, _ := query.Values(params)
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "users/follows", RawQuery: v.Encode()})
resp, err := c.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetFollowedChannelsResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package channels
import (
"context"
"encoding/json"
"io"
"net/http"
@ -52,24 +53,27 @@ type ModifyContentClassificationLabel struct {
// Updates a channels properties.
//
// Requires a user access token that includes the channel:manage:broadcast scope.
func (c *Channels) ModifyChannelInformation(broadcasterID string, req *ModifyChannelInformationRequest) error {
func (c *Channels) ModifyChannelInformation(ctx context.Context, broadcasterID string, body *ModifyChannelInformationRequest) error {
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channels", RawQuery: "broadcaster_id=" + broadcasterID})
r, w := io.Pipe()
go func() {
if err := json.NewEncoder(w).Encode(req); err != nil {
if err := json.NewEncoder(w).Encode(body); err != nil {
w.CloseWithError(err)
} else {
w.Close()
}
}()
_, err := c.client.Do(&http.Request{
Method: http.MethodPatch,
URL: endpoint,
Body: r,
})
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint.String(), r)
if err != nil {
return err
}
return err
if _, err := c.client.Do(req); err != nil {
return err
}
return nil
}

View File

@ -1,7 +1,9 @@
package charity
import (
"context"
"encoding/json"
"net/http"
"net/url"
"go.fifitido.net/twitch/api/types"
@ -52,18 +54,23 @@ type CharityCampaign struct {
// subscribe to the channel.charity_campaign.progress subscription type.
//
// Requires a user access token that includes the channel:read:charity scope.
func (c *Charity) GetCharityCampaign(broadcasterID string) (*GetCharityCampaignResponse, error) {
func (c *Charity) GetCharityCampaign(ctx context.Context, broadcasterID string) (*GetCharityCampaignResponse, error) {
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "charity/campaigns", RawQuery: "broadcaster_id=" + broadcasterID})
resp, err := c.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetCharityCampaignResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}

View File

@ -1,7 +1,9 @@
package charity
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/google/go-querystring/query"
@ -56,17 +58,22 @@ type CharityCampaignDonation struct {
// To receive events as donations occur, subscribe to the channel.charity_campaign.donate subscription type.
//
// Requires a user access token that includes the channel:read:charity scope.
func (c *Charity) GetCharityCampaignDonations(params *GetCharityCampaignDonationsParams) (*GetCharityCampaignDonationsResponse, error) {
func (c *Charity) GetCharityCampaignDonations(ctx context.Context, params *GetCharityCampaignDonationsParams) (*GetCharityCampaignDonationsResponse, error) {
v, _ := query.Values(params)
endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "charity/campaigns/donations", RawQuery: v.Encode()})
resp, err := c.client.Get(endpoint.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
var respBody GetCharityCampaignDonationsResponse
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
if err := json.NewDecoder(res.Body).Decode(&respBody); err != nil {
return nil, err
}

View File

@ -0,0 +1,83 @@
package eventsub
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
)
type CreateEventSubSubscriptionRequest struct {
// The type of subscription to create.
SubscriptionType
// A JSON object that contains the parameter values that are specific to the specified subscription type.
//For the objects required and optional fields, see the subscription types documentation.
Condition Condition `json:"condition"`
// The transport details that you want Twitch to use when sending you notifications.
Transport *Transport `json:"transport"`
}
type CreateEventSubSubscriptionResponse struct {
// A list that contains the single subscription that you created.
Data []*Subscription `json:"data"`
// The total number of subscriptions youve created.
Total int `json:"total"`
// The sum of all of your subscription costs. Learn More: https://dev.twitch.tv/docs/eventsub/manage-subscriptions/#subscription-limits
TotalCost int `json:"total_cost"`
// The maximum total cost that youre allowed to incur for all subscriptions you create.
MaxTotalCost int `json:"max_total_cost"`
}
// Creates an EventSub subscription.
//
// If you use webhooks to receive events, the request must specify an app access token.
// The request will fail if you use a user access token. If the subscription type requires user authorization,
// the user must have granted your app (client ID) permissions to receive those events before you subscribe to them.
// For example, to subscribe to channel.subscribe events, your app must get a user access token that includes the
// channel:read:subscriptions scope, which adds the required permission to your app access tokens client ID.
//
// If you use WebSockets to receive events, the request must specify a user access token.
// The request will fail if you use an app access token. If the subscription type requires user authorization,
// the token must include the required scope. However, if the subscription type doesnt include user authorization,
// the token may include any scopes or no scopes.
//
// If you use Conduits to receive events, the request must specify an app access token.
// The request will fail if you use a user access token.
func (e *EventSub) CreateEventSubSubscription(ctx context.Context, body *CreateEventSubSubscriptionRequest) (*CreateEventSubSubscriptionResponse, error) {
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "eventsub/subscriptions"})
r, w := io.Pipe()
go func() {
if err := json.NewEncoder(w).Encode(body); err != nil {
w.CloseWithError(err)
} else {
w.Close()
}
}()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), r)
if err != nil {
return nil, err
}
res, err := e.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data CreateEventSubSubscriptionResponse
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}
return &data, nil
}

View File

@ -1,48 +0,0 @@
package eventsub
import (
"encoding/json"
"io"
"net/url"
)
type CreateSubscriptionRequest struct {
SubscriptionType
Condition Condition `json:"condition"`
Transport *Transport `json:"transport"`
}
type CreateSubscriptionResponse struct {
Data []*Subscription `json:"data"`
Total int `json:"total"`
TotalCost int `json:"total_cost"`
MaxTotalCost int `json:"max_total_cost"`
}
func (e *EventSub) CreateSubscription(req *CreateSubscriptionRequest) (*CreateSubscriptionResponse, error) {
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "eventsub/subscriptions"})
r, w := io.Pipe()
go func() {
if err := json.NewEncoder(w).Encode(req); err != nil {
w.CloseWithError(err)
} else {
w.Close()
}
}()
resp, err := e.client.Post(endpoint.String(), "application/json", r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var data CreateSubscriptionResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
return &data, nil
}

View File

@ -0,0 +1,29 @@
package eventsub
import (
"context"
"net/http"
"net/url"
)
// Deletes an EventSub subscription.
//
// If you use webhooks to receive events, the request must specify an app access token.
// The request will fail if you use a user access token.
//
// If you use WebSockets to receive events, the request must specify a user access token.
// The request will fail if you use an app access token. The token may include any scopes.
func (e *EventSub) DeleteEventSubSubscription(ctx context.Context, id string) error {
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "eventsub/subscriptions", RawQuery: "id=" + id})
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint.String(), nil)
if err != nil {
return err
}
if _, err := e.client.Do(req); err != nil {
return err
}
return nil
}

View File

@ -1,20 +0,0 @@
package eventsub
import (
"net/http"
"net/url"
)
func (e *EventSub) DeleteSubscription(id string) error {
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "eventsub/subscriptions", RawQuery: "id=" + id})
_, err := e.client.Do(&http.Request{
Method: http.MethodDelete,
URL: endpoint,
})
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,80 @@
package eventsub
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/google/go-querystring/query"
"go.fifitido.net/twitch/api/types"
)
type GetEventSubSubscriptionsParams struct {
// Filter subscriptions by its status.
Status *Status `url:"status,omitempty"`
// Filter subscriptions by subscription type. For a list of subscription types.
// See Subscription Types: https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#subscription-types
Type *SubscriptionType `url:"type,omitempty"`
// Filter subscriptions by user ID.
// The response contains subscriptions where this ID matches a user ID that you specified in the Condition object when you created the subscription.
UserID *string `url:"user_id,omitempty"`
// The cursor used to get the next page of results.
// The pagination object in the response contains the cursor's value.
After *types.Cursor `url:"after,omitempty"`
}
type GetEventSubSubscriptionsResponse struct {
// The list of subscriptions. The list is ordered by the oldest subscription first.
// The list is empty if the client hasn't created subscriptions or there are no subscriptions that match the specified filter criteria.
Data []Subscription `json:"data"`
// The total number of subscriptions that you've created.
Total int `json:"total"`
// The sum of all of your subscription costs.
// Learn More: https://dev.twitch.tv/docs/eventsub/manage-subscriptions/#subscription-limits
TotalCost int `json:"total_cost"`
// The maximum total cost that you're allowed to incur for all subscriptions that you create.
MaxTotalCost int `json:"max_total_cost"`
// An object that contains the cursor used to get the next page of subscriptions.
// The object is empty if there are no more pages to get.
// The number of subscriptions returned per page is undertermined.
Pagination types.Pagination `json:"pagination"`
}
// Gets a list of EventSub subscriptions that the client in the access token created.
//
// If you use webhooks to receive events, the request must specify an app access token.
// The request will fail if you use a user access token.
//
// If you use WebSockets to receive events, the request must specify a user access token.
// The request will fail if you use an app access token. The token may include any scopes.
func (e *EventSub) GetEventSubSubscriptions(ctx context.Context, params *GetEventSubSubscriptionsParams) (*GetEventSubSubscriptionsResponse, error) {
v, _ := query.Values(params)
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "eventsub/subscriptions", RawQuery: v.Encode()})
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, err
}
res, err := e.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data GetEventSubSubscriptionsResponse
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}
return &data, nil
}

View File

@ -1,43 +0,0 @@
package eventsub
import (
"encoding/json"
"net/url"
"github.com/google/go-querystring/query"
"go.fifitido.net/twitch/api/types"
)
type GetSubscriptionsParams struct {
Status *Status `url:"status,omitempty"`
Type *SubscriptionType `url:"type,omitempty"`
UserID *string `url:"user_id,omitempty"`
After *types.Cursor `url:"after,omitempty"`
}
type GetSubscriptionsResponse struct {
Data []Subscription `json:"data"`
Total int `json:"total"`
TotalCost int `json:"total_cost"`
MaxTotalCost int `json:"max_total_cost"`
Pagination types.Pagination `json:"pagination"`
}
func (e *EventSub) GetSubscriptions(params *GetSubscriptionsParams) (*GetSubscriptionsResponse, error) {
v, _ := query.Values(params)
endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "eventsub/subscriptions", RawQuery: v.Encode()})
resp, err := e.client.Get(endpoint.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
var data GetSubscriptionsResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
return &data, nil
}

View File

@ -5,42 +5,116 @@ import "time"
type Condition map[string]any
type Subscription struct {
ID string `json:"id"`
Status string `json:"status"`
Type string `json:"type"`
Version string `json:"version"`
Condition Condition `json:"condition"`
CreatedAt time.Time `json:"created_at"`
// An ID that identifies the subscription.
ID string `json:"id"`
// The subscriptions status. The subscriber receives events only for enabled subscriptions. Possible values are:
//
// enabled — The subscription is enabled.
//
// webhook_callback_verification_pending — The subscription is pending verification of the specified callback URL (see Responding to a challenge request).
Status string `json:"status"`
// The subscriptions type.
// See Subscription Types: https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#subscription-types
Type string `json:"type"`
// The version number that identifies this definition of the subscriptions data.
Version string `json:"version"`
// The subscriptions parameter values. This is a string-encoded JSON object whose contents are determined by the subscription type.
Condition Condition `json:"condition"`
// The date and time (in RFC3339 format) of when the subscription was created.
CreatedAt time.Time `json:"created_at"`
// The transport details used to send the notifications.
Transport *Transport `json:"transport"`
Cost int `json:"cost"`
// The amount that the subscription counts against your limit.
// Learn More: https://dev.twitch.tv/docs/eventsub/manage-subscriptions/#subscription-limits
Cost int `json:"cost"`
}
type Status string
const (
StatusEnabled = "enabled"
// enabled — The subscription is enabled.
StatusEnabled = "enabled"
// webhook_callback_verification_pending — The subscription is pending verification of the specified callback URL.
StatusWebhookCallbackVerificationPending = "webhook_callback_verification_pending"
StatusWebhookCallbackVerificationFailed = "webhook_callback_verification_failed"
NotificationFailuresExceeded = "notification_failures_exceeded"
AuthorizationRevoked = "authorization_revoked"
ModeratorRemoved = "moderator_removed"
USerRemoved = "user_removed"
VersionRemoved = "version_removed"
BetaMaintenance = "beta_maintenance"
WebsocketDisconnected = "websocket_disconnected"
WebsocketFailedPingPong = "websocket_failed_ping_pong"
WebsocketReceivedInboundTraffic = "websocket_received_inbound_traffic"
WebsocketConnectionUnused = "websocket_connection_unused"
WebsocketInternalError = "websocket_internal_error"
WebsocketNetworkTimeout = "websocket_network_timeout"
WebsocketnetworkError = "websocket_network_error"
// webhook_callback_verification_failed — The specified callback URL failed verification.
StatusWebhookCallbackVerificationFailed = "webhook_callback_verification_failed"
// notification_failures_exceeded — The notification delivery failure rate was too high.
NotificationFailuresExceeded = "notification_failures_exceeded"
// authorization_revoked — The authorization was revoked for one or more users specified in the Condition object.
AuthorizationRevoked = "authorization_revoked"
// moderator_removed — The moderator that authorized the subscription is no longer one of the broadcaster's moderators.
ModeratorRemoved = "moderator_removed"
// user_removed — One of the users specified in the Condition object was removed.
UserRemoved = "user_removed"
// version_removed — The subscription to subscription type and version is no longer supported.
VersionRemoved = "version_removed"
// beta_maintenance — The subscription to the beta subscription type was removed due to maintenance.
BetaMaintenance = "beta_maintenance"
// websocket_disconnected — The client closed the connection.
WebsocketDisconnected = "websocket_disconnected"
// websocket_failed_ping_pong — The client failed to respond to a ping message.
WebsocketFailedPingPong = "websocket_failed_ping_pong"
// websocket_received_inbound_traffic — The client sent a non-pong message.
// Clients may only send pong messages (and only in response to a ping message).
WebsocketReceivedInboundTraffic = "websocket_received_inbound_traffic"
// websocket_connection_unused — The client failed to subscribe to events within the required time.
WebsocketConnectionUnused = "websocket_connection_unused"
// websocket_internal_error — The Twitch WebSocket server experienced an unexpected error.
WebsocketInternalError = "websocket_internal_error"
// websocket_network_timeout — The Twitch WebSocket server timed out writing the message to the client.
WebsocketNetworkTimeout = "websocket_network_timeout"
// websocket_network_error — The Twitch WebSocket server experienced a network error writing the message to the client.
WebsocketnetworkError = "websocket_network_error"
)
type Transport struct {
Method string `json:"method"`
Callback *string `json:"callback,omitempty"`
Secret *string `json:"secret,omitempty"`
// The transport method. Possible values are:
//
// webhook, websocket, conduit
Method string `json:"method"`
// The callback URL where the notifications are sent. The URL must use the HTTPS protocol and port 443.
// See Processing an event. Specify this field only if method is set to webhook.
//
// NOTE: Redirects are not followed.
Callback *string `json:"callback,omitempty"`
// The secret used to verify the signature.
// The secret must be an ASCII string thats a minimum of 10 characters long and a maximum of 100 characters long.
// For information about how the secret is used,
// see Verifying the event message: https://dev.twitch.tv/docs/eventsub/handling-webhook-events#verifying-the-event-message
// Specify this field only if method is set to webhook.
Secret *string `json:"secret,omitempty"`
// An ID that identifies the WebSocket to send notifications to. When you connect to EventSub using WebSockets,
// the server returns the ID in the Welcome message. Specify this field only if method is set to websocket.
SessionID *string `json:"session_id,omitempty"`
// An ID that identifies the conduit to send notifications to.
// When you create a conduit, the server returns the conduit ID.
// Specify this field only if method is set to conduit.
ConduitID *string `json:"conduit_id,omitempty"`
}
@ -75,7 +149,10 @@ func ConduitTransport(conduitID string) *Transport {
}
type SubscriptionType struct {
Name string `json:"type"`
// The type of subscription.
Name string `json:"type"`
// The version number that identifies the definition of the subscription type that you want the response to use.
Version string `json:"version"`
}

View File

@ -1,6 +1,8 @@
package eventsub
import (
"context"
"go.fifitido.net/twitch/api"
"go.fifitido.net/twitch/api/eventsub"
)
@ -19,8 +21,8 @@ func New(api *api.API, trans *eventsub.Transport) *EventSub {
}
}
func (e *EventSub) Subscribe(subType eventsub.SubscriptionType, cond eventsub.Condition) error {
res, err := e.api.EventSub.CreateSubscription(&eventsub.CreateSubscriptionRequest{
func (e *EventSub) Subscribe(ctx context.Context, subType eventsub.SubscriptionType, cond eventsub.Condition) error {
res, err := e.api.EventSub.CreateEventSubSubscription(ctx, &eventsub.CreateEventSubSubscriptionRequest{
SubscriptionType: subType,
Condition: cond,
Transport: e.transport,
@ -39,7 +41,7 @@ func (e *EventSub) Subscribe(subType eventsub.SubscriptionType, cond eventsub.Co
func (e *EventSub) Close() error {
for _, sub := range e.subscriptions {
if err := e.api.EventSub.DeleteSubscription(sub.ID); err != nil {
if err := e.api.EventSub.DeleteEventSubSubscription(context.Background(), sub.ID); err != nil {
return err
}
}