In CheckIfMyAnimeListLoggedIn and MyAnimeListLogin the keyring lookups assigned MyAnimeListAccessToken to refreshToken and MyAnimeListRefreshToken to accessToken. Downstream code checks len(accessToken.Data) for login state and assigns accessToken.Data -> myAnimeListJwt.AccessToken, so the swap gated login on the refresh token and stored each token in the wrong field. Swap the variable names to match their keyring keys so login detection, JWT restoration, and the empty-token fallback in MyAnimeListLogin use the correct values.
382 lines
11 KiB
Go
382 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/99designs/keyring"
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
var myAnimeListJwt MyAnimeListJWT
|
|
|
|
var myAnimeListRing, _ = keyring.Open(keyring.Config{
|
|
ServiceName: "AniTrack",
|
|
KeychainName: "AniTrack",
|
|
KeychainSynchronizable: false,
|
|
KeychainTrustApplication: true,
|
|
KeychainAccessibleWhenUnlocked: true,
|
|
})
|
|
|
|
var myAnimeListCtxShutdown, myAnimeListCancel = context.WithCancel(context.Background())
|
|
|
|
type CodeVerifier struct {
|
|
Value string
|
|
}
|
|
|
|
const (
|
|
length = 32
|
|
)
|
|
|
|
func base64URLEncode(str []byte) string {
|
|
encoded := base64.StdEncoding.EncodeToString(str)
|
|
encoded = strings.Replace(encoded, "+", "-", -1)
|
|
encoded = strings.Replace(encoded, "/", "_", -1)
|
|
encoded = strings.Replace(encoded, "=", "", -1)
|
|
return encoded
|
|
}
|
|
|
|
func verifier() (*CodeVerifier, error) {
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
b := make([]byte, length)
|
|
for i := 0; i < length; i++ {
|
|
b[i] = byte(r.Intn(255))
|
|
}
|
|
return CreateCodeVerifierFromBytes(b)
|
|
}
|
|
|
|
func CreateCodeVerifierFromBytes(b []byte) (*CodeVerifier, error) {
|
|
return &CodeVerifier{
|
|
Value: base64URLEncode(b),
|
|
}, nil
|
|
}
|
|
|
|
func (v *CodeVerifier) CodeChallengeS256() string {
|
|
h := sha256.New()
|
|
h.Write([]byte(v.Value))
|
|
return base64URLEncode(h.Sum(nil))
|
|
}
|
|
|
|
func (a *App) CheckIfMyAnimeListLoggedIn() bool {
|
|
if (MyAnimeListJWT{} == myAnimeListJwt) {
|
|
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType")
|
|
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn")
|
|
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken")
|
|
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken")
|
|
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
|
|
return false
|
|
} else {
|
|
var expiresInConvertErr error
|
|
myAnimeListJwt.TokenType = string(tokenType.Data)
|
|
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
|
|
if expiresInConvertErr != nil {
|
|
fmt.Println("unable to convert string to int")
|
|
}
|
|
myAnimeListJwt.AccessToken = string(accessToken.Data)
|
|
myAnimeListJwt.RefreshToken = string(refreshToken.Data)
|
|
return true
|
|
}
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (a *App) MyAnimeListLogin() {
|
|
if !a.CheckIfMyAnimeListLoggedIn() {
|
|
fmt.Println("check logged in function failed")
|
|
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType")
|
|
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn")
|
|
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken")
|
|
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken")
|
|
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
|
|
verifier, _ := verifier()
|
|
getMyAnimeListCodeUrl := "https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=" + Environment.MAL_CLIENT_ID + "&redirect_uri=" + Environment.MAL_CALLBACK_URI + "&code_challenge=" + verifier.Value + "&code_challenge_method=plain"
|
|
runtime.BrowserOpenURL(*wailsContext, getMyAnimeListCodeUrl)
|
|
serverDone := &sync.WaitGroup{}
|
|
serverDone.Add(1)
|
|
a.handleMyAnimeListCallback(serverDone, verifier)
|
|
serverDone.Wait()
|
|
} else {
|
|
var expiresInConvertErr error
|
|
myAnimeListJwt.TokenType = string(tokenType.Data)
|
|
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
|
|
if expiresInConvertErr != nil {
|
|
fmt.Println("unable to convert string to int in Login function")
|
|
}
|
|
myAnimeListJwt.AccessToken = string(accessToken.Data)
|
|
myAnimeListJwt.RefreshToken = string(refreshToken.Data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) handleMyAnimeListCallback(wg *sync.WaitGroup, verifier *CodeVerifier) {
|
|
mux := http.NewServeMux()
|
|
srv := &http.Server{Addr: ":6734", Handler: mux}
|
|
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case <-myAnimeListCtxShutdown.Done():
|
|
fmt.Println("Shutting down...")
|
|
return
|
|
default:
|
|
}
|
|
content := r.FormValue("code")
|
|
|
|
if content != "" {
|
|
myAnimeListJwt = getMyAnimeListAuthorizationToken(content, verifier)
|
|
_ = myAnimeListRing.Set(keyring.Item{
|
|
Key: "MyAnimeListTokenType",
|
|
Data: []byte(myAnimeListJwt.TokenType),
|
|
})
|
|
_ = myAnimeListRing.Set(keyring.Item{
|
|
Key: "MyAnimeListExpiresIn",
|
|
Data: []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)),
|
|
})
|
|
_ = myAnimeListRing.Set(keyring.Item{
|
|
Key: "MyAnimeListAccessToken",
|
|
Data: []byte(myAnimeListJwt.AccessToken),
|
|
})
|
|
_ = myAnimeListRing.Set(keyring.Item{
|
|
Key: "MyAnimeListRefreshToken",
|
|
Data: []byte(myAnimeListJwt.RefreshToken),
|
|
})
|
|
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{
|
|
Title: "MyAnimeList Authorization",
|
|
Message: "It is now safe to close your browser tab",
|
|
})
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
fmt.Println("Shutting down...")
|
|
myAnimeListCancel()
|
|
err = srv.Shutdown(context.Background())
|
|
if err != nil {
|
|
log.Println("server.Shutdown:", err)
|
|
}
|
|
} else {
|
|
_, err := fmt.Fprintf(w, "Getting code failed.")
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
log.Printf("Server error: %s\n", err)
|
|
}
|
|
fmt.Println("Shutting down...")
|
|
}()
|
|
}
|
|
|
|
func getMyAnimeListAuthorizationToken(content string, verifier *CodeVerifier) MyAnimeListJWT {
|
|
dataForURLs := struct {
|
|
GrantType string `json:"grant_type"`
|
|
ClientID string `json:"client_id"`
|
|
ClientSecret string `json:"client_secret"`
|
|
RedirectURI string `json:"redirect_uri"`
|
|
Code string `json:"code"`
|
|
CodeVerifier *CodeVerifier `json:"code_verifier"`
|
|
}{
|
|
GrantType: "authorization_code",
|
|
ClientID: Environment.MAL_CLIENT_ID,
|
|
ClientSecret: Environment.MAL_CLIENT_SECRET,
|
|
RedirectURI: Environment.MAL_CALLBACK_URI,
|
|
Code: content,
|
|
CodeVerifier: verifier,
|
|
}
|
|
|
|
data := url.Values{}
|
|
data.Set("grant_type", dataForURLs.GrantType)
|
|
data.Set("client_id", dataForURLs.ClientID)
|
|
data.Set("client_secret", dataForURLs.ClientSecret)
|
|
data.Set("redirect_uri", dataForURLs.RedirectURI)
|
|
data.Set("code", dataForURLs.Code)
|
|
data.Set("code_verifier", dataForURLs.CodeVerifier.Value)
|
|
|
|
response, err := http.NewRequest("POST", "https://myanimelist.net/v1/oauth2/token", strings.NewReader(data.Encode()))
|
|
if err != nil {
|
|
log.Printf("Failed at response, %s\n", err)
|
|
}
|
|
response.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
client := &http.Client{}
|
|
res, resErr := client.Do(response)
|
|
if resErr != nil {
|
|
log.Printf("Failed at res, %s\n", err)
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
returnedBody, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("Could not read returned body, %s\n", err)
|
|
}
|
|
|
|
var post MyAnimeListJWT
|
|
err = json.Unmarshal(returnedBody, &post)
|
|
if err != nil {
|
|
log.Printf("Failed at unmarshal, %s\n", err)
|
|
}
|
|
|
|
return post
|
|
}
|
|
|
|
func refreshMyAnimeListAuthorizationToken() bool {
|
|
dataForURLs := struct {
|
|
GrantType string `json:"grant_type"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
ClientID string `json:"client_id"`
|
|
ClientSecret string `json:"client_secret"`
|
|
RedirectURI string `json:"redirect_uri"`
|
|
}{
|
|
GrantType: "refresh_token",
|
|
RefreshToken: myAnimeListJwt.RefreshToken,
|
|
ClientID: Environment.MAL_CLIENT_ID,
|
|
ClientSecret: Environment.MAL_CLIENT_SECRET,
|
|
RedirectURI: Environment.MAL_CALLBACK_URI,
|
|
}
|
|
|
|
data := url.Values{}
|
|
data.Set("grant_type", dataForURLs.GrantType)
|
|
data.Set("refresh_token", dataForURLs.RefreshToken)
|
|
data.Set("client_id", dataForURLs.ClientID)
|
|
data.Set("client_secret", dataForURLs.ClientSecret)
|
|
data.Set("redirect_uri", dataForURLs.RedirectURI)
|
|
|
|
response, err := http.NewRequest("POST", "https://myanimelist.net/v1/oauth2/token", strings.NewReader(data.Encode()))
|
|
if err != nil {
|
|
log.Printf("Failed at response, %s\n", err)
|
|
return false
|
|
}
|
|
response.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
client := &http.Client{}
|
|
res, resErr := client.Do(response)
|
|
if resErr != nil {
|
|
log.Printf("Failed at res, %s\n", resErr)
|
|
return false
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
returnedBody, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("Could not read returned body, %s\n", err)
|
|
return false
|
|
}
|
|
|
|
var refreshed MyAnimeListJWT
|
|
err = json.Unmarshal(returnedBody, &refreshed)
|
|
if err != nil {
|
|
log.Printf("Failed at unmarshal, %s\n", err)
|
|
return false
|
|
}
|
|
|
|
if refreshed.AccessToken == "" {
|
|
return false
|
|
}
|
|
|
|
myAnimeListJwt = refreshed
|
|
|
|
_ = myAnimeListRing.Set(keyring.Item{
|
|
Key: "MyAnimeListTokenType",
|
|
Data: []byte(myAnimeListJwt.TokenType),
|
|
})
|
|
_ = myAnimeListRing.Set(keyring.Item{
|
|
Key: "MyAnimeListExpiresIn",
|
|
Data: []byte(strconv.Itoa(myAnimeListJwt.ExpiresIn)),
|
|
})
|
|
_ = myAnimeListRing.Set(keyring.Item{
|
|
Key: "MyAnimeListAccessToken",
|
|
Data: []byte(myAnimeListJwt.AccessToken),
|
|
})
|
|
_ = myAnimeListRing.Set(keyring.Item{
|
|
Key: "MyAnimeListRefreshToken",
|
|
Data: []byte(myAnimeListJwt.RefreshToken),
|
|
})
|
|
return true
|
|
}
|
|
|
|
func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
|
|
a.MyAnimeListLogin()
|
|
user := createUser()
|
|
if user.Name == "" && !a.refreshMyAnimeListAndGetUser(&user) {
|
|
a.LogoutMyAnimeList()
|
|
a.MyAnimeListLogin()
|
|
user = createUser()
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
func (a *App) refreshMyAnimeListAndGetUser(user *MyAnimeListUser) bool {
|
|
if !refreshMyAnimeListAuthorizationToken() {
|
|
return false
|
|
}
|
|
freshUser := createUser()
|
|
if freshUser.Name == "" {
|
|
return false
|
|
}
|
|
*user = freshUser
|
|
return true
|
|
}
|
|
|
|
func createUser() MyAnimeListUser {
|
|
client := &http.Client{}
|
|
|
|
req, _ := http.NewRequest("GET", "https://api.myanimelist.net/v2/users/@me?fields=anime_statistics", nil)
|
|
|
|
req.Header.Add("Content-Type", "application/json")
|
|
req.Header.Add("Authorization", "Bearer "+myAnimeListJwt.AccessToken)
|
|
req.Header.Add("myAnimeList-api-key", Environment.MAL_CLIENT_ID)
|
|
|
|
response, err := client.Do(req)
|
|
if err != nil {
|
|
log.Printf("Failed at request, %s\n", err)
|
|
return MyAnimeListUser{}
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
var user MyAnimeListUser
|
|
|
|
respBody, _ := io.ReadAll(response.Body)
|
|
|
|
err = json.Unmarshal(respBody, &user)
|
|
if err != nil {
|
|
log.Printf("Failed at unmarshal, %s\n", err)
|
|
}
|
|
|
|
return user
|
|
|
|
}
|
|
|
|
func (a *App) LogoutMyAnimeList() string {
|
|
if (MyAnimeListJWT{} != myAnimeListJwt) {
|
|
typeErr := myAnimeListRing.Remove("MyAnimeListTokenType")
|
|
expiresInErr := myAnimeListRing.Remove("MyAnimeListExpiresIn")
|
|
accessTokenErr := myAnimeListRing.Remove("MyAnimeListAccessToken")
|
|
refreshTokenErr := myAnimeListRing.Remove("MyAnimeListRefreshToken")
|
|
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
|
|
fmt.Println("MAL Logout Failed")
|
|
}
|
|
myAnimeListJwt = MyAnimeListJWT{}
|
|
}
|
|
|
|
return "MAL Logged Out Successfully"
|
|
}
|