commit 422102ccee6ddf7c75dcb6e0909010d48a1c87b8 Author: Evan Fiordeliso Date: Tue Feb 27 22:13:57 2024 -0500 Initial commit diff --git a/api.go b/api.go new file mode 100644 index 0000000..0c9af44 --- /dev/null +++ b/api.go @@ -0,0 +1,7 @@ +package twitch + +import "go.fifitido.net/twitch/api" + +func NewAPI() *api.API { + return api.New() +} diff --git a/api/ads/ads.go b/api/ads/ads.go new file mode 100644 index 0000000..2191273 --- /dev/null +++ b/api/ads/ads.go @@ -0,0 +1,18 @@ +package ads + +import ( + "net/http" + "net/url" +) + +type Ads struct { + client *http.Client + baseUrl *url.URL +} + +func New(client *http.Client, baseUrl *url.URL) *Ads { + return &Ads{ + client: client, + baseUrl: baseUrl, + } +} diff --git a/api/ads/get_ad_schedule.go b/api/ads/get_ad_schedule.go new file mode 100644 index 0000000..a827ccf --- /dev/null +++ b/api/ads/get_ad_schedule.go @@ -0,0 +1,55 @@ +package ads + +import ( + "encoding/json" + "net/url" + "time" +) + +type GetAdScheduleResponse struct { + // A list that contains information related to the channel’s ad schedule. + Data []GetAdScheduleData `json:"data"` +} + +type GetAdScheduleData struct { + // The number of snoozes available for the broadcaster. + SnoozeCount int `json:"snooze_count"` + + // The UTC timestamp when the broadcaster will gain an additional snooze, in RFC3339 format. + SnoozeRefreshAt time.Time `json:"snooze_refresh_at"` + + // The UTC timestamp of the broadcaster’s next scheduled ad, in RFC3339 format. Empty if the channel has no ad scheduled or is not live. + NextAdAt time.Time `json:"next_ad_at"` + + // The length in seconds of the scheduled upcoming ad break. + Duration int `json:"duration"` + + // The UTC timestamp of the broadcaster’s last ad-break, in RFC3339 format. Empty if the channel has not run an ad or is not live. + LastAdAt time.Time `json:"last_ad_at"` + + // The amount of pre-roll free time remaining for the channel in seconds. Returns 0 if they are currently not pre-roll free. + PrerollFreeTime int `json:"preroll_free_time"` +} + +// This endpoint returns ad schedule related information, including snooze, when the last ad was run, when the next ad is scheduled, +// and if the channel is currently in pre-roll free time. Note that a new ad cannot be run until 8 minutes after running a previous ad. +// +// 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) { + endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "channels/ads", RawQuery: "broadcaster_id=" + broadcasterID}) + + resp, err := e.client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetAdScheduleResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/ads/snooze_next_ad.go b/api/ads/snooze_next_ad.go new file mode 100644 index 0000000..c815745 --- /dev/null +++ b/api/ads/snooze_next_ad.go @@ -0,0 +1,46 @@ +package ads + +import ( + "encoding/json" + "net/url" + "time" +) + +type SnoozeNextAdResponse struct { + // A list that contains information about the channel’s snoozes and next upcoming ad after successfully snoozing. + Data []SnoozeNextAdData `json:"data"` +} + +type SnoozeNextAdData struct { + // The number of snoozes available for the broadcaster. + SnoozeCount int `json:"snooze_count"` + + // The UTC timestamp when the broadcaster will gain an additional snooze, in RFC3339 format. + SnoozeRefreshAt time.Time `json:"snooze_refresh_at"` + + // The UTC timestamp of the broadcaster’s next scheduled ad, in RFC3339 format. + NextAdAt time.Time `json:"next_ad_at"` +} + +// If available, pushes back the timestamp of the upcoming automatic mid-roll ad by 5 minutes. +// This endpoint duplicates the snooze functionality in the creator dashboard’s Ads Manager. +// +// 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) { + 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) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data SnoozeNextAdResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/ads/start_commercial.go b/api/ads/start_commercial.go new file mode 100644 index 0000000..d886a39 --- /dev/null +++ b/api/ads/start_commercial.go @@ -0,0 +1,68 @@ +package ads + +import ( + "encoding/json" + "io" + "net/url" +) + +type StartCommercialRequest struct { + // The ID of the partner or affiliate broadcaster that wants to run the commercial. This ID must match the user ID found in the OAuth token. + BroadcasterID string `json:"broadcaster_id"` + + // The length of the commercial to run, in seconds. + // Twitch tries to serve a commercial that’s the requested length, but it may be shorter or longer. + // The maximum length you should request is 180 seconds. + Duration int `json:"duration"` +} + +type StartCommercialResponse struct { + // An array that contains a single object with the status of your start commercial request. + Data []StartCommercialData `json:"data"` +} + +type StartCommercialData struct { + // The length of the commercial you requested. If you request a commercial that’s longer than 180 seconds, the API uses 180 seconds. + Length int `json:"length"` + + // A message that indicates whether Twitch was able to serve an ad. + Message string `json:"message"` + + // The number of seconds you must wait before running another commercial. + RetryAfter int `json:"retry_after"` +} + +// Starts a commercial on the specified channel. +// +// NOTE: Only partners and affiliates may run commercials and they must be streaming live at the time. +// +// NOTE: Only the broadcaster may start a commercial; the broadcaster’s 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) { + endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "channels/commercial"}) + + 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 StartCommercialResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/analytics/analytics.go b/api/analytics/analytics.go new file mode 100644 index 0000000..b4d81bc --- /dev/null +++ b/api/analytics/analytics.go @@ -0,0 +1,18 @@ +package analytics + +import ( + "net/http" + "net/url" +) + +type Analytics struct { + client *http.Client + baseUrl *url.URL +} + +func New(client *http.Client, baseUrl *url.URL) *Analytics { + return &Analytics{ + client: client, + baseUrl: baseUrl, + } +} diff --git a/api/analytics/get_extension_analytics.go b/api/analytics/get_extension_analytics.go new file mode 100644 index 0000000..141a320 --- /dev/null +++ b/api/analytics/get_extension_analytics.go @@ -0,0 +1,98 @@ +package analytics + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetExtensionAnalyticsParams struct { + // The extension's client ID. If specified, the response contains a report for the specified extension. + // If not specified, the response includes a report for each extension that the authenticated user owns. + ExtensionID *string `url:"extension_id,omitempty"` + + // The type of analytics report to get. Possible values are: + // + // - overview_v2 + Type *string `url:"type,omitempty"` + + // The reporting window's start date, in RFC3339 format. Set the time portion to zeroes (for example, 2021-10-22T00:00:00Z). + // + // The start date must be on or after January 31, 2018. If you specify an earlier date, the API ignores it and uses January 31, 2018. + // If you specify a start date, you must specify an end date. If you don't specify a start and end date, + // the report includes all available data since January 31, 2018. + // + // The report contains one row of data for each day in the reporting window. + StartedAt *time.Time `url:"started_at,omitempty"` + + // The reporting window's end date, in RFC3339 format. Set the time portion to zeroes (for example, 2021-10-27T00:00:00Z). + // The report is inclusive of the end date. + // + // Specify an end date only if you provide a start date. Because it can take up to two days for the data to be available, + // you must specify an end date that's earlier than today minus one to two days. + // If not, the API ignores your end date and uses an end date that is today minus one to two days. + EndedAt *time.Time `url:"ended_at,omitempty"` + + // The maximum number of report URLs to return per page in the response. + // The minimum page size is 1 URL per page and the maximum is 100 URLs per page. The default is 20. + // + // NOTE: While you may specify a maximum value of 100, the response will contain at most 20 URLs per page. + First *int `url:"first,omitempty"` + + // The cursor used to get the next page of results. The Pagination object in the response contains the cursor’s value. + // Read More: https://dev.twitch.tv/docs/api/guide#pagination + // + // This parameter is ignored if the extension_id parameter is set. + After *types.Cursor `url:"after,omitempty"` +} + +type GetExtensionAnalyticsResponse struct { + // A list of reports. The reports are returned in no particular order; however, the data within each report is in ascending order by date (newest first). + // The report contains one row of data per day of the reporting window; the report contains rows for only those days that the extension was used. + // The array is empty if there are no reports. + Data []ExtensionAnalyticsReport `json:"data"` + + // Contains the information used to page through the list of results. The object is empty if there are no more pages left to page through. + // Read More: https://dev.twitch.tv/docs/api/guide#pagination + Pagination types.Pagination `json:"pagination"` +} + +type ExtensionAnalyticsReport struct { + // An ID that identifies the extension that the report was generated for. + ExtensionID string `json:"extension_id"` + + // The URL that you use to download the report. The URL is valid for 5 minutes. + URL string `json:"URL"` + + // The type of report. + Type string `json:"type"` + + // The reporting window’s start and end dates, in RFC3339 format. + DateRange types.DateRange `json:"date_range"` +} + +// Gets an analytics report for one or more extensions. The response contains the URLs used to download the reports (CSV files). +// 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) { + v, _ := query.Values(params) + endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "analytics/extensions", RawQuery: v.Encode()}) + + resp, err := e.client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetExtensionAnalyticsResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/analytics/get_game_analytics.go b/api/analytics/get_game_analytics.go new file mode 100644 index 0000000..1e41971 --- /dev/null +++ b/api/analytics/get_game_analytics.go @@ -0,0 +1,99 @@ +package analytics + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetGameAnalyticsParams struct { + // The game’s client ID. If specified, the response contains a report for the specified game. + // If not specified, the response includes a report for each of the authenticated user’s games. + GameID *string `url:"game_id,omitempty"` + + // The type of analytics report to get. Possible values are: + // + // - overview_v2 + Type *string `url:"type,omitempty"` + + // The reporting window’s start date, in RFC3339 format. Set the time portion to zeroes (for example, 2021-10-22T00:00:00Z). + // If you specify a start date, you must specify an end date. + // + // The start date must be within one year of today’s date. If you specify an earlier date, + // the API ignores it and uses a date that’s one year prior to today’s date. If you don’t specify a start and end date, + // the report includes all available data for the last 365 days from today. + // + // The report contains one row of data for each day in the reporting window. + StartedAt *time.Time `url:"started_at,omitempty"` + + // The reporting window’s end date, in RFC3339 format. Set the time portion to zeroes (for example, 2021-10-22T00:00:00Z). + // The report is inclusive of the end date. + // + // Specify an end date only if you provide a start date. Because it can take up to two days for the data to be available, + // you must specify an end date that’s earlier than today minus one to two days. + // If not, the API ignores your end date and uses an end date that is today minus one to two days. + EndedAt *time.Time `url:"ended_at,omitempty"` + + // The maximum number of report URLs to return per page in the response. + // The minimum page size is 1 URL per page and the maximum is 100 URLs per page.The default is 20. + // + // NOTE: While you may specify a maximum value of 100, the response will contain at most 20 URLs per page. + First *int `url:"first,omitempty"` + + // The cursor used to get the next page of results. The Pagination object in the response contains the cursor’s value. + // Read More: https://dev.twitch.tv/docs/api/guide#pagination + // + // This parameter is ignored if game_id parameter is set. + After *types.Cursor `url:"after,omitempty"` +} + +type GetGameAnalyticsResponse struct { + // A list of reports. The reports are returned in no particular order; however, the data within each report is in ascending order by date (newest first). + // The report contains one row of data per day of the reporting window; the report contains rows for only those days that the game was used. + // A report is available only if the game was broadcast for at least 5 hours over the reporting period. The array is empty if there are no reports. + Data []GameAnalyticsReport `json:"data"` + + // Contains the information used to page through the list of results. + // The object is empty if there are no more pages left to page through. + Pagination types.Pagination `json:"pagination"` +} + +type GameAnalyticsReport struct { + // An ID that identifies the game that the report was generated for. + GameID string `json:"game_id"` + + // The URL that you use to download the report. The URL is valid for 5 minutes. + URL string `json:"URL"` + + // The type of report. + Type string `json:"type"` + + // The reporting window’s start and end dates, in RFC3339 format. + DateRange types.DateRange `json:"date_range"` +} + +// Gets an analytics report for one or more games. The response contains the URLs used to download the reports (CSV files). +// 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) { + v, _ := query.Values(params) + endpoint := e.baseUrl.ResolveReference(&url.URL{Path: "analytics/games", RawQuery: v.Encode()}) + + resp, err := e.client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetGameAnalyticsResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..e217771 --- /dev/null +++ b/api/api.go @@ -0,0 +1,44 @@ +package api + +import ( + "net/http" + "net/url" + + "go.fifitido.net/twitch/api/ads" + "go.fifitido.net/twitch/api/analytics" + "go.fifitido.net/twitch/api/bits" + "go.fifitido.net/twitch/api/channelpoints" + "go.fifitido.net/twitch/api/channels" + "go.fifitido.net/twitch/api/eventsub" +) + +const HelixBaseUrl = "https://api.twitch.tv/helix" + +type API struct { + client *http.Client + baseUrl *url.URL + + Ads *ads.Ads + Analytics *analytics.Analytics + Bits *bits.Bits + Channels *channels.Channels + ChannelPoints *channelpoints.ChannelPoints + EventSub *eventsub.EventSub +} + +func New() *API { + client := &http.Client{} + baseUrl, _ := url.Parse(HelixBaseUrl) + + return &API{ + client: client, + baseUrl: baseUrl, + + Ads: ads.New(client, baseUrl), + Analytics: analytics.New(client, baseUrl), + Bits: bits.New(client, baseUrl), + Channels: channels.New(client, baseUrl), + ChannelPoints: channelpoints.New(client, baseUrl), + EventSub: eventsub.New(client, baseUrl), + } +} diff --git a/api/bits/bits.go b/api/bits/bits.go new file mode 100644 index 0000000..11c7aa5 --- /dev/null +++ b/api/bits/bits.go @@ -0,0 +1,38 @@ +package bits + +import ( + "net/http" + "net/url" +) + +type Bits struct { + client *http.Client + baseUrl *url.URL +} + +func New(client *http.Client, baseUrl *url.URL) *Bits { + return &Bits{ + client: client, + baseUrl: baseUrl, + } +} + +type Period string + +const ( + Day Period = "day" + Week Period = "week" + Month Period = "month" + Year Period = "year" + All Period = "all" +) + +type CheermoteType string + +const ( + GlobalFirstParty CheermoteType = "global_first_party" + GlobalThirdParty CheermoteType = "global_third_party" + ChannelCustom CheermoteType = "channel_custom" + DisplayOnly CheermoteType = "display_only" + Sponsored CheermoteType = "sponsored" +) diff --git a/api/bits/get_bits_leaderboard.go b/api/bits/get_bits_leaderboard.go new file mode 100644 index 0000000..cda4096 --- /dev/null +++ b/api/bits/get_bits_leaderboard.go @@ -0,0 +1,86 @@ +package bits + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetBitsLeaderboardParams struct { + // The number of results to return. The minimum count is 1 and the maximum is 100. The default is 10. + Count *int `url:"count,omitempty"` + + // The time period over which data is aggregated (uses the PST time zone). + Period *Period `url:"period,omitempty"` + + // The start date, in RFC3339 format, used for determining the aggregation period. Specify this parameter only if you specify the period query parameter. + // The start date is ignored if period is all. + // + // Note that the date is converted to PST before being used, so if you set the start time to 2022-01-01T00:00:00.0Z and period to month, + // the actual reporting period is December 2021, not January 2022. If you want the reporting period to be January 2022, + // you must set the start time to 2022-01-01T08:00:00.0Z or 2022-01-01T00:00:00.0-08:00. + // + // If your start date uses the ‘+’ offset operator (for example, 2022-01-01T00:00:00.0+05:00), you must URL encode the start date. + StartedAt *time.Time `url:"started_at,omitempty"` + + // An ID that identifies a user that cheered bits in the channel. + // If count is greater than 1, the response may include users ranked above and below the specified user. + // To get the leaderboard’s top leaders, don’t specify a user ID. + UserID *string `url:"user_id,omitempty"` +} + +type GetBitsLeaderboardResponse struct { + // A list of leaderboard leaders. The leaders are returned in rank order by how much they’ve cheered. + // The array is empty if nobody has cheered bits. + Data []LeaderboardEntry `json:"data"` + + // The reporting window’s start and end dates, in RFC3339 format. The dates are calculated by using the started_at and period query parameters. + // If you don’t specify the started_at query parameter, the fields contain empty strings. + DateRange types.DateRange `json:"date_range"` + + // The number of ranked users in data. + // This is the value in the count query parameter or the total number of entries on the leaderboard, whichever is less. + Total int `json:"total"` +} + +type LeaderboardEntry struct { + // An ID that identifies a user on the leaderboard. + UserID string `json:"user_id"` + + // The user’s login name. + UserLogin string `json:"user_login"` + + // The user’s display name. + UserName string `json:"user_name"` + + // The user’s position on the leaderboard. + Rank int `json:"rank"` + + // The number of Bits the user has cheered. + Score int `json:"score"` +} + +// 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) { + v, _ := query.Values(params) + endpoint := b.baseUrl.ResolveReference(&url.URL{Path: "bits/leaderboard", RawQuery: v.Encode()}) + + resp, err := b.client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetBitsLeaderboardResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/bits/get_cheermotes.go b/api/bits/get_cheermotes.go new file mode 100644 index 0000000..83cfc65 --- /dev/null +++ b/api/bits/get_cheermotes.go @@ -0,0 +1,98 @@ +package bits + +import ( + "encoding/json" + "net/url" + "time" +) + +type GetCheermotesResponse struct { + // The list of Cheermotes. The list is in ascending order by the order field’s value. + Data []Cheermote `json:"data"` +} + +type Cheermote struct { + // The name portion of the Cheermote string that you use in chat to cheer Bits. + // The full Cheermote string is the concatenation of {prefix} + {number of Bits}. + // For example, if the prefix is “Cheer” and you want to cheer 100 Bits, the full Cheermote string is Cheer100. + // When the Cheermote string is entered in chat, Twitch converts it to the image associated with the Bits tier that was cheered. + Prefix string `json:"prefix"` + + // A list of tier levels that the Cheermote supports. + // Each tier identifies the range of Bits that you can cheer at that tier level and an image that graphically identifies the tier level. + Tiers []CheermoteTier `json:"tiers"` + + // The type of Cheermote. + Type CheermoteType `json:"type"` + + // The order that the Cheermotes are shown in the Bits card. The numbers may not be consecutive. For example, the numbers may jump from 1 to 7 to 13. + // The order numbers are unique within a Cheermote type (for example, global_first_party) but may not be unique amongst all Cheermotes in the response. + Order int `json:"order"` + + // The date and time, in RFC3339 format, when this Cheermote was last updated. + LastUpdated time.Time `json:"last_updated"` + + // A Boolean value that indicates whether this Cheermote provides a charitable contribution match during charity campaigns. + IsCharitable bool `json:"is_charitable"` +} + +type CheermoteTier struct { + // The minimum number of Bits that you must cheer at this tier level. + // The maximum number of Bits that you can cheer at this level is determined by the required minimum Bits of the next tier level minus 1. + // For example, if min_bits is 1 and min_bits for the next tier is 100, the Bits range for this tier level is 1 through 99. + // The minimum Bits value of the last tier is the maximum number of Bits you can cheer using this Cheermote. For example, 10000. + MinBits int `json:"min_bits"` + + // The tier level. Possible tiers are: + // + // 1, 100, 500, 1000, 5000, 10000, 100000 + ID string `json:"id"` + + // The hex code of the color associated with this tier level (for example, #979797). + Color string `json:"color"` + + // The animated and static image sets for the Cheermote. The dictionary of images is organized by theme, format, and size. + // The theme keys are dark and light. Each theme is a dictionary of formats: animated and static. + // Each format is a dictionary of sizes: 1, 1.5, 2, 3, and 4. The value of each size contains the URL to the image. + Images map[string]CheermoteImage `json:"images"` + + // A Boolean value that determines whether users can cheer at this tier level. + CanCheer bool `json:"can_cheer"` + + // A Boolean value that determines whether this tier level is shown in the Bits card. Is true if this tier level is shown in the Bits card. + ShowInBitsCard bool `json:"show_in_bits_card"` +} + +type CheermoteImage struct { + Animated *CheermoteImageSizes `json:"animated"` + Static *CheermoteImageSizes `json:"static"` +} + +type CheermoteImageSizes struct { + One CheermoteImage `json:"1"` + One5 CheermoteImage `json:"1.5"` + Two CheermoteImage `json:"2"` + Three CheermoteImage `json:"3"` + Four CheermoteImage `json:"4"` +} + +// Gets a list of Cheermotes that users can use to cheer Bits in any Bits-enabled channel’s 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) { + endpoint := b.baseUrl.ResolveReference(&url.URL{Path: "bits/cheermotes", RawQuery: "broadcaster_id=" + broadcasterID}) + + resp, err := b.client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetCheermotesResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/bits/get_extension_transactions.go b/api/bits/get_extension_transactions.go new file mode 100644 index 0000000..5aafc24 --- /dev/null +++ b/api/bits/get_extension_transactions.go @@ -0,0 +1,113 @@ +package bits + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetExtensionTransactionsParams struct { + // The ID of the extension whose list of transactions you want to get. + ExtensionID string `url:"extension_id"` + + // A transaction ID used to filter the list of transactions. Specify this parameter for each transaction you want to get. + // For example, id=1234&id=5678. You may specify a maximum of 100 IDs. + IDs []string `url:"ids,omitempty"` + + // The maximum number of items to return per page in the response. + // The minimum page size is 1 item per page and the maximum is 100 items per page. The default is 20. + First *int `url:"first,omitempty"` + + // The cursor used to get the next page of results. The Pagination object in the response contains the cursor’s value. + // Read More: https://dev.twitch.tv/docs/api/guide#pagination + After *types.Cursor `url:"after,omitempty"` +} + +type GetExtensionTransactionsResponse struct { + // The list of transactions. + Data []ExtensionTransaction `json:"data"` + + // Contains the information used to page through the list of results. The object is empty if there are no more pages left to page through. + // Read More: https://dev.twitch.tv/docs/api/guide#pagination + Pagination types.Pagination `json:"pagination"` +} + +type ExtensionTransaction struct { + // An ID that identifies the transaction. + ID string `json:"id"` + + // The UTC date and time (in RFC3339 format) of the transaction. + Timestamp time.Time `json:"timestamp"` + + // The ID of the broadcaster that owns the channel where the transaction occurred. + BroadcasterID string `json:"broadcaster_id"` + + // The broadcaster’s login name. + BroadcasterLogin string `json:"broadcaster_login"` + + // The broadcaster’s display name. + BroadcasterName string `json:"broadcaster_name"` + + // The type of transaction. Possible values are: + // + // BITS_IN_EXTENSION + ProductType string `json:"product_type"` +} + +type ProductData struct { + // An ID that identifies the digital product. + SKU string `json:"sku"` + + // Set to twitch.ext. + . + Domain string `json:"domain"` + + // Contains details about the digital product’s cost. + Cost ProductDataCost `json:"cost"` + + // A Boolean value that determines whether the product is in development. Is true if the digital product is in development and cannot be exchanged. + InDevelopment bool `json:"in_development"` + + // The name of the digital product. + DisplayName string `json:"display_name"` + + // This field is always empty since you may purchase only unexpired products. + Expiration string `json:"expiration"` + + // A Boolean value that determines whether the data was broadcast to all instances of the extension. Is true if the data was broadcast to all instances. + Broadcast bool `json:"broadcast"` +} + +type ProductDataCost struct { + // The amount exchanged for the digital product. + Amount int `json:"amount"` + + // The type of currency exchanged. Possible values are: + // + // bits + Type string `json:"type"` +} + +// Gets an extension’s 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) { + v, _ := query.Values(params) + endpoint := b.baseUrl.ResolveReference(&url.URL{Path: "extensions/transactions", RawQuery: v.Encode()}) + + resp, err := b.client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetExtensionTransactionsResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channelpoints/channelpoints.go b/api/channelpoints/channelpoints.go new file mode 100644 index 0000000..bd6de85 --- /dev/null +++ b/api/channelpoints/channelpoints.go @@ -0,0 +1,18 @@ +package channelpoints + +import ( + "net/http" + "net/url" +) + +type ChannelPoints struct { + client *http.Client + baseUrl *url.URL +} + +func New(client *http.Client, baseUrl *url.URL) *ChannelPoints { + return &ChannelPoints{ + client: client, + baseUrl: baseUrl, + } +} diff --git a/api/channelpoints/create_custom_rewards.go b/api/channelpoints/create_custom_rewards.go new file mode 100644 index 0000000..0fe30ca --- /dev/null +++ b/api/channelpoints/create_custom_rewards.go @@ -0,0 +1,96 @@ +package channelpoints + +import ( + "encoding/json" + "io" + "net/url" +) + +type CreateCustomRewardsRequest struct { + // The custom reward’s title. The title may contain a maximum of 45 characters and it must be unique amongst all of the broadcaster’s custom rewards. + Title string `json:"title"` + + // The cost of the reward, in Channel Points. + // The minimum is 1 point. + Cost int64 `json:"cost"` + + // The prompt shown to the viewer when they redeem the reward. + // Specify a prompt if is_user_input_required is true. + // The prompt is limited to a maximum of 200 characters. + Prompt *string `json:"prompt"` + + // A Boolean value that determines whether the reward is enabled. Viewers see only enabled rewards. + // The default is true. + IsEnabled *bool `json:"is_enabled"` + + // The background color to use for the reward. Specify the color using Hex format (for example, #9147FF). + BackgroundColor *string `json:"background_color"` + + // A Boolean value that determines whether the user needs to enter information when redeeming the reward. See the prompt field. + // The default is false. + IsUserInputRequired *bool `json:"is_user_input_required"` + + // A Boolean value that determines whether to limit the maximum number of redemptions allowed per live stream (see the max_per_stream field). + // The default is false. + IsMaxPerStreamEnabled *bool `json:"is_max_per_stream_enabled"` + + // The maximum number of redemptions allowed per live stream. Applied only if is_max_per_stream_enabled is true. + // The minimum value is 1. + MaxPerStream *int `json:"max_per_stream"` + + // A Boolean value that determines whether to limit the maximum number of redemptions allowed per user per stream (see the max_per_user_per_stream field). + // The default is false. + IsMaxPerUserPerStreamEnabled *bool `json:"is_max_per_user_per_stream_enabled"` + + // The maximum number of redemptions allowed per user per stream. Applied only if is_max_per_user_per_stream_enabled is true. + // The minimum value is 1. + MaxPerUserPerStream *int `json:"max_per_user_per_stream"` + + // A Boolean value that determines whether to apply a cooldown period between redemptions (see the global_cooldown_seconds field for the duration of the cooldown period). + // The default is false. + IsGlobalCooldownEnabled *bool `json:"is_global_cooldown_enabled"` + + // The cooldown period, in seconds. Applied only if the is_global_cooldown_enabled field is true. + // The minimum value is 1; however, the minimum value is 60 for it to be shown in the Twitch UX. + GlobalCooldownSeconds *int `json:"global_cooldown_seconds"` + + // A Boolean value that determines whether redemptions should be set to FULFILLED status immediately when a reward is redeemed. + // If false, status is set to UNFULFILLED and follows the normal request queue process. + // The default is false. + ShouldRedemptionsSkipRequestQueue *bool `json:"should_redemptions_skip_request_queue"` +} + +type CreateCustomRewardsResponse struct { + Data []CustomReward `json:"data"` +} + +// Creates a Custom Reward in the broadcaster’s 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) { + 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 { + w.CloseWithError(err) + } else { + w.Close() + } + }() + + resp, err := c.client.Post(endpoint.String(), "application/json", r) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data CreateCustomRewardsResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channelpoints/delete_custom_reward.go b/api/channelpoints/delete_custom_reward.go new file mode 100644 index 0000000..587b7c7 --- /dev/null +++ b/api/channelpoints/delete_custom_reward.go @@ -0,0 +1,39 @@ +package channelpoints + +import ( + "net/http" + "net/url" + + "github.com/google/go-querystring/query" +) + +type DeleteCustomRewardParams struct { + // The ID of the broadcaster that created the custom reward. This ID must match the user ID found in the OAuth token. + BroadcasterID string `url:"broadcaster_id"` + + // The ID of the custom reward to delete. + ID string `url:"id"` +} + +// Deletes a custom reward that the broadcaster created. +// +// The app used to create the reward is the only app that may delete it. +// If the reward’s 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 { + 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, + }) + if err != nil { + return err + } + + defer resp.Body.Close() + + return nil +} diff --git a/api/channelpoints/get_custom_reward.go b/api/channelpoints/get_custom_reward.go new file mode 100644 index 0000000..fae6f22 --- /dev/null +++ b/api/channelpoints/get_custom_reward.go @@ -0,0 +1,53 @@ +package channelpoints + +import ( + "encoding/json" + "net/url" + + "github.com/google/go-querystring/query" +) + +type GetCustomRewardParams struct { + // The ID of the broadcaster whose custom rewards you want to get. This ID must match the user ID found in the OAuth token. + BroadcasterID string `url:"broadcaster_id"` + + // A list of IDs to filter the rewards by. To specify more than one ID, include this parameter for each reward you want to get. + // For example, id=1234&id=5678. You may specify a maximum of 50 IDs. + // + // Duplicate IDs are ignored. The response contains only the IDs that were found. If none of the IDs were found, the response is 404 Not Found. + IDs []string `url:"id,omitempty"` + + // A Boolean value that determines whether the response contains only the custom rewards that the app may manage + // (the app is identified by the ID in the Client-Id header). Set to true to get only the custom rewards that the app may manage. + // The default is false. + OnlyManageableRewards *bool `url:"only_manageable_rewards,omitempty"` +} + +type GetCustomRewardResponse struct { + // A list of custom rewards. The list is in ascending order by id. If the broadcaster hasn’t created custom rewards, the list is empty. + Data []CustomReward `json:"data"` +} + +// Gets a list of custom rewards that the specified broadcaster created. +// +// 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) { + 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()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetCustomRewardResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channelpoints/get_custom_reward_redemption.go b/api/channelpoints/get_custom_reward_redemption.go new file mode 100644 index 0000000..a63d876 --- /dev/null +++ b/api/channelpoints/get_custom_reward_redemption.go @@ -0,0 +1,69 @@ +package channelpoints + +import ( + "encoding/json" + "net/url" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetCustomRewardRedemptionParams struct { + // The ID of the broadcaster that owns the custom reward. This ID must match the user ID found in the user OAuth token. + BroadcasterID string `url:"broadcaster_id"` + + // The ID that identifies the custom reward whose redemptions you want to get. + RewardID string `url:"reward_id"` + + // The status of the redemptions to return. The possible case-sensitive values are: + // + // NOTE: This field is required only if you don’t specify the id query parameter. + // + // NOTE: Canceled and fulfilled redemptions are returned for only a few days after they’re canceled or fulfilled. + Status *RewardRedemptionStatus `url:"status,omitempty"` + + // A list of IDs to filter the redemptions by. To specify more than one ID, include this parameter for each redemption you want to get. + // For example, id=1234&id=5678. You may specify a maximum of 50 IDs. + // + // Duplicate IDs are ignored. The response contains only the IDs that were found. If none of the IDs were found, the response is 404 Not Found. + IDs []string `url:"id,omitempty"` + + // The order to sort redemptions by. The default is OLDEST. + Sort *types.SortOrder `url:"sort,omitempty"` + + // The cursor used to get the next page of results. The Pagination object in the response contains the cursor’s value. + // Read more: https://dev.twitch.tv/docs/api/guide/#pagination + After *string `url:"after,omitempty"` + + // The maximum number of redemptions to return per page in the response. + // The minimum page size is 1 redemption per page and the maximum is 50. + // The default is 20. + First *int `url:"first,omitempty"` +} + +type GetCustomRewardRedemptionResponse struct { + // The list of redemptions for the specified reward. The list is empty if there are no redemptions that match the redemption criteria. + Data []CustomRewardRedemption `json:"data"` +} + +// 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) { + 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()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetCustomRewardRedemptionResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channelpoints/modals.go b/api/channelpoints/modals.go new file mode 100644 index 0000000..c6561cb --- /dev/null +++ b/api/channelpoints/modals.go @@ -0,0 +1,153 @@ +package channelpoints + +import "time" + +type CustomReward struct { + // The ID that uniquely identifies the broadcaster. + BroadcasterID string `json:"broadcaster_id"` + + // The broadcaster’s login name. + BroadcasterLogin string `json:"broadcaster_login"` + + // The broadcaster’s display name. + BroadcasterName string `json:"broadcaster_name"` + + // The ID that uniquely identifies this custom reward. + ID string `json:"id"` + + // The title of the custom reward. + Title string `json:"title"` + + // The prompt shown to the viewer when they redeem the reward if user input is required (see the is_user_input_required field). + Prompt string `json:"prompt"` + + // The cost of the reward in Channel Points. + Cost int `json:"cost"` + + // A set of custom images for the reward. + // This field is set to null if the broadcaster didn’t upload images. + CustomImages *CustomRewardImage `json:"custom_images"` + + // A set of default images for the reward. + DefaultImage CustomRewardImage `json:"default_image"` + + // The background color to use for the reward. The color is in Hex format (for example, #00E5CB). + BackgroundColor string `json:"background_color"` + + // A Boolean value that determines whether the reward is enabled. + // Is true if enabled; otherwise, false. Disabled rewards aren’t shown to the user. + IsEnabled bool `json:"is_enabled"` + + // A Boolean value that determines whether the user must enter information when redeeming the reward. + // Is true if the reward requires user input. + IsUserInputRequired bool `json:"is_user_input_required"` + + // The settings used to determine whether to apply a maximum to the number to the redemptions allowed per live stream. + MaxPerStreamSetting struct { + // A Boolean value that determines whether the reward applies a limit on the number of redemptions allowed per live stream. + // Is true if the reward applies a limit. + IsEnabled bool `json:"is_enabled"` + + // The maximum number of redemptions allowed per live stream. + MaxPerStream int64 `json:"max_per_stream"` + } `json:"max_per_stream_setting"` + + // The settings used to determine whether to apply a maximum to the number of redemptions allowed per user per live stream. + MaxPerUserPerStreamSetting struct { + // A Boolean value that determines whether the reward applies a limit on the number of redemptions allowed per user per live stream. + // Is true if the reward applies a limit. + IsEnabled bool `json:"is_enabled"` + + // The maximum number of redemptions allowed per user per live stream. + MaxPerUserPerStream int64 `json:"max_per_user_per_stream"` + } `json:"max_per_user_per_stream_setting"` + + // The settings used to determine whether to apply a cooldown period between redemptions and the length of the cooldown. + GlobalCooldownSetting struct { + // A Boolean value that determines whether to apply a cooldown period. Is true if a cooldown period is enabled. + IsEnabled bool `json:"is_enabled"` + + // The cooldown period, in seconds. + GlobalCooldownSeconds int64 `json:"global_cooldown_seconds"` + } `json:"global_cooldown_setting"` + + // A Boolean value that determines whether the reward is currently paused. Is true if the reward is paused. Viewers can’t redeem paused rewards. + IsPaused bool `json:"is_paused"` + + // A Boolean value that determines whether the reward is currently in stock. Is true if the reward is in stock. Viewers can’t redeem out of stock rewards. + IsInStock bool `json:"is_in_stock"` + + // A Boolean value that determines whether redemptions should be set to FULFILLED status immediately when a reward is redeemed. + // If false, status is UNFULFILLED and follows the normal request queue process. + ShouldRedemptionsSkipRequestQueue bool `json:"should_redemptions_skip_request_queue"` + + // The number of redemptions redeemed during the current live stream. The number counts against the max_per_stream_setting limit. + // This field is null if the broadcaster’s stream isn’t live or max_per_stream_setting isn’t enabled. + RedemptionsRedeemedCurrentStream *int `json:"redemptions_redeemed_current_stream"` + + // The timestamp of when the cooldown period expires. Is null if the reward isn’t in a cooldown state (see the global_cooldown_setting field). + CooldownExpiresAt *time.Time `json:"cooldown_expires_at"` +} + +type CustomRewardImage struct { + // The URL to a small version of the image. + URL1X string `json:"url_1x"` + + // The URL to a medium version of the image. + URL2X string `json:"url_2x"` + + // The URL to a large version of the image. + URL4X string `json:"url_4x"` +} + +type RewardRedemptionStatus string + +const ( + RewardRedemptionStatusCanceled RewardRedemptionStatus = "CANCELED" + RewardRedemptionStatusFulfilled RewardRedemptionStatus = "FULFILLED" + RewardRedemptionStatusUnfulfilled RewardRedemptionStatus = "UNFULFILLED" +) + +type CustomRewardRedemption struct { + // The ID that uniquely identifies the broadcaster. + BroadcasterID string `json:"broadcaster_id"` + + // The broadcaster's login name. + BroadcasterLogin string `json:"broadcaster_login"` + + // The broadcaster's display name. + BroadcasterName string `json:"broadcaster_name"` + + // The ID that uniquely identifies this redemption. + ID string `json:"id"` + + // The ID that uniquely identifies the user that redeemed the reward. + UserID string `json:"user_id"` + + // The user's login name. + UserLogin string `json:"user_login"` + + // The user's display name. + UserName string `json:"user_name"` + + // The state of the redemption. + Status RewardRedemptionStatus `json:"status"` + + //The date and time of when the reward was redeemed, in RFC3339 format. + RedeemedAt time.Time `json:"redeemed_at"` + + // The reward that the user redeemed. + Reward struct { + // The ID that uniquely identifies the redeemed reward. + ID string `json:"id"` + + // The reward's title. + Title string `json:"title"` + + // The prompt displayed to the viewer if user input is required. + Prompt string `json:"prompt"` + + // The reward’s cost, in Channel Points. + Cost int64 `json:"cost"` + } `json:"reward"` +} diff --git a/api/channelpoints/update_custom_reward.go b/api/channelpoints/update_custom_reward.go new file mode 100644 index 0000000..b336e4f --- /dev/null +++ b/api/channelpoints/update_custom_reward.go @@ -0,0 +1,113 @@ +package channelpoints + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" +) + +type UpdateCustomRewardParams struct { + // The ID of the broadcaster that’s updating the reward. This ID must match the user ID found in the OAuth token. + BroadcasterID string `url:"broadcaster_id"` + + // The ID of the reward to update. + ID string `url:"id"` +} + +type UpdateCustomRewardRequest struct { + // The custom reward’s title. The title may contain a maximum of 45 characters and it must be unique amongst all of the broadcaster’s custom rewards. + Title *string `json:"title"` + + // The cost of the reward, in Channel Points. + // The minimum is 1 point. + Cost *int64 `json:"cost"` + + // The prompt shown to the viewer when they redeem the reward. + // Specify a prompt if is_user_input_required is true. + // The prompt is limited to a maximum of 200 characters. + Prompt *string `json:"prompt"` + + // A Boolean value that determines whether the reward is enabled. Viewers see only enabled rewards. + // The default is true. + IsEnabled *bool `json:"is_enabled"` + + // The background color to use for the reward. Specify the color using Hex format (for example, #9147FF). + BackgroundColor *string `json:"background_color"` + + // A Boolean value that determines whether the user needs to enter information when redeeming the reward. See the prompt field. + // The default is false. + IsUserInputRequired *bool `json:"is_user_input_required"` + + // A Boolean value that determines whether to limit the maximum number of redemptions allowed per live stream (see the max_per_stream field). + // The default is false. + IsMaxPerStreamEnabled *bool `json:"is_max_per_stream_enabled"` + + // The maximum number of redemptions allowed per live stream. Applied only if is_max_per_stream_enabled is true. + // The minimum value is 1. + MaxPerStream *int `json:"max_per_stream"` + + // A Boolean value that determines whether to limit the maximum number of redemptions allowed per user per stream (see the max_per_user_per_stream field). + // The default is false. + IsMaxPerUserPerStreamEnabled *bool `json:"is_max_per_user_per_stream_enabled"` + + // The maximum number of redemptions allowed per user per stream. Applied only if is_max_per_user_per_stream_enabled is true. + // The minimum value is 1. + MaxPerUserPerStream *int `json:"max_per_user_per_stream"` + + // A Boolean value that determines whether to apply a cooldown period between redemptions (see the global_cooldown_seconds field for the duration of the cooldown period). + // The default is false. + IsGlobalCooldownEnabled *bool `json:"is_global_cooldown_enabled"` + + // The cooldown period, in seconds. Applied only if the is_global_cooldown_enabled field is true. + // The minimum value is 1; however, the minimum value is 60 for it to be shown in the Twitch UX. + GlobalCooldownSeconds *int `json:"global_cooldown_seconds"` + + // A Boolean value that determines whether redemptions should be set to FULFILLED status immediately when a reward is redeemed. + // If false, status is set to UNFULFILLED and follows the normal request queue process. + // The default is false. + ShouldRedemptionsSkipRequestQueue *bool `json:"should_redemptions_skip_request_queue"` +} + +type UpdateCustomRewardResponse struct { + // The list contains the single reward that you updated. + Data []CustomReward `json:"data"` +} + +// 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) + 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 { + w.CloseWithError(err) + } else { + w.Close() + } + }() + + resp, err := c.client.Do(&http.Request{ + Method: http.MethodPatch, + URL: endpoint, + Body: r, + }) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data UpdateCustomRewardResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channelpoints/update_redemption_status.go b/api/channelpoints/update_redemption_status.go new file mode 100644 index 0000000..f5e0f94 --- /dev/null +++ b/api/channelpoints/update_redemption_status.go @@ -0,0 +1,73 @@ +package channelpoints + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" +) + +type UpdateRedemptionStatusParams struct { + // A list of IDs that identify the redemptions to update. To specify more than one ID, include this parameter for each redemption you want to update. + // For example, id=1234&id=5678. You may specify a maximum of 50 IDs. + IDs []string `url:"id"` + + // The ID of the broadcaster that’s updating the redemption. This ID must match the user ID in the user access token. + BroadcasterID string `url:"broadcaster_id"` + + // The ID that identifies the reward that’s been redeemed. + RewardID string `url:"reward_id"` +} + +type UpdateRedemptionStatusRequest struct { + // The status to set the redemption to. Possible values are: + // + // CANCELED, FULFILLED + // + // Setting the status to CANCELED refunds the user’s channel points. + Status RewardRedemptionStatus `json:"status"` +} + +type UpdateRedemptionStatusResponse struct { + // The list contains the single redemption that you updated. + Data []CustomRewardRedemption `json:"data"` +} + +// Updates a redemption’s status. You may update a redemption only if its status is UNFULFILLED. +// 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) + 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 { + w.CloseWithError(err) + } else { + w.Close() + } + }() + + resp, err := c.client.Do(&http.Request{ + Method: http.MethodPatch, + URL: endpoint, + Body: r, + }) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data UpdateRedemptionStatusResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channels/channels.go b/api/channels/channels.go new file mode 100644 index 0000000..fa6130b --- /dev/null +++ b/api/channels/channels.go @@ -0,0 +1,18 @@ +package channels + +import ( + "net/http" + "net/url" +) + +type Channels struct { + client *http.Client + baseUrl *url.URL +} + +func New(client *http.Client, baseUrl *url.URL) *Channels { + return &Channels{ + client: client, + baseUrl: baseUrl, + } +} diff --git a/api/channels/get_channel_editors.go b/api/channels/get_channel_editors.go new file mode 100644 index 0000000..c058e9e --- /dev/null +++ b/api/channels/get_channel_editors.go @@ -0,0 +1,44 @@ +package channels + +import ( + "encoding/json" + "net/url" + "time" +) + +type GetChannelEditorsResponse struct { + // A list of users that are editors for the specified broadcaster. The list is empty if the broadcaster doesn’t have editors. + Data []ChannelEditor `json:"data"` +} + +type ChannelEditor struct { + // An ID that uniquely identifies a user with editor permissions. + UserID string `json:"user_id"` + + // The user’s display name. + UserName string `json:"user_name"` + + // The date and time, in RFC3339 format, when the user became one of the broadcaster’s editors. + CreatedAt time.Time `json:"created_at"` +} + +// Gets the broadcaster’s list editors. +// +// Requires a user access token that includes the channel:read:editors scope. +func (c *Channels) GetChannelEditors(broadcasterID string) (*GetChannelEditorsResponse, error) { + endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channels/editors", RawQuery: "broadcaster_id=" + broadcasterID}) + + resp, err := c.client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetChannelEditorsResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channels/get_channel_followers.go b/api/channels/get_channel_followers.go new file mode 100644 index 0000000..f8f11b3 --- /dev/null +++ b/api/channels/get_channel_followers.go @@ -0,0 +1,87 @@ +package channels + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetChannelFollowersParams struct { + // A user’s ID. Use this parameter to see whether the user follows this broadcaster. + // If specified, the response contains this user if they follow the broadcaster. + // If not specified, the response contains all users that follow the broadcaster. + // + // Using this parameter requires both a user access token with the moderator:read:followers scope and the user ID + // in the access token match the broadcaster_id or be the user ID for a moderator of the specified broadcaster. + UserID *string `url:"user_id"` + + // The broadcaster’s ID. Returns the list of users that follow this broadcaster. + BroadcasterID string `url:"broadcaster_id"` + + // The maximum number of items to return per page in the response. The minimum page size is 1 item per page and the maximum is 100. The default is 20. + First *int `url:"first,omitempty"` + + // The cursor used to get the next page of results. The Pagination object in the response contains the cursor’s value. + // Read more: https://dev.twitch.tv/docs/api/guide#pagination + After *types.Cursor `url:"after,omitempty"` +} + +type GetChannelFollowersResponse struct { + // The list of users that follow the specified broadcaster. The list is in descending order by followed_at (with the most recent follower first). + // The list is empty if nobody follows the broadcaster, the specified user_id isn’t in the follower list, + // the user access token is missing the moderator:read:followers scope, or the user isn’t the broadcaster or moderator for the channel. + Data []ChannelFollower `json:"data"` + + // Contains the information used to page through the list of results. The object is empty if there are no more pages left to page through. + // Read more: https://dev.twitch.tv/docs/api/guide#pagination + Pagination types.Pagination `json:"pagination"` + + // The total number of users that follow this broadcaster. + // As someone pages through the list, the number of users may change as users follow or unfollow the broadcaster. + Total int `json:"total"` +} + +type ChannelFollower struct { + // An ID that uniquely identifies the user that’s following the broadcaster. + UserID string `json:"user_id"` + + // The user’s login name. + UserLogin string `json:"user_login"` + + // The user’s display name. + UserName string `json:"user_name"` + + // The UTC timestamp when the user started following the broadcaster. + FollowedAt time.Time `json:"followed_at"` +} + +// Gets a list of users that follow the specified broadcaster. You can also use this endpoint to see whether a specific user follows the broadcaster. +// +// - Requires a user access token that includes the moderator:read:followers scope. +// +// - The ID in the broadcaster_id query parameter must match the user ID in the access token or the user ID in the access token must be a moderator for the specified broadcaster. +// +// This endpoint will return specific follower information only if both of the above are true. +// If a scope is not provided or the user isn’t 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) { + v, _ := query.Values(params) + endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "channels/followers", RawQuery: v.Encode()}) + + resp, err := c.client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetChannelFollowersResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channels/get_channel_information.go b/api/channels/get_channel_information.go new file mode 100644 index 0000000..e4bce9d --- /dev/null +++ b/api/channels/get_channel_information.go @@ -0,0 +1,86 @@ +package channels + +import ( + "encoding/json" + "net/url" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetChannelInformationParams struct { + // The ID of the broadcaster whose channel you want to get. To specify more than one ID, include this parameter for each broadcaster you want to get. + // For example, broadcaster_id=1234&broadcaster_id=5678. You may specify a maximum of 100 IDs. The API ignores duplicate IDs and IDs that are not found. + BroadcasterIDs []string `url:"broadcaster_id,omitempty"` +} + +type GetChannelInformdationResponse struct { + // A list that contains information about the specified channels. The list is empty if the specified channels weren’t found. + Data []ChannelInformation `json:"data"` +} + +type ChannelInformation struct { + // An ID that uniquely identifies the broadcaster. + BroadcasterID string `json:"broadcaster_id"` + + // The broadcaster’s login name. + BroadcasterLogin string `json:"broadcaster_login"` + + // The broadcaster’s display name. + BroadcasterName string `json:"broadcaster_name"` + + // The broadcaster’s preferred language. The value is an ISO 639-1 two-letter language code (for example, en for English). + // The value is set to “other” if the language is not a Twitch supported language. + BroadcasterLanguage string `json:"broadcaster_language"` + + // The name of the game that the broadcaster is playing or last played. The value is an empty string if the broadcaster has never played a game. + GameName string `json:"game_name"` + + // An ID that uniquely identifies the game that the broadcaster is playing or last played. + // The value is an empty string if the broadcaster has never played a game. + GameID string `json:"game_id"` + + // The title of the stream that the broadcaster is currently streaming or last streamed. The value is an empty string if the broadcaster has never streamed. + Title string `json:"title"` + + // The value of the broadcaster’s stream delay setting, in seconds. + // This field’s value defaults to zero unless + // + // 1) the request specifies a user access token + // + // 2) the ID in the broadcaster_id query parameter matches the user ID in the access token + // + // 3) the broadcaster has partner status and they set a non-zero stream delay value. + Delay uint `json:"delay"` + + // The tags applied to the channel. + Tags []string `json:"tags"` + + // The CCLs applied to the channel. + ContentClassficationLabels []types.CCL `json:"content_classification"` + + // Boolean flag indicating if the channel has branded content. + IsBrandedContent bool `json:"is_branded_content"` +} + +// Gets information about one or more channels. +// +// Requires an app access token or user access token. +func (c *Channels) GetChannelInformation(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()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetChannelInformdationResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channels/get_followed_channels.go b/api/channels/get_followed_channels.go new file mode 100644 index 0000000..6b8b808 --- /dev/null +++ b/api/channels/get_followed_channels.go @@ -0,0 +1,78 @@ +package channels + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetFollowedChannelsParams struct { + // A user’s ID. Returns the list of broadcasters that this user follows. This ID must match the user ID in the user OAuth token. + UserID string `url:"user_id"` + + // A broadcaster’s ID. Use this parameter to see whether the user follows this broadcaster. + // If specified, the response contains this broadcaster if the user follows them. + // If not specified, the response contains all broadcasters that the user follows. + BroadcasterID *string `url:"broadcaster_id,omitempty"` + + // The maximum number of items to return per page in the response. The minimum page size is 1 item per page and the maximum is 100. The default is 20. + First *int `url:"first,omitempty"` + + // The cursor used to get the next page of results. The Pagination object in the response contains the cursor’s value. + // Read more: https://dev.twitch.tv/docs/api/guide#pagination + After *types.Cursor `url:"after,omitempty"` +} + +type GetFollowedChannelsResponse struct { + // The list of broadcasters that the user follows. + // The list is in descending order by followed_at (with the most recently followed broadcaster first) + // The list is empty if the user doesn’t follow anyone. + Data []FollowedChannel `json:"data"` + + // Contains the information used to page through the list of results. The object is empty if there are no more pages left to page through. + // Read more: https://dev.twitch.tv/docs/api/guide#pagination + Pagination types.Pagination `json:"pagination"` + + // The total number of broadcasters that the user follows. + // As someone pages through the list, the number may change as the user follows or unfollows broadcasters. + Total int `json:"total"` +} + +type FollowedChannel struct { + // An ID that uniquely identifies the broadcaster that this user is following. + BroadcasterID string `json:"broadcaster_id"` + + // The broadcaster’s login name. + BroadcasterLogin string `json:"broadcaster_login"` + + // The broadcaster’s display name. + BroadcasterName string `json:"broadcaster_name"` + + // The UTC timestamp when the user started following the broadcaster. + FollowedAt time.Time `json:"followed_at"` +} + +// 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) { + v, _ := query.Values(params) + endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "users/follows", RawQuery: v.Encode()}) + + resp, err := c.client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + var data GetFollowedChannelsResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/channels/modify_channel_information.go b/api/channels/modify_channel_information.go new file mode 100644 index 0000000..dee49fa --- /dev/null +++ b/api/channels/modify_channel_information.go @@ -0,0 +1,75 @@ +package channels + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + + "go.fifitido.net/twitch/api/types" +) + +type ModifyChannelInformationRequest struct { + // The ID of the game that the user plays. The game is not updated if the ID isn’t a game ID that Twitch recognizes. + // To unset this field, use “0” or “” (an empty string). + GameID *string `json:"game_id,omitempty"` + + // The user’s preferred language. Set the value to an ISO 639-1 two-letter language code (for example, en for English). + // Set to “other” if the user’s preferred language is not a Twitch supported language. + // The language isn’t updated if the language code isn’t a Twitch supported language. + Language *string `json:"language,omitempty"` + + // The title of the user’s stream. You may not set this field to an empty string. + Title *string `json:"title,omitempty"` + + // The number of seconds you want your broadcast buffered before streaming it live. The delay helps ensure fairness during competitive play. + // Only users with Partner status may set this field. The maximum delay is 900 seconds (15 minutes). + Delay *int `json:"delay,omitempty"` + + // A list of channel-defined tags to apply to the channel. To remove all tags from the channel, set tags to an empty array. + // Tags help identify the content that the channel streams. Learn More: https://help.twitch.tv/s/article/guide-to-tags + // + // A channel may specify a maximum of 10 tags. + // Each tag is limited to a maximum of 25 characters and may not be an empty string or contain spaces or special characters. + // Tags are case insensitive. For readability, consider using camelCasing or PascalCasing. + Tags *[]string `json:"tags,omitempty"` + + // List of labels that should be set as the Channel’s CCLs. + ContentClassificationLabels []ModifyContentClassificationLabel `json:"content_classification_labels,omitempty"` + + // Boolean flag indicating if the channel has branded content. + IsBrandedContent *bool `json:"is_branded_content,omitempty"` +} + +type ModifyContentClassificationLabel struct { + // ID of the Content Classification Labels that must be added/removed from the channel. + ID types.CCL `json:"id"` + + // Boolean flag indicating whether the label should be enabled (true) or disabled for the channel. + IsEnabled bool `json:"is_enabled"` +} + +// Updates a channel’s properties. +// +// Requires a user access token that includes the channel:manage:broadcast scope. +func (c *Channels) ModifyChannelInformation(broadcasterID string, req *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 { + w.CloseWithError(err) + } else { + w.Close() + } + }() + + _, err := c.client.Do(&http.Request{ + Method: http.MethodPatch, + URL: endpoint, + Body: r, + }) + + return err +} diff --git a/api/eventsub/create_subscription.go b/api/eventsub/create_subscription.go new file mode 100644 index 0000000..d98ff9a --- /dev/null +++ b/api/eventsub/create_subscription.go @@ -0,0 +1,48 @@ +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 +} diff --git a/api/eventsub/delete_subscription.go b/api/eventsub/delete_subscription.go new file mode 100644 index 0000000..3e677a5 --- /dev/null +++ b/api/eventsub/delete_subscription.go @@ -0,0 +1,20 @@ +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 +} diff --git a/api/eventsub/eventsub.go b/api/eventsub/eventsub.go new file mode 100644 index 0000000..1d4e191 --- /dev/null +++ b/api/eventsub/eventsub.go @@ -0,0 +1,18 @@ +package eventsub + +import ( + "net/http" + "net/url" +) + +type EventSub struct { + client *http.Client + baseUrl *url.URL +} + +func New(client *http.Client, baseUrl *url.URL) *EventSub { + return &EventSub{ + client: client, + baseUrl: baseUrl, + } +} diff --git a/api/eventsub/get_subscriptions.go b/api/eventsub/get_subscriptions.go new file mode 100644 index 0000000..72001ce --- /dev/null +++ b/api/eventsub/get_subscriptions.go @@ -0,0 +1,43 @@ +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 +} diff --git a/api/eventsub/models.go b/api/eventsub/models.go new file mode 100644 index 0000000..ac27c04 --- /dev/null +++ b/api/eventsub/models.go @@ -0,0 +1,140 @@ +package eventsub + +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"` + Transport *Transport `json:"transport"` + Cost int `json:"cost"` +} + +type Status string + +const ( + StatusEnabled = "enabled" + 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" +) + +type Transport struct { + Method string `json:"method"` + Callback *string `json:"callback,omitempty"` + Secret *string `json:"secret,omitempty"` + SessionID *string `json:"session_id,omitempty"` + ConduitID *string `json:"conduit_id,omitempty"` +} + +func WebhookTransport(callback string, secret string) *Transport { + return &Transport{ + Method: "webhook", + Callback: &callback, + Secret: &secret, + SessionID: nil, + ConduitID: nil, + } +} + +func WebSocketTransport(sessionID string) *Transport { + return &Transport{ + Method: "websocket", + Callback: nil, + Secret: nil, + SessionID: &sessionID, + ConduitID: nil, + } +} + +func ConduitTransport(conduitID string) *Transport { + return &Transport{ + Method: "websocket", + Callback: nil, + Secret: nil, + SessionID: nil, + ConduitID: &conduitID, + } +} + +type SubscriptionType struct { + Name string `json:"type"` + Version string `json:"version"` +} + +var ( + ChannelUpdate = SubscriptionType{Name: "channel.update", Version: "2"} + ChannelFollow = SubscriptionType{Name: "channel.follow", Version: "2"} + ChannelAdBreakBegin = SubscriptionType{Name: "channel.ad_break_begin", Version: "1"} + ChannelChatClear = SubscriptionType{Name: "channel.chat.clear", Version: "1"} + ChannelChatClearUserMessages = SubscriptionType{Name: "channel.chat.clear_user_messages", Version: "1"} + ChannelChatMessage = SubscriptionType{Name: "channel.chat.message", Version: "1"} + ChannelChatMessageDelete = SubscriptionType{Name: "channel.chat.message_delete", Version: "1"} + ChannelChatNotification = SubscriptionType{Name: "channel.chat.notification", Version: "1"} + ChannelChatSettingsUpdate = SubscriptionType{Name: "channel.chat_settings.update", Version: "beta"} + ChannelSubscribe = SubscriptionType{Name: "channel.subscribe", Version: "1"} + ChannelSubscriptionEnd = SubscriptionType{Name: "channel.subscription.end", Version: "1"} + ChannelSubscriptionGift = SubscriptionType{Name: "channel.subscription.gift", Version: "1"} + ChannelSubscriptionMessage = SubscriptionType{Name: "channel.subscription.message", Version: "1"} + ChannelCheer = SubscriptionType{Name: "channel.cheer", Version: "1"} + ChannelRaid = SubscriptionType{Name: "channel.raid", Version: "1"} + ChannelBan = SubscriptionType{Name: "channel.ban", Version: "1"} + ChannelUnban = SubscriptionType{Name: "channel.unban", Version: "1"} + ChannelModeratorAdd = SubscriptionType{Name: "channel.moderator.add", Version: "1"} + ChannelModeratorRemove = SubscriptionType{Name: "channel.moderator.remove", Version: "1"} + ChannelGuestStarSessionBegin = SubscriptionType{Name: "channel.guest_star_session.begin", Version: "beta"} + ChannelGuestStarSessionEnd = SubscriptionType{Name: "channel.guest_star_session.end", Version: "beta"} + ChannelGuestStarGuestUpdate = SubscriptionType{Name: "channel.guest_star_guest.update", Version: "beta"} + ChannelGuestStarSettingsUpdate = SubscriptionType{Name: "channel.guest_star_settings.update", Version: "beta"} + ChannelPointsCustomRewardAdd = SubscriptionType{Name: "channel.channel_points_custom_reward.add", Version: "1"} + ChannelPointsCustomRewardUpdate = SubscriptionType{Name: "channel.channel_points_custom_reward.update", Version: "1"} + ChannelPointsCustomRewardRemove = SubscriptionType{Name: "channel.channel_points_custom_reward.remove", Version: "1"} + ChannelPointsCustomRewardRedemptionAdd = SubscriptionType{Name: "channel.channel_points_custom_reward_redemption.add", Version: "1"} + ChannelPointsCustomRewardRedemptionUpdate = SubscriptionType{Name: "channel.channel_points_custom_reward_redemption.update", Version: "1"} + ChannelPollBegin = SubscriptionType{Name: "channel.poll.begin", Version: "1"} + ChannelPollProgress = SubscriptionType{Name: "channel.poll.progress", Version: "1"} + ChannelPollEnd = SubscriptionType{Name: "channel.poll.end", Version: "1"} + ChannelPredictionBegin = SubscriptionType{Name: "channel.prediction.begin", Version: "1"} + ChannelPredictionProgress = SubscriptionType{Name: "channel.prediction.progress", Version: "1"} + ChannelPredictionLock = SubscriptionType{Name: "channel.prediction.lock", Version: "1"} + ChannelPredictionEnd = SubscriptionType{Name: "channel.prediction.end", Version: "1"} + CharityCampaignDonate = SubscriptionType{Name: "channel.charity_campaign.donate", Version: "1"} + CharityCampaignStart = SubscriptionType{Name: "channel.charity_campaign.start", Version: "1"} + CharityCampaignProgress = SubscriptionType{Name: "channel.charity_campaign.progress", Version: "1"} + CharityCampaignStop = SubscriptionType{Name: "channel.charity_campaign.stop", Version: "1"} + ConduitShardDisabled = SubscriptionType{Name: "conduit.shard.disabled", Version: "1"} + DropEntitlementGrant = SubscriptionType{Name: "drop.entitlement.grant", Version: "1"} + ExtensionBitsTransactionCreate = SubscriptionType{Name: "extension.bits.transaction.create", Version: "1"} + GoalBegin = SubscriptionType{Name: "goal.begin", Version: "1"} + GoalProgress = SubscriptionType{Name: "goal.progress", Version: "1"} + GoalEnd = SubscriptionType{Name: "goal.end", Version: "1"} + HypeTrainBegin = SubscriptionType{Name: "hype_train.begin", Version: "1"} + HypeTrainProgress = SubscriptionType{Name: "hype_train.progress", Version: "1"} + HypeTrainEnd = SubscriptionType{Name: "hype_train.end", Version: "1"} + ShieldModeBegin = SubscriptionType{Name: "shield_mode.begin", Version: "1"} + ShieldModeEnd = SubscriptionType{Name: "shield_mode.end", Version: "1"} + ShoutoutCreate = SubscriptionType{Name: "shoutout.create", Version: "1"} + ShoutoutReceived = SubscriptionType{Name: "shoutout.received", Version: "1"} + StreamOnline = SubscriptionType{Name: "stream.online", Version: "1"} + StreamOffline = SubscriptionType{Name: "stream.offline", Version: "1"} + UserAuthorizationGrant = SubscriptionType{Name: "user.authorization.grant", Version: "1"} + UserAuthorizationRevoke = SubscriptionType{Name: "user.authorization.revoke", Version: "1"} + UserUpdate = SubscriptionType{Name: "user.update", Version: "1"} +) diff --git a/api/types/types.go b/api/types/types.go new file mode 100644 index 0000000..bfbb030 --- /dev/null +++ b/api/types/types.go @@ -0,0 +1,66 @@ +package types + +import ( + "encoding/json" + "time" +) + +type Cursor string + +func (c Cursor) String() string { + return string(c) +} + +func (c *Cursor) UnmarshalText(text []byte) error { + *c = Cursor(string(text)) + return nil +} + +func (c Cursor) MarshalText() ([]byte, error) { + return []byte(string(c)), nil +} + +func (c *Cursor) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + *c = Cursor(s) + return nil +} + +func (c Cursor) MarshalJSON() ([]byte, error) { + return json.Marshal(string(c)) +} + +// Contains the information used to page through the list of results. +// The object is empty if there are no more pages left to page through. +type Pagination struct { + // The cursor used to get the next page of results. Use the cursor to set the request’s after query parameter. + Cursor Cursor `json:"cursor"` +} + +type DateRange struct { + StartedAt time.Time `json:"started_at"` + EndedAt time.Time `json:"ended_at"` +} + +// Content Classification Label +type CCL string + +const ( + CCLDrugsIntoxication CCL = "DrugsIntoxication" + CCLSexualThemes CCL = "SexualThemes" + CCLViolentGraphic CCL = "ViolentGraphic" + CCLGambling CCL = "Gambling" + CCLProfanityVulgarity CCL = "ProfanityVulgarity" +) + +// Sort Order +type SortOrder string + +const ( + SortOrderOldest SortOrder = "OLDEST" + SortOrderNewest SortOrder = "NEWEST" +) diff --git a/eventsub/eventsub.go b/eventsub/eventsub.go new file mode 100644 index 0000000..ee86933 --- /dev/null +++ b/eventsub/eventsub.go @@ -0,0 +1,48 @@ +package eventsub + +import ( + "go.fifitido.net/twitch/api" + "go.fifitido.net/twitch/api/eventsub" +) + +type EventSub struct { + api *api.API + transport *eventsub.Transport + + subscriptions map[string]*eventsub.Subscription +} + +func New(api *api.API, trans *eventsub.Transport) *EventSub { + return &EventSub{ + api: api, + transport: trans, + } +} + +func (e *EventSub) Subscribe(subType eventsub.SubscriptionType, cond eventsub.Condition) error { + res, err := e.api.EventSub.CreateSubscription(&eventsub.CreateSubscriptionRequest{ + SubscriptionType: subType, + Condition: cond, + Transport: e.transport, + }) + + if err != nil { + return err + } + + for _, sub := range res.Data { + e.subscriptions[sub.ID] = sub + } + + return nil +} + +func (e *EventSub) Close() error { + for _, sub := range e.subscriptions { + if err := e.api.EventSub.DeleteSubscription(sub.ID); err != nil { + return err + } + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..39d7188 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module go.fifitido.net/twitch + +go 1.21.7 + +require github.com/google/go-querystring v1.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f99081b --- /dev/null +++ b/go.sum @@ -0,0 +1,5 @@ +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/ptr_types.go b/ptr_types.go new file mode 100644 index 0000000..de3f95c --- /dev/null +++ b/ptr_types.go @@ -0,0 +1,152 @@ +package twitch + +import ( + "time" + + "go.fifitido.net/twitch/api/bits" + "go.fifitido.net/twitch/api/channelpoints" + "go.fifitido.net/twitch/api/types" +) + +func String(s string) *string { + return &s +} + +func ToString(s *string) string { + if s == nil { + return "" + } + return *s +} + +func StringSlice(s []string) *[]string { + return &s +} + +func ToStringSlice(s *[]string) []string { + if s == nil { + return []string{} + } + return *s +} + +func Bool(b bool) *bool { + return &b +} + +func ToBool(b *bool) bool { + if b == nil { + return false + } + return *b +} + +func Int(i int) *int { + return &i +} + +func ToInt(i *int) int { + if i == nil { + return 0 + } + return *i +} + +func Int64(i int64) *int64 { + return &i +} + +func ToInt64(i *int64) int64 { + if i == nil { + return 0 + } + return *i +} + +func Int32(i int32) *int32 { + return &i +} + +func ToInt32(i *int32) int32 { + if i == nil { + return 0 + } + return *i +} + +func Float32(f float32) *float32 { + return &f +} + +func ToFloat32(f *float32) float32 { + if f == nil { + return 0 + } + return *f +} + +func Float64(f float64) *float64 { + return &f +} + +func ToFloat64(f *float64) float64 { + if f == nil { + return 0 + } + return *f +} + +func Time(t time.Time) *time.Time { + return &t +} + +func ToTime(t *time.Time) time.Time { + if t == nil { + return time.Time{} + } + return *t +} + +func Cursor(c types.Cursor) *types.Cursor { + return &c +} + +func ToCursor(c *types.Cursor) types.Cursor { + if c == nil { + return "" + } + return *c +} + +func BitsPeriod(p bits.Period) *bits.Period { + return &p +} + +func ToBitsPeriod(p *bits.Period) bits.Period { + if p == nil { + return "" + } + return *p +} + +func RewardRedemptionStatus(s channelpoints.RewardRedemptionStatus) *channelpoints.RewardRedemptionStatus { + return &s +} + +func ToRewardRedemptionStatus(s *channelpoints.RewardRedemptionStatus) channelpoints.RewardRedemptionStatus { + if s == nil { + return "" + } + return *s +} + +func SortOrder(s types.SortOrder) *types.SortOrder { + return &s +} + +func ToSortOrder(s *types.SortOrder) types.SortOrder { + if s == nil { + return "" + } + return *s +}