package twitch import ( "fmt" "net/http" "slices" "time" "github.com/nicklaw5/helix/v2" ) type AuthResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` Scope []string `json:"scope"` Time time.Time } var scopes = []string{ "bits:read", "channel:bot", "channel:moderate", "channel:read:hype_train", "channel:read:polls", "channel:read:predictions", "channel:read:redemptions", "channel:read:subscriptions", "chat:edit", "chat:read", "moderator:manage:announcements", "moderator:read:chatters", "moderator:read:followers", "user_read", "user:bot", "user:manage:whispers", "user:read:chat", "whispers:edit", "whispers:read", } func (c *Client) GetAuthorizationURL() string { if c.API == nil { return "twitch-not-configured" } return c.API.GetAuthorizationURL(&helix.AuthorizationURLParams{ ResponseType: "code", Scopes: scopes, }) } // CheckScopes checks if the user has authorized all required scopes // Normally this would be the case but between versions strimertul has changed // the required scopes, and it's possible that some users have not re-authorized // the application with the new scopes. func (c *Client) CheckScopes() (bool, error) { var authResp AuthResponse if err := c.db.GetJSON(AuthKey, &authResp); err != nil { return false, err } // Sort scopes for comparison slices.Sort(authResp.Scope) return slices.Equal(scopes, authResp.Scope), nil } func (c *Client) GetUserClient(forceRefresh bool) (*helix.Client, error) { var authResp AuthResponse if err := c.db.GetJSON(AuthKey, &authResp); err != nil { return nil, err } // Handle token expiration if forceRefresh || time.Now().After(authResp.Time.Add(time.Duration(authResp.ExpiresIn)*time.Second)) { // Refresh tokens refreshed, err := c.API.RefreshUserAccessToken(authResp.RefreshToken) if err != nil { return nil, err } authResp.AccessToken = refreshed.Data.AccessToken authResp.RefreshToken = refreshed.Data.RefreshToken authResp.Time = time.Now().Add(time.Duration(refreshed.Data.ExpiresIn) * time.Second) // Save new token pair err = c.db.PutJSON(AuthKey, authResp) if err != nil { return nil, err } } config := c.Config.Get() return helix.NewClient(&helix.Options{ ClientID: config.APIClientID, ClientSecret: config.APIClientSecret, UserAccessToken: authResp.AccessToken, }) } func (c *Client) GetLoggedUser() (helix.User, error) { if c.User.ID != "" { return c.User, nil } client, err := c.GetUserClient(false) if err != nil { return helix.User{}, fmt.Errorf("failed getting API client for user: %w", err) } users, err := client.GetUsers(&helix.UsersParams{}) if err != nil { return helix.User{}, fmt.Errorf("failed looking up user: %w", err) } if len(users.Data.Users) < 1 { return helix.User{}, fmt.Errorf("no users found") } c.User = users.Data.Users[0] return c.User, nil } func (c *Client) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Get code from params code := req.URL.Query().Get("code") if code == "" { // TODO Nice error page http.Error(w, "missing code", http.StatusBadRequest) return } // Exchange code for access/refresh tokens userTokenResponse, err := c.API.RequestUserAccessToken(code) if err != nil { http.Error(w, "failed auth token request: "+err.Error(), http.StatusInternalServerError) return } err = c.db.PutJSON(AuthKey, AuthResponse{ AccessToken: userTokenResponse.Data.AccessToken, RefreshToken: userTokenResponse.Data.RefreshToken, ExpiresIn: userTokenResponse.Data.ExpiresIn, Scope: userTokenResponse.Data.Scopes, Time: time.Now(), }) if err != nil { http.Error(w, "error saving auth data for user: "+err.Error(), http.StatusInternalServerError) return } w.Header().Add("Content-Type", "text/html") _, _ = fmt.Fprintf(w, `

All done, you can close me now!

`) } type RefreshResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` TokenType string `json:"token_type"` Scope []string `json:"scope"` } func getRedirectURI(baseurl string) string { return fmt.Sprintf("http://%s/twitch/callback", baseurl) }