From 237958cce5948180ef8a2934448c59e7e5e371b1 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 30 Jul 2024 12:59:06 -0400 Subject: [PATCH] added simkl login to backend --- SimklFunctions.go | 35 +++++++ SimklTypes.go | 16 +++ SimklUserFunctions.go | 162 ++++++++++++++++++++++++++++++ frontend/wailsjs/go/main/App.d.ts | 4 + frontend/wailsjs/go/main/App.js | 8 ++ frontend/wailsjs/go/models.ts | 31 ++++++ 6 files changed, 256 insertions(+) create mode 100644 SimklTypes.go diff --git a/SimklFunctions.go b/SimklFunctions.go index 06ab7d0..46c498d 100644 --- a/SimklFunctions.go +++ b/SimklFunctions.go @@ -1 +1,36 @@ package main + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" +) + +func SimklQuery(body interface{}, login bool) (json.RawMessage, string) { + reader, _ := json.Marshal(body) + response, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(reader)) + if err != nil { + log.Printf("Failed at response, %s\n", err) + } + if login && (SimklJWT{}) != simklJwt { + response.Header.Add("Authorization", "Bearer "+simklJwt.AccessToken) + } else if login { + return nil, "Please login to anilist to make this request" + } + response.Header.Add("Content-Type", "application/json") + response.Header.Add("Accept", "application/json") + + 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) + + return returnedBody, "" +} diff --git a/SimklTypes.go b/SimklTypes.go new file mode 100644 index 0000000..3805f8e --- /dev/null +++ b/SimklTypes.go @@ -0,0 +1,16 @@ +package main + +type SimklJWT struct { + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + Scope string `json:"scope"` +} + +type SimklUser struct { + Data struct { + Viewer struct { + ID int `json:"id"` + Name string `json:"name"` + } `json:"Viewer"` + } `json:"data"` +} diff --git a/SimklUserFunctions.go b/SimklUserFunctions.go index 06ab7d0..39d432f 100644 --- a/SimklUserFunctions.go +++ b/SimklUserFunctions.go @@ -1 +1,163 @@ package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "sync" + + "github.com/99designs/keyring" + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +var simklJwt SimklJWT + +var simklRing, _ = keyring.Open(keyring.Config{ + ServiceName: "AniTrack", +}) + +var simklCtxShutdown, simklCancel = context.WithCancel(context.Background()) + +func (a *App) SimklLogin() { + if (SimklJWT{}) == simklJwt { + tokenType, err := simklRing.Get("SimklTokenType") + accessToken, err := simklRing.Get("SimklAccessToken") + scope, err := simklRing.Get("SimklScope") + if err != nil { + getSimklCodeUrl := "https://simkl.com/oauth/authorize?response_type=code&client_id=" + os.Getenv("SIMKL_CLIENT_ID") + "&redirect_uri=" + os.Getenv("SIMKL_CALLBACK_URI") + runtime.BrowserOpenURL(a.ctx, getSimklCodeUrl) + + serverDone := &sync.WaitGroup{} + serverDone.Add(1) + handleSimklCallback(serverDone) + serverDone.Wait() + } else { + simklJwt.TokenType = string(tokenType.Data) + simklJwt.AccessToken = string(accessToken.Data) + simklJwt.Scope = string(scope.Data) + } + } +} + +func handleSimklCallback(wg *sync.WaitGroup) { + srv := &http.Server{Addr: ":6734"} + http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + select { + case <-simklCtxShutdown.Done(): + fmt.Println("Shutting down...") + return + default: + } + content := r.FormValue("code") + + if content != "" { + simklJwt = getSimklAuthorizationToken(content) + _ = simklRing.Set(keyring.Item{ + Key: "SimklTokenType", + Data: []byte(simklJwt.TokenType), + }) + _ = simklRing.Set(keyring.Item{ + Key: "SimklAccessToken", + Data: []byte(simklJwt.AccessToken), + }) + _ = simklRing.Set(keyring.Item{ + Key: "SimklScope", + Data: []byte(simklJwt.Scope), + }) + fmt.Println("Shutting down...") + simklCancel() + 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 getSimklAuthorizationToken(content string) SimklJWT { + data := 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"` + }{ + GrantType: "authorization_code", + ClientID: os.Getenv("SIMKL_CLIENT_ID"), + ClientSecret: os.Getenv("SIMKL_CLIENT_SECRET"), + RedirectURI: os.Getenv("SIMKL_CALLBACK_URI"), + Code: content, + } + jsonData, err := json.Marshal(data) + if err != nil { + log.Fatal(err) + } + + response, err := http.NewRequest("POST", "https://api.simkl.com/oauth/token", bytes.NewBuffer(jsonData)) + if err != nil { + log.Printf("Failed at response, %s\n", err) + } + response.Header.Add("Content-Type", "application/json") + + 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 SimklJWT + err = json.Unmarshal(returnedBody, &post) + if err != nil { + log.Printf("Failed at unmarshal, %s\n", err) + } + + return post +} + +func (a *App) GetSimklLoggedInUserId() SimklUser { + a.SimklLogin() + body := struct { + Query string `json:"query"` + }{ + Query: ` + query { + Viewer { + id + name + } + } + `, + } + + user, _ := SimklQuery(body, true) + + var post SimklUser + err := json.Unmarshal(user, &post) + if err != nil { + log.Printf("Failed at unmarshal, %s\n", err) + } + + return post +} diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 4e4d63c..8c3abfa 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -14,4 +14,8 @@ export function GetAniListLoggedInUserId():Promise; export function GetAniListUserWatchingList(arg1:number,arg2:number,arg3:string):Promise; +export function GetSimklLoggedInUserId():Promise; + export function Greet(arg1:string):Promise; + +export function SimklLogin():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index df26c18..7fd68be 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -26,6 +26,14 @@ export function GetAniListUserWatchingList(arg1, arg2, arg3) { return window['go']['main']['App']['GetAniListUserWatchingList'](arg1, arg2, arg3); } +export function GetSimklLoggedInUserId() { + return window['go']['main']['App']['GetSimklLoggedInUserId'](); +} + export function Greet(arg1) { return window['go']['main']['App']['Greet'](arg1); } + +export function SimklLogin() { + return window['go']['main']['App']['SimklLogin'](); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 8f280b9..5688c83 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -147,6 +147,37 @@ export namespace main { return a; } } + export class SimklUser { + // Go type: struct { Viewer struct { ID int "json:\"id\""; Name string "json:\"name\"" } "json:\"Viewer\"" } + data: any; + + static createFrom(source: any = {}) { + return new SimklUser(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.data = this.convertValues(source["data"], Object); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } }