2022-11-23 21:22:49 +00:00
|
|
|
package twitch
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"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
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) GetAuthorizationURL() string {
|
|
|
|
return c.API.GetAuthorizationURL(&helix.AuthorizationURLParams{
|
|
|
|
ResponseType: "code",
|
|
|
|
Scopes: []string{"bits:read channel:read:subscriptions channel:read:redemptions channel:read:polls channel:read:predictions channel:read:hype_train user_read"},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-11-24 12:16:08 +00:00
|
|
|
func (c *Client) GetUserClient() (*helix.Client, error) {
|
2022-11-23 21:22:49 +00:00
|
|
|
var authResp AuthResponse
|
|
|
|
err := c.db.GetJSON(AuthKey, &authResp)
|
|
|
|
if err != nil {
|
2022-11-24 12:16:08 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// Handle token expiration
|
|
|
|
if time.Now().After(authResp.Time.Add(time.Duration(authResp.ExpiresIn) * time.Second)) {
|
|
|
|
// Refresh tokens
|
2022-12-03 15:16:32 +00:00
|
|
|
refreshed, err := c.API.RefreshUserAccessToken(authResp.RefreshToken)
|
2022-11-24 12:16:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-12-03 15:16:32 +00:00
|
|
|
authResp.AccessToken = refreshed.Data.AccessToken
|
|
|
|
authResp.RefreshToken = refreshed.Data.RefreshToken
|
|
|
|
authResp.Time = time.Now().Add(time.Duration(refreshed.Data.ExpiresIn) * time.Second)
|
2022-11-24 12:16:08 +00:00
|
|
|
|
|
|
|
// Save new token pair
|
|
|
|
err = c.db.PutJSON(AuthKey, authResp)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-11-23 21:22:49 +00:00
|
|
|
}
|
2022-11-24 12:16:08 +00:00
|
|
|
|
2022-12-03 15:16:32 +00:00
|
|
|
config := c.Config.Get()
|
2022-11-24 12:16:08 +00:00
|
|
|
return helix.NewClient(&helix.Options{
|
2022-12-03 15:16:32 +00:00
|
|
|
ClientID: config.APIClientID,
|
|
|
|
ClientSecret: config.APIClientSecret,
|
2022-11-23 21:22:49 +00:00
|
|
|
UserAccessToken: authResp.AccessToken,
|
|
|
|
})
|
2022-11-24 12:16:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) GetLoggedUser() (helix.User, error) {
|
|
|
|
client, err := c.GetUserClient()
|
|
|
|
if err != nil {
|
|
|
|
return helix.User{}, fmt.Errorf("failed getting API client for user: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-11-23 21:22:49 +00:00
|
|
|
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")
|
|
|
|
}
|
2022-11-30 18:15:47 +00:00
|
|
|
|
2022-11-23 21:22:49 +00:00
|
|
|
return users.Data.Users[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) AuthorizeCallback(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
|
|
|
|
}
|
2022-12-03 15:16:32 +00:00
|
|
|
|
2022-11-23 21:22:49 +00:00
|
|
|
// Exchange code for access/refresh tokens
|
2022-12-03 15:16:32 +00:00
|
|
|
userTokenResponse, err := c.API.RequestUserAccessToken(code)
|
2022-11-23 21:22:49 +00:00
|
|
|
if err != nil {
|
2022-12-03 15:16:32 +00:00
|
|
|
http.Error(w, "failed auth token request: "+err.Error(), http.StatusInternalServerError)
|
2022-11-23 21:22:49 +00:00
|
|
|
return
|
|
|
|
}
|
2022-12-03 15:16:32 +00:00
|
|
|
|
|
|
|
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(),
|
|
|
|
})
|
2022-11-23 21:22:49 +00:00
|
|
|
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, `<html><body><h2>All done, you can close me now!</h2><script>window.close();</script></body></html>`)
|
|
|
|
}
|
|
|
|
|
2022-11-24 12:16:08 +00:00
|
|
|
type RefreshResponse struct {
|
|
|
|
AccessToken string `json:"access_token"`
|
|
|
|
RefreshToken string `json:"refresh_token"`
|
|
|
|
TokenType string `json:"token_type"`
|
|
|
|
Scope []string `json:"scope"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) refreshAccessToken(refreshToken string) (r RefreshResponse, err error) {
|
|
|
|
// Exchange code for access/refresh tokens
|
|
|
|
query := url.Values{
|
|
|
|
"client_id": {c.Config.APIClientID},
|
|
|
|
"client_secret": {c.Config.APIClientSecret},
|
|
|
|
"grant_type": {"refresh_token"},
|
|
|
|
"refresh_token": {refreshToken},
|
|
|
|
}
|
|
|
|
authRequest, err := http.NewRequest("POST", "https://id.twitch.tv/oauth2/token?"+query.Encode(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return RefreshResponse{}, err
|
|
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(authRequest)
|
|
|
|
if err != nil {
|
|
|
|
return RefreshResponse{}, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
var refreshResp RefreshResponse
|
|
|
|
err = jsoniter.ConfigFastest.NewDecoder(resp.Body).Decode(&refreshResp)
|
|
|
|
return refreshResp, err
|
|
|
|
}
|
|
|
|
|
2022-11-23 21:22:49 +00:00
|
|
|
func (c *Client) getRedirectURI() (string, error) {
|
|
|
|
var severConfig struct {
|
|
|
|
Bind string `json:"bind"`
|
|
|
|
}
|
|
|
|
err := c.db.GetJSON("http/config", &severConfig)
|
|
|
|
return fmt.Sprintf("http://%s/twitch/callback", severConfig.Bind), err
|
|
|
|
}
|