package main import ( "context" "crypto/sha256" "encoding/base64" "encoding/json" "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", }) 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, 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, err := myAnimeListRing.Get("MyAnimeListTokenType") expiresIn, err := myAnimeListRing.Get("MyAnimeListExpiresIn") accessToken, err := myAnimeListRing.Get("MyAnimeListAccessToken") refreshToken, err := myAnimeListRing.Get("MyAnimeListRefreshToken") if err != nil || len(accessToken.Data) == 0 { return false } else { myAnimeListJwt.TokenType = string(tokenType.Data) myAnimeListJwt.ExpiresIn, err = strconv.Atoi(string(expiresIn.Data)) if err != 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() == false { tokenType, err := myAnimeListRing.Get("MyAnimeListTokenType") expiresIn, err := myAnimeListRing.Get("MyAnimeListExpiresIn") accessToken, err := myAnimeListRing.Get("MyAnimeListAccessToken") refreshToken, err := myAnimeListRing.Get("MyAnimeListRefreshToken") if err != 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 { myAnimeListJwt.TokenType = string(tokenType.Data) myAnimeListJwt.ExpiresIn, err = strconv.Atoi(string(expiresIn.Data)) if err != 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 && err != http.ErrServerClosed { log.Fatalf("listen: %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) var post MyAnimeListJWT err = json.Unmarshal(returnedBody, &post) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) } return post } func refreshMyAnimeListAuthorizationToken() { 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) } 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) err = json.Unmarshal(returnedBody, &myAnimeListJwt) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) } _ = 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 { fmt.Println(err) } return } func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser { a.MyAnimeListLogin() 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) { err := myAnimeListRing.Remove("MyAnimeListTokenType") err = myAnimeListRing.Remove("MyAnimeListExpiresIn") err = myAnimeListRing.Remove("MyAnimeListAccessToken") err = myAnimeListRing.Remove("MyAnimeListRefreshToken") if err != nil { fmt.Println("MAL Logout Failed", err) } myAnimeListJwt = MyAnimeListJWT{} } return "MAL Logged Out Successfully" }