Skip to content

Commit

Permalink
PROPOSAL: helix extension endpoints
Browse files Browse the repository at this point in the history
- JWT signing
- extension configuration segments endpoints
- extension secrets endpoints
- UPDATE extension docs
  • Loading branch information
jackmcguire1 committed Jul 27, 2021
1 parent 343474f commit 43e48e7
Show file tree
Hide file tree
Showing 11 changed files with 542 additions and 5 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ If you are looking for the Twitch API docs, see the [Twitch Developer website](h
- [ ] Get Extension Secret
- [ ] Revoke Extension Secrets
- [ ] Get Live Channels with Extension Activated
- [ ] Set Extension Required Configuration
- [ ] Set Extension Configuration Segment
- [x] Set Extension Required Configuration
- [x] Set Extension Configuration Segment
- [ ] Get Extension Channel Configuration
- [ ] Get Extension Configuration Segment
- [ ] Send Extension PubSub Message
- [ ] Send Extension Chat Message
- [x] Get Extension Configuration Segment
- [x] Send Extension PubSub Message
- [x] Send Extension Chat Message

## Quick Usage Example

Expand Down
93 changes: 93 additions & 0 deletions docs/extensions_docs.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,98 @@
# Extensions Documentation

## Extension Helix Requests

#### Generate PUBSUB JWT Permissions
> relevant PUBSUB permission must be passed to the 'ExtensionCreateClaims()' func, in order to correctly publish a pubsub message of a particular type
Broadcast pubsub type
```go
client.FormBroadcastSendPubSubPermissions()
```

Global pubsub type
```go
perms := client.FormGlobalSendPubSubPermissions()
```

Whisper User type
```go
client.FormWhisperSendPubSubPermissions(userId)
```

### JWT ROLES
> Note:- Currently only the 'external' role is supported by helix endpoints

### EBS JWT
this is used to set the correct header for any Extension helix requests

```go
client, err := helix.NewClient(&helix.Options{
ClientID: "your-client-id",
UserAccessToken: "your-user-access-token",
ExtensionOpts: helix.ExtensionOptions{
OwnerUserID: os.Getenv(""),
Secret: os.Getenv(""),
ConfigurationVersion: os.Getenv(""),
Version: os.Getenv(""),
},
})


// see docs below to see what permissions and roles you can pass
claims, err := client.ExtensionCreateClaims(broadcasterID, ExternalRole, client.FormBroadcastSendPubSubPermissions(), 0)
if err != nil {
// handle err
}

jwt,err := client.ExtensionJWTSign(claims)
if err != nil {
// handle err
}

// set this before doing extension endpoint requests
client.SetExtensionSignedJWTToken(jwt)
```
## Get Extension Configuration Segments

```go

client, err := helix.NewClient(&helix.Options{
ClientID: "your-client-id",
UserAccessToken: "your-user-access-token",
ExtensionOpts: helix.ExtensionOptions{
OwnerUserID: os.Getenv("EXT_OWNER_ID"),
Secret: os.Getenv("EXT_SECRET"),
ConfigurationVersion: os.Getenv("EXT_CFG_VERSION"),
Version: os.Getenv("EXT_VERSION"),
},
})
if err != nil {
// handle error
}

claims, err := client.ExtensionCreateClaims(broadcasterID, ExternalRole, FormBroadcastSendPubSubPermissions(), 0)
if err != nil {
// handle error
}

// set the JWT token to be used as in the Auth bearer header
jwt := client.ExtensionJWTSign(claims)
client.SetExtensionSignedJWTToken(jwt)

params := helix.ExtensionGetConfigurationParams{
ExtensionID: "some-extension-id", // Required
Segments: []helix.ExtensionSegmentType{helix.GlobalSegment}, // Optional
}
resp, err := client.GetExtensionConfigurationSegment
if err != nil {
// handle error
}

fmt.Printf("%+v\n", resp)
```

## Get Extension Transactions

```go
Expand Down
97 changes: 97 additions & 0 deletions extension_configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package helix

// SegmentType A segment configuration type
type ExtensionSegmentType string

// Types of segments datastores for the configuration service
const (
BroadcasterSegment ExtensionSegmentType = "broadcaster"
DeveloperSegment ExtensionSegmentType = "developer"
GlobalSegment ExtensionSegmentType = "global"
)

func (s ExtensionSegmentType) String() string {
return string(s)
}

type ExtensionConfigurationParams struct {
Segment ExtensionSegmentType `json:"segment"`
ExtensionId string `json:"extension-id"`
Version string `json:"version"`
Content string `json:"content"`
}

type ExtensionConfigurationSegment struct {
Segment ExtensionSegmentType `json:"segment"`
BroadcasterID string `json:"broadcaster_id,omitempty"` // populated if segment is of type 'developer' || 'broadcaster'
Version string `json:"version"`
Content string `json:"content"`
}

type ExtensionGetConfigurationParams struct {
ExtensionID string `query:"extension_id"`
Segment []ExtensionSegmentType `query:"segment"`
}

type ExtensionSetRequiredConfigurationParams struct {
BroadcasterID string `json:"-" query:"broadcaster_id"`
ExtensionID string `json:"extension_id"`
ExtensionVersion string `json:"extension_version"`
RequiredConfiguration string `json:"required_configuration"`
}

type ExtensionSetRequiredConfigurationResponse struct {
ResponseCommon
}

type ExtensionGetConfigurationSegmentResponse struct {
ResponseCommon
Data ManyExtensionConfigurationSegments
}

type ManyExtensionConfigurationSegments struct {
Segments []ExtensionConfigurationSegment
}

type ExtensionSetConfigurationResponse struct {
ResponseCommon
}

// https://dev.twitch.tv/docs/extensions/reference/#set-extension-configuration-segment
func (c *Client) SetExtensionSegmentConfig(params *ExtensionConfigurationParams) (*ExtensionSetConfigurationResponse, error) {
resp, err := c.putAsJSON("/extensions/configurations", &ManyPolls{}, params)
if err != nil {
return nil, err
}

setExtCnfgResp := &ExtensionSetConfigurationResponse{}
resp.HydrateResponseCommon(&setExtCnfgResp.ResponseCommon)

return setExtCnfgResp, nil
}

func (c *Client) GetExtensionConfigurationSegment(params *ExtensionGetConfigurationParams) (*ExtensionGetConfigurationSegmentResponse, error) {
resp, err := c.get("/extensions/configurations", &ManyExtensionConfigurationSegments{}, params)
if err != nil {
return nil, err
}

extCfgSegResp := &ExtensionGetConfigurationSegmentResponse{}
resp.HydrateResponseCommon(&extCfgSegResp.ResponseCommon)
extCfgSegResp.Data.Segments = resp.Data.(*ManyExtensionConfigurationSegments).Segments

return extCfgSegResp, nil
}

func (c *Client) SetExtensionRequiredConfiguration(params *ExtensionSetRequiredConfigurationParams) (*ExtensionSetRequiredConfigurationResponse, error) {

resp, err := c.putAsJSON("/extensions/configurations/required_configuration", &ExtensionSetRequiredConfigurationResponse{}, params)
if err != nil {
return nil, err
}

extReqCfgResp := &ExtensionSetRequiredConfigurationResponse{}
resp.HydrateResponseCommon(&extReqCfgResp.ResponseCommon)

return extReqCfgResp, nil
}
155 changes: 155 additions & 0 deletions extension_jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package helix

import (
"encoding/base64"
"fmt"
"time"

"github.com/dgrijalva/jwt-go"
)

// RoleType The user role type
type roleType string

// Types of user roles used within the JWT Claims
// TODO expose these when helix supports them
const (
BroadcasterRole roleType = "broadcaster"
ExternalRole roleType = "external"
ModeratorRole roleType = "moderator"
ViewerRole roleType = "viewer"

toAllChannels = "all"
)

// PubSubPermissions publish permissions used within
// JWT claims
type PubSubPermissions struct {
Send []ExtensionPubSubPublishType `json:"send,omitempty"`
Listen []ExtensionPubSubPublishType `json:"listen,omitempty"`
}

// TwitchJWTClaims contains information
// containing twitch specific JWT information.
type TwitchJWTClaims struct {
OpaqueUserID string `json:"opaque_user_id,omitempty"`
UserID string `json:"user_id"`
ChannelID string `json:"channel_id,omitempty"`
Role roleType `json:"role"`
Unlinked bool `json:"is_unlinked,omitempty"`
Permissions *PubSubPermissions `json:"pubsub_perms"`
jwt.StandardClaims
}

// CreateClaims will construct a claims suitable for generating a JWT token,
// containing necessary information required by the Twitch API.
// @param BroadcasterID if this value is empty it will default to 'all'
// @param pubsub the pubsub permission to attach to the claim
// @param expiration the epoch of jwt expiration, default 3 minutes from time.Now
func (c *Client) ExtensionCreateClaims(
broadcasterID string,
pubsub *PubSubPermissions,
expiration int64,
) (
*TwitchJWTClaims,
error,
) {
err := c.validateExtensionOpts()
if err != nil {
return nil, err
}

// default expiration to 3 minutes
if expiration == 0 {
expiration = time.Now().Add(time.Minute*3).UnixNano() / int64(time.Millisecond)
}

if broadcasterID == "" {
broadcasterID = toAllChannels
}

claims := &TwitchJWTClaims{
UserID: c.opts.ExtensionOpts.OwnerUserID,
ChannelID: broadcasterID,
Role: ExternalRole,
Permissions: pubsub,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expiration,
},
}

return claims, nil
}

// ExtensionJWTSign Sign the a JWT Claim to produce a base64 token.
func (c *Client) ExtensionJWTSign(claims *TwitchJWTClaims) (tokenString string, err error) {

err = c.validateExtensionOpts()
if err != nil {
return "", err
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

key, err := base64.StdEncoding.DecodeString(c.opts.ExtensionOpts.Secret)
if err != nil {
return
}

tokenString, err = token.SignedString(key)
if err != nil {
return
}

return
}

// ExtensionJWTVerify validates a extension client side twitch base64 token and converts it
// into a twitch claim type, containing relevant information.
func (c *Client) ExtensionJWTVerify(token string) (claims *TwitchJWTClaims, err error) {
if token == "" {
err = fmt.Errorf("JWT token string missing")
return
}

err = c.validateExtensionOpts()
if err != nil {
return nil, err
}

parsedToken, err := jwt.ParseWithClaims(token, &TwitchJWTClaims{}, func(tkn *jwt.Token) (interface{}, error) {
if _, ok := tkn.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %s", tkn.Header["alg"])
}

key, err := base64.StdEncoding.DecodeString(c.opts.ExtensionOpts.Secret)

if err != nil {
return nil, err
}
return key, nil
})
if err != nil {
return
}

claims, ok := parsedToken.Claims.(*TwitchJWTClaims)
if !ok || !parsedToken.Valid {
err = fmt.Errorf("could not parse JWT")
return
}

return
}

func (c *Client) validateExtensionOpts() error {
if c.opts.ExtensionOpts.OwnerUserID == "" {
return fmt.Errorf("extension secret is empty")
}

if c.opts.ExtensionOpts.Secret == "" {
return fmt.Errorf("extension secret is empty")
}

return nil
}
Loading

0 comments on commit 43e48e7

Please sign in to comment.