aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--goanilist.go172
-rw-r--r--types.go48
2 files changed, 220 insertions, 0 deletions
diff --git a/goanilist.go b/goanilist.go
new file mode 100644
index 0000000..54cdaf8
--- /dev/null
+++ b/goanilist.go
@@ -0,0 +1,172 @@
1package goanilist
2
3import (
4 "database/sql"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io/ioutil"
9 "log"
10 "net/http"
11 "net/url"
12 "time"
13)
14
15const API_URL = "https://anilist.co/api/"
16
17var (
18 ErrAnilistCoupled = errors.New("Anilist is already coupled!")
19 ErrAnilistNotCoupled = errors.New("Anilist is not coupled")
20)
21
22var httpClient = &http.Client{}
23
24type Client struct {
25 db *sql.DB
26 user int
27 id string
28 secret string
29 token string
30 refreshToken string
31 tokenExpires int64
32 IsCoupled bool
33}
34
35func NewClient(db *sql.DB, user int, id, secret string) *Client {
36 c := &Client{
37 db: db,
38 user: user,
39 id: id,
40 secret: secret,
41 }
42 err := db.QueryRow("SELECT token, refresh_token FROM login.anilist_api_tokens WHERE user_id = $1;", user).Scan(&c.token, &c.refreshToken)
43 c.IsCoupled = err == nil
44 return c
45}
46
47func (c *Client) AuthorizeURL() string {
48 return fmt.Sprintf("%sauth/authorize?grant_type=%s&client_id=%s&response_type=%s",
49 API_URL,
50 "authorization_pin",
51 url.QueryEscape(c.id),
52 "pin")
53}
54
55func (c *Client) NeedsToRenew() bool {
56 t := time.Now().UTC().Unix()
57 return t > (c.tokenExpires - 60) //60 seconds of buffer time
58}
59
60func (c *Client) CoupleByPin(pin string) error {
61 if c.IsCoupled {
62 return ErrAnilistCoupled
63 }
64 res := AccessTokenResult{}
65 err := c.post("auth/access_token", url.Values{
66 "grant_type": {"authorization_pin"},
67 "client_id": {c.id},
68 "client_secret": {c.secret},
69 "code": {pin},
70 }, &res)
71 switch res.Error {
72 case "invalid_request":
73 err = errors.New("Der eingegebene Code ist nicht korrekt. Bitte prüfe, ob du ihn richtig kopiert hast.")
74 }
75 if err != nil {
76 return err
77 }
78 _, err = c.db.Exec("INSERT INTO login.anilist_api_tokens (user_id, token, refresh_token) VALUES ($1, $2, $3);", c.user, res.AccessToken, res.RefreshToken)
79 if err != nil {
80 return err
81 }
82 c.token = res.AccessToken
83 c.refreshToken = res.RefreshToken
84 c.tokenExpires = res.Expires
85 return nil
86}
87
88func (c *Client) RenewToken() error {
89 if !c.IsCoupled {
90 return ErrAnilistNotCoupled
91 }
92 log.Println("Token expired, get new token..")
93 res := AccessTokenResult{}
94 err := c.post("auth/access_token", url.Values{
95 "grant_type": {"refresh_token"},
96 "client_id": {c.id},
97 "client_secret": {c.secret},
98 "refresh_token": {c.refreshToken},
99 }, &res)
100 if err != nil {
101 return err
102 }
103 _, err = c.db.Exec("UPDATE login.anilist_api_tokens SET token = $1 WHERE user_id = $2;", res.AccessToken, c.user)
104 if err != nil {
105 return err
106 }
107 c.token = res.AccessToken
108 c.tokenExpires = res.Expires
109 return nil
110}
111
112func (c *Client) User() (*UserResult, error) {
113 if !c.IsCoupled {
114 return nil, ErrAnilistNotCoupled
115 }
116 if c.NeedsToRenew() {
117 if err := c.RenewToken(); err != nil {
118 return nil, err
119 }
120 }
121 res := &UserResult{}
122 err := c.get("user", res)
123 if err != nil {
124 return nil, err
125 }
126 return res, nil
127}
128
129func (c *Client) post(path string, values url.Values, result interface{}) error {
130 resp, err := http.PostForm(fmt.Sprintf("%s%s", API_URL, path), values)
131 if err != nil {
132 return err
133 }
134 return c.handleResponse(resp, path, values, result)
135}
136
137func (c *Client) get(path string, result interface{}) error {
138 req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", API_URL, path), nil)
139 if err != nil {
140 return err
141 }
142 req.Header.Add("Authorization", "Bearer "+c.token)
143 resp, err := httpClient.Do(req)
144 if err != nil {
145 return err
146 }
147 return c.handleResponse(resp, path, url.Values{}, result)
148}
149
150func (c *Client) handleResponse(resp *http.Response, path string, values url.Values, result interface{}) error {
151 body, err := ioutil.ReadAll(resp.Body)
152 if err != nil {
153 return err
154 }
155 err = json.Unmarshal(body, result)
156 if err != nil {
157 log.Println(string(body))
158 return err
159 }
160 resErr := result.(APIError).Get()
161 resErr.ParseRawError()
162 if resp.StatusCode != 200 {
163 log.Printf("%s: %v -> %s\n", path, values, resp.Status)
164 if resErr.Error != "" {
165 log.Printf("%v\n", resErr)
166 return errors.New(fmt.Sprintf("%s: %s", resErr.Error, resErr.ErrorMessage))
167 } else {
168 return errors.New(resp.Status)
169 }
170 }
171 return nil
172}
diff --git a/types.go b/types.go
new file mode 100644
index 0000000..e451806
--- /dev/null
+++ b/types.go
@@ -0,0 +1,48 @@
1package goanilist
2
3import "encoding/json"
4
5type APIError interface {
6 Get() *FlexibleError
7}
8
9type FlexibleError struct {
10 RawError json.RawMessage `json:"error"`
11 Error string
12 ErrorMessage string `json:"error_message"`
13}
14
15type complexError struct {
16 Error string `json:"error"`
17 ErrorDescription string `json:"error_description"`
18}
19
20func (e *FlexibleError) ParseRawError() {
21 ce := complexError{}
22 err := json.Unmarshal(e.RawError, &ce)
23 if err == nil {
24 e.Error = ce.Error
25 e.ErrorMessage = ce.ErrorDescription
26 } else {
27 e.Error = string(e.RawError)
28 }
29}
30
31func (e *FlexibleError) Get() *FlexibleError {
32 return e
33}
34
35type AccessTokenResult struct {
36 FlexibleError
37 AccessToken string `json:"access_token"`
38 TokenType string `json:"token_type"`
39 Expires int64 `json:"expires"`
40 ExpiresIn int `json:"expires_in"`
41 RefreshToken string `json:"refresh_token"`
42}
43
44type UserResult struct {
45 FlexibleError
46 ID int `json:"id"`
47 DisplayName string `json:"display_name"`
48}