package goanilist import ( "database/sql" "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "net/url" "sort" "time" ) const API_URL = "https://anilist.co/api/" var ( ErrAnilistCoupled = errors.New("Anilist is already coupled!") ErrAnilistNotCoupled = errors.New("Anilist is not coupled") ) var httpClient = &http.Client{} type Client struct { db *sql.DB user int id string secret string token string refreshToken string tokenExpires int64 IsCoupled bool } func NewClient(db *sql.DB, user int, id, secret string) *Client { c := &Client{ db: db, user: user, id: id, secret: secret, } err := db.QueryRow("SELECT token, refresh_token, expires_at FROM login.anilist_api_tokens WHERE user_id = $1;", user).Scan(&c.token, &c.refreshToken, &c.tokenExpires) c.IsCoupled = err == nil return c } func (c *Client) AuthorizeURL() string { return fmt.Sprintf("%sauth/authorize?grant_type=%s&client_id=%s&response_type=%s", API_URL, "authorization_pin", url.QueryEscape(c.id), "pin") } func (c *Client) NeedsToRenew() bool { t := time.Now().UTC().Unix() return t > (c.tokenExpires - 60) //60 seconds of buffer time } func (c *Client) CoupleByPin(pin string) error { if c.IsCoupled { return ErrAnilistCoupled } res := AccessTokenResult{} err := c.post("auth/access_token", url.Values{ "grant_type": {"authorization_pin"}, "client_id": {c.id}, "client_secret": {c.secret}, "code": {pin}, }, &res) switch res.Error { case "invalid_request": err = errors.New("Der eingegebene Code ist nicht korrekt. Bitte prüfe, ob du ihn richtig kopiert hast.") } if err != nil { return err } _, err = c.db.Exec("INSERT INTO login.anilist_api_tokens (user_id, token, refresh_token, expires_at) VALUES ($1, $2, $3, $4);", c.user, res.AccessToken, res.RefreshToken, res.Expires) if err != nil { return err } c.token = res.AccessToken c.refreshToken = res.RefreshToken c.tokenExpires = res.Expires return nil } func (c *Client) RenewToken() error { if !c.IsCoupled { return ErrAnilistNotCoupled } if !c.NeedsToRenew() { return nil } log.Println("Token expired, get new token..") res := AccessTokenResult{} err := c.post("auth/access_token", url.Values{ "grant_type": {"refresh_token"}, "client_id": {c.id}, "client_secret": {c.secret}, "refresh_token": {c.refreshToken}, }, &res) if err != nil { return err } _, err = c.db.Exec("UPDATE login.anilist_api_tokens SET token = $1, expires_at = $2 WHERE user_id = $3;", res.AccessToken, res.Expires, c.user) if err != nil { return err } c.token = res.AccessToken c.tokenExpires = res.Expires return nil } func (c *Client) User() (*UserResult, error) { if !c.IsCoupled { return nil, ErrAnilistNotCoupled } if err := c.RenewToken(); err != nil { return nil, err } res := &UserResult{} err := c.get("user", res) if err != nil { return nil, err } return res, nil } func (c *Client) Favourites(user int) (*FavouritesResult, error) { if !c.IsCoupled { return nil, ErrAnilistNotCoupled } if err := c.RenewToken(); err != nil { return nil, err } res := &FavouritesResult{} err := c.get(fmt.Sprintf("user/%d/favourites", user), res) if err != nil { return nil, err } if res.Characters != nil { sort.Stable(res.Characters) } return res, nil } func (c *Client) CharacterPage(id int) (*CharacterPageResult, error) { if !c.IsCoupled { return nil, ErrAnilistNotCoupled } if err := c.RenewToken(); err != nil { return nil, err } res := &CharacterPageResult{} err := c.get(fmt.Sprintf("character/%d/page", id), res) if err != nil { return nil, err } return res, nil } func (c *Client) post(path string, values url.Values, result interface{}) error { resp, err := http.PostForm(fmt.Sprintf("%s%s", API_URL, path), values) if err != nil { return err } return c.handleResponse(resp, path, values, result) } func (c *Client) get(path string, result interface{}) error { req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", API_URL, path), nil) if err != nil { return err } req.Header.Add("Authorization", "Bearer "+c.token) resp, err := httpClient.Do(req) if err != nil { return err } return c.handleResponse(resp, path, url.Values{}, result) } func (c *Client) handleResponse(resp *http.Response, path string, values url.Values, result interface{}) error { body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } err = json.Unmarshal(body, result) //log.Println(string(body)) //Praktisch zum testen von neuen API-Funktionen if err != nil { log.Println(string(body)) return err } resErr := result.(APIError).Get() resErr.ParseRawError() if resp.StatusCode != 200 { log.Printf("%s: %v -> %s\n", path, values, resp.Status) if resErr.Error != "" { log.Printf("%v\n", resErr) return errors.New(fmt.Sprintf("%s: %s", resErr.Error, resErr.ErrorMessage)) } else { return errors.New(resp.Status) } } return nil }