From e1b6b5ce8836aa5ece85f1a5fa4c7d1e6eb77985 Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Wed, 28 Feb 2024 13:50:07 -0500 Subject: [PATCH] Add conduit endpoints to API --- api/api.go | 3 + api/conduit/conduit.go | 18 ++++++ api/conduit/create_conduits.go | 38 +++++++++++ api/conduit/delete_conduit.go | 28 ++++++++ api/conduit/get_conduit_shards.go | 56 ++++++++++++++++ api/conduit/get_conduits.go | 38 +++++++++++ api/conduit/models.go | 76 ++++++++++++++++++++++ api/conduit/update_conduit_shards.go | 96 ++++++++++++++++++++++++++++ api/conduit/update_conduits.go | 58 +++++++++++++++++ ptr_types.go | 12 ++++ 10 files changed, 423 insertions(+) create mode 100644 api/conduit/conduit.go create mode 100644 api/conduit/create_conduits.go create mode 100644 api/conduit/delete_conduit.go create mode 100644 api/conduit/get_conduit_shards.go create mode 100644 api/conduit/get_conduits.go create mode 100644 api/conduit/models.go create mode 100644 api/conduit/update_conduit_shards.go create mode 100644 api/conduit/update_conduits.go diff --git a/api/api.go b/api/api.go index 6ec27d6..08e761d 100644 --- a/api/api.go +++ b/api/api.go @@ -11,6 +11,7 @@ import ( "go.fifitido.net/twitch/api/channels" "go.fifitido.net/twitch/api/charity" "go.fifitido.net/twitch/api/chat" + "go.fifitido.net/twitch/api/conduit" "go.fifitido.net/twitch/api/eventsub" ) @@ -27,6 +28,7 @@ type API struct { ChannelPoints *channelpoints.ChannelPoints Charity *charity.Charity Chat *chat.Chat + Conduit *conduit.Conduit EventSub *eventsub.EventSub } @@ -45,6 +47,7 @@ func New() *API { ChannelPoints: channelpoints.New(client, baseUrl), Charity: charity.New(client, baseUrl), Chat: chat.New(client, baseUrl), + Conduit: conduit.New(client, baseUrl), EventSub: eventsub.New(client, baseUrl), } } diff --git a/api/conduit/conduit.go b/api/conduit/conduit.go new file mode 100644 index 0000000..eeffdad --- /dev/null +++ b/api/conduit/conduit.go @@ -0,0 +1,18 @@ +package conduit + +import ( + "net/http" + "net/url" +) + +type Conduit struct { + client *http.Client + baseUrl *url.URL +} + +func New(client *http.Client, baseUrl *url.URL) *Conduit { + return &Conduit{ + client: client, + baseUrl: baseUrl, + } +} diff --git a/api/conduit/create_conduits.go b/api/conduit/create_conduits.go new file mode 100644 index 0000000..aced715 --- /dev/null +++ b/api/conduit/create_conduits.go @@ -0,0 +1,38 @@ +package conduit + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +type CreateConduitsResponse struct { + Data []ConduitData `json:"data"` +} + +// Creates a new conduit. +// +// Requires an app access token. +func (c *Conduit) CreateConduits(ctx context.Context, shareCount int) (*CreateConduitsResponse, error) { + endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "eventsub/conduits", RawQuery: "share_count=" + fmt.Sprint(shareCount)}) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), nil) + if err != nil { + return nil, err + } + + res, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data CreateConduitsResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/conduit/delete_conduit.go b/api/conduit/delete_conduit.go new file mode 100644 index 0000000..512bbdd --- /dev/null +++ b/api/conduit/delete_conduit.go @@ -0,0 +1,28 @@ +package conduit + +import ( + "context" + "net/http" + "net/url" +) + +// Deletes a specified conduit. +// Note that it may take some time for Eventsub subscriptions on a deleted conduit to show as disabled when calling Get Eventsub Subscriptions. +// +// Requires an app access token. +func (c *Conduit) DeleteConduit(ctx context.Context, id string) error { + endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "eventsub/conduits", RawQuery: "id=" + id}) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint.String(), nil) + if err != nil { + return err + } + + res, err := c.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + return nil +} diff --git a/api/conduit/get_conduit_shards.go b/api/conduit/get_conduit_shards.go new file mode 100644 index 0000000..3c5205f --- /dev/null +++ b/api/conduit/get_conduit_shards.go @@ -0,0 +1,56 @@ +package conduit + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" + "go.fifitido.net/twitch/api/types" +) + +type GetConduitShardsParams struct { + // Conduit ID. + ConduitID string `url:"conduit_id"` + + // Status to filter by. + Status *Status `url:"status,omitempty"` + + // The cursor used to get the next page of results. The pagination object in the response contains the cursor’s value. + Cursor *types.Cursor `url:"cursor,omitempty"` +} + +type GetConduitShardsResponse struct { + // List of information about a conduit's shards. + Data []Shard `json:"data"` + + // The cursor used to get the next page of results. Use the cursor to set the request’s after query parameter. + Pagination types.Pagination `json:"pagination"` +} + +// Gets a lists of all shards for a conduit. +// +// Requires an app access token. +func (c *Conduit) GetConduitShards(ctx context.Context, params *GetConduitShardsParams) (*GetConduitShardsResponse, error) { + v, _ := query.Values(params) + endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "eventsub/conduits/shards", RawQuery: v.Encode()}) + + 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 + } + defer res.Body.Close() + + var data GetConduitShardsResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/conduit/get_conduits.go b/api/conduit/get_conduits.go new file mode 100644 index 0000000..64760b3 --- /dev/null +++ b/api/conduit/get_conduits.go @@ -0,0 +1,38 @@ +package conduit + +import ( + "context" + "encoding/json" + "net/http" + "net/url" +) + +type GetConduitsResponse struct { + // List of information about the client’s conduits. + Data []ConduitData `json:"data"` +} + +// Gets the conduits for a client ID. +// +// Requires an app access token. +func (c *Conduit) GetConduits(ctx context.Context) (*GetConduitsResponse, error) { + endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "eventsub/conduits"}) + + 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 + } + defer res.Body.Close() + + var data GetConduitsResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/conduit/models.go b/api/conduit/models.go new file mode 100644 index 0000000..d4e2ec2 --- /dev/null +++ b/api/conduit/models.go @@ -0,0 +1,76 @@ +package conduit + +import "time" + +type ConduitData struct { + // Conduit ID. + ID string `json:"id"` + + // Number of shards associated with this conduit. + ShardCount int `json:"shard_count"` +} + +type Shard struct { + // Shard ID. + ID string `json:"id"` + + // The shard status. The subscriber receives events only for enabled shards + Status Status `json:"status"` + + // The transport details used to send the notifications. + Transport *Transport `json:"transport"` +} + +type Status string + +const ( + // enabled — The shard is enabled. + StatusEnabled Status = "enabled" + + // webhook_callback_verification_pending — The shard is pending verification of the specified callback URL. + StatusWebhookCallbackVerificationPending Status = "webhook_callback_verification_pending" + + // webhook_callback_verification_failed — The specified callback URL failed verification. + StatusWebhookCallbackVerificationFailed Status = "webhook_callback_verification_failed" + + // notification_failures_exceeded — The notification delivery failure rate was too high. + NotificationFailuresExceeded Status = "notification_failures_exceeded" + + // websocket_disconnected — The client closed the connection. + WebsocketDisconnected Status = "websocket_disconnected" + + // websocket_failed_ping_pong — The client failed to respond to a ping message. + WebsocketFailedPingPong Status = "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 Status = "websocket_received_inbound_traffic" + + // websocket_connection_unused — The client failed to subscribe to events within the required time. + WebsocketConnectionUnused Status = "websocket_connection_unused" + + // websocket_internal_error — The Twitch WebSocket server experienced an unexpected error. + WebsocketInternalError Status = "websocket_internal_error" + + // websocket_network_timeout — The Twitch WebSocket server timed out writing the message to the client. + WebsocketNetworkTimeout Status = "websocket_network_timeout" + + // websocket_network_error — The Twitch WebSocket server experienced a network error writing the message to the client. + WebsocketnetworkError Status = "websocket_network_error" +) + +type Transport struct { + // The transport method. Possible values are: + // + // webhook, websocket + Method string `json:"method"` + + // The callback URL where the notifications are sent. Included only if method is set to webhook. + Callback *string `json:"callback,omitempty"` + + // The UTC date and time that the WebSocket connection was established. Included only if method is set to websocket. + ConnectedAt *time.Time `json:"connected_at,omitempty"` + + // The UTC date and time that the WebSocket connection was lost. Included only if method is set to websocket. + DisconnectedAt *time.Time `json:"disconnected_at,omitempty"` +} diff --git a/api/conduit/update_conduit_shards.go b/api/conduit/update_conduit_shards.go new file mode 100644 index 0000000..1efd40b --- /dev/null +++ b/api/conduit/update_conduit_shards.go @@ -0,0 +1,96 @@ +package conduit + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" +) + +type UpdateConduitShardsRequest struct { + // Conduit ID. + ConduitID string `json:"conduit_id"` + + // List of Shards to update. + Shards []ShardUpdate `json:"shards"` +} + +type ShardUpdate struct { + // Shard ID. + ID string `json:"id"` + + // The transport details that you want Twitch to use when sending you notifications. + Transport *Transport `json:"transport"` +} + +type UpdateConduitShardsResponse struct { + // List of successful shard updates. + Data []ConduitData `json:"data"` + + // List of unsuccessful updates. + Errors []UpdateConduitShardsError `json:"errors"` +} + +type UpdateConduitShardsError struct { + // Shard ID. + ID string `json:"id"` + + // The error that occurred while updating the shard. + // Possible errors: + // + // The length of the string in the secret field is not valid. + // + // The URL in the transport's callback field is not valid. The URL must use the HTTPS protocol and the 443 port number. + // + // The value specified in the method field is not valid. + // + // The callback field is required if you specify the webhook transport method. + // + // The session_id field is required if you specify the WebSocket transport method. + // + // The websocket session is not connected. + // + // The shard id is outside of the conduit’s range. + Message string `json:"message"` + + // Error codes used to represent a specific error condition while attempting to update shards. + Code string `json:"code"` +} + +// Updates shard(s) for a conduit. +// +// NOTE: Shard IDs are indexed starting at 0, so a conduit with a shard_count of 5 will have shards with IDs 0 through 4. +// +// Requires an app access token. +func (c *Conduit) UpdateConduitShards(ctx context.Context, body *UpdateConduitShardsRequest) (*UpdateConduitShardsResponse, error) { + endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "eventsub/conduits/shards"}) + + 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.MethodPatch, endpoint.String(), r) + if err != nil { + return nil, err + } + + res, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data UpdateConduitShardsResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/api/conduit/update_conduits.go b/api/conduit/update_conduits.go new file mode 100644 index 0000000..3edb247 --- /dev/null +++ b/api/conduit/update_conduits.go @@ -0,0 +1,58 @@ +package conduit + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" +) + +type UpdateConduitsRequest struct { + // Conduit ID. + ID string `json:"id"` + + // The new number of shards for this conduit. + ShardCount int `json:"shard_count"` +} + +type UpdateConduitsResponse struct { + // List of information about the client’s conduits. + Data []ConduitData `json:"data"` +} + +// Updates a conduit’s shard count. To delete shards, update the count to a lower number, and the shards above the count will be deleted. +// For example, if the existing shard count is 100, by resetting shard count to 50, shards 50-99 are disabled. +// +// Requires an app access token. +func (c *Conduit) UpdateConduits(ctx context.Context, body *UpdateConduitsRequest) (*UpdateConduitsResponse, error) { + endpoint := c.baseUrl.ResolveReference(&url.URL{Path: "eventsub/conduits"}) + + 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.MethodPatch, endpoint.String(), r) + if err != nil { + return nil, err + } + + res, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var data UpdateConduitsResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/ptr_types.go b/ptr_types.go index c49310f..d6134d6 100644 --- a/ptr_types.go +++ b/ptr_types.go @@ -5,6 +5,7 @@ import ( "go.fifitido.net/twitch/api/bits" "go.fifitido.net/twitch/api/channelpoints" + "go.fifitido.net/twitch/api/conduit" "go.fifitido.net/twitch/api/types" ) @@ -161,3 +162,14 @@ func ToAccentColor(s *types.AccentColor) types.AccentColor { } return *s } + +func ConduitStatus(s conduit.Status) *conduit.Status { + return &s +} + +func ToConduitStatus(s *conduit.Status) conduit.Status { + if s == nil { + return "" + } + return *s +}