github.com/wtfutil/wtf@v0.43.0/modules/pocket/client.go (about) 1 package pocket 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 ) 10 11 // Client pocket client Documention at https://getpocket.com/developer/docs/overview 12 type Client struct { 13 consumerKey string 14 accessToken *string 15 baseURL string 16 redirectURL string 17 } 18 19 // NewClient returns a new PocketClient 20 func NewClient(consumerKey, redirectURL string) *Client { 21 return &Client{ 22 consumerKey: consumerKey, 23 redirectURL: redirectURL, 24 baseURL: "https://getpocket.com/v3", 25 } 26 27 } 28 29 // Item represents link in pocket api 30 type Item struct { 31 ItemID string `json:"item_id"` 32 ResolvedID string `json:"resolved_id"` 33 GivenURL string `json:"given_url"` 34 GivenTitle string `json:"given_title"` 35 Favorite string `json:"favorite"` 36 Status string `json:"status"` 37 TimeAdded string `json:"time_added"` 38 TimeUpdated string `json:"time_updated"` 39 TimeRead string `json:"time_read"` 40 TimeFavorited string `json:"time_favorited"` 41 SortID int `json:"sort_id"` 42 ResolvedTitle string `json:"resolved_title"` 43 ResolvedURL string `json:"resolved_url"` 44 Excerpt string `json:"excerpt"` 45 IsArticle string `json:"is_article"` 46 IsIndex string `json:"is_index"` 47 HasVideo string `json:"has_video"` 48 HasImage string `json:"has_image"` 49 WordCount string `json:"word_count"` 50 Lang string `json:"lang"` 51 TimeToRead int `json:"time_to_read"` 52 TopImageURL string `json:"top_image_url"` 53 ListenDurationEstimate int `json:"listen_duration_estimate"` 54 } 55 56 // ItemLists represent list of links 57 type ItemLists struct { 58 Status int `json:"status"` 59 Complete int `json:"complete"` 60 List map[string]Item `json:"list"` 61 Since int `json:"since"` 62 } 63 64 type request struct { 65 requestBody interface{} 66 method string 67 headers map[string]string 68 url string 69 } 70 71 func (*Client) request(req request, result interface{}) error { 72 var reqBody io.Reader 73 if req.requestBody != nil { 74 jsonValues, err := json.Marshal(req.requestBody) 75 if err != nil { 76 return err 77 } 78 reqBody = bytes.NewBuffer(jsonValues) 79 } 80 81 request, err := http.NewRequest(req.method, req.url, reqBody) 82 if err != nil { 83 return err 84 } 85 86 for key, value := range req.headers { 87 request.Header.Add(key, value) 88 } 89 request.Header.Set("User-Agent", "wtfutil (https://github.com/wtfutil/wtf)") 90 91 resp, err := http.DefaultClient.Do(request) 92 93 if err != nil { 94 return err 95 } 96 defer func() { _ = resp.Body.Close() }() 97 98 responseBody, err := io.ReadAll(resp.Body) 99 100 if err != nil { 101 return err 102 } 103 if resp.StatusCode >= 400 { 104 return fmt.Errorf(`server responded with [%d]:%s,url:%s`, resp.StatusCode, responseBody, req.url) 105 } 106 107 if err := json.Unmarshal(responseBody, &result); err != nil { 108 return fmt.Errorf("could not unmarshal url [%s] \n\t\tresponse [%s] error:%w", 109 req.url, responseBody, err) 110 } 111 112 return nil 113 114 } 115 116 type obtainRequestTokenRequest struct { 117 ConsumerKey string `json:"consumer_key"` 118 RedirectURI string `json:"redirect_uri"` 119 } 120 121 // ObtainRequestToken get request token to be used in the auth workflow 122 func (client *Client) ObtainRequestToken() (code string, err error) { 123 url := fmt.Sprintf("%s/oauth/request", client.baseURL) 124 requestData := obtainRequestTokenRequest{ConsumerKey: client.consumerKey, RedirectURI: client.redirectURL} 125 126 var responseData map[string]string 127 req := request{ 128 headers: map[string]string{ 129 "X-Accept": "application/json", 130 "Content-Type": "application/json", 131 }, 132 method: "POST", 133 requestBody: requestData, 134 url: url, 135 } 136 err = client.request(req, &responseData) 137 138 if err != nil { 139 return code, err 140 } 141 142 return responseData["code"], nil 143 144 } 145 146 // CreateAuthLink create authorization link to redirect the user to 147 func (client *Client) CreateAuthLink(requestToken string) string { 148 return fmt.Sprintf("https://getpocket.com/auth/authorize?request_token=%s&redirect_uri=%s", requestToken, client.redirectURL) 149 } 150 151 type accessTokenRequest struct { 152 ConsumerKey string `json:"consumer_key"` 153 RequestCode string `json:"code"` 154 } 155 156 // accessTokenResponse represents 157 type accessTokenResponse struct { 158 AccessToken string `json:"access_token"` 159 } 160 161 // GetAccessToken exchange request token for accesstoken 162 func (client *Client) GetAccessToken(requestToken string) (accessToken string, err error) { 163 url := fmt.Sprintf("%s/oauth/authorize", client.baseURL) 164 requestData := accessTokenRequest{ 165 ConsumerKey: client.consumerKey, 166 RequestCode: requestToken, 167 } 168 req := request{ 169 method: "POST", 170 url: url, 171 requestBody: requestData, 172 } 173 req.headers = map[string]string{ 174 "X-Accept": "application/json", 175 "Content-Type": "application/json", 176 } 177 178 var response accessTokenResponse 179 err = client.request(req, &response) 180 if err != nil { 181 return "", err 182 } 183 return response.AccessToken, nil 184 185 } 186 187 /* 188 LinkState represents link states to be retrieved 189 According to the api https://getpocket.com/developer/docs/v3/retrieve 190 there are 3 states: 191 192 1-archive 193 2-unread 194 3-all 195 196 however archive does not really well work and returns links that are in the 197 unread list 198 buy inspecting getpocket I found out that there is an undocumanted read state 199 */ 200 type LinkState string 201 202 const ( 203 // Read links that has been read (undocumanted) 204 Read LinkState = "read" 205 // Unread links has not been read 206 Unread LinkState = "unread" 207 ) 208 209 // GetLinks retrieve links of a given states https://getpocket.com/developer/docs/v3/retrieve 210 func (client *Client) GetLinks(state LinkState) (response ItemLists, err error) { 211 url := fmt.Sprintf("%s/get?sort=newest&state=%s&consumer_key=%s&access_token=%s", client.baseURL, state, client.consumerKey, *client.accessToken) 212 req := request{ 213 method: "GET", 214 url: url, 215 } 216 req.headers = map[string]string{ 217 "X-Accept": "application/json", 218 "Content-Type": "application/json", 219 } 220 221 err = client.request(req, &response) 222 return response, err 223 } 224 225 // Action represents a mutation to link 226 type Action string 227 228 const ( 229 // Archive to put the link in the archived list (read list) 230 Archive Action = "archive" 231 // ReAdd to put the link back in the to reed list 232 ReAdd Action = "readd" 233 ) 234 235 type actionParams struct { 236 Action Action `json:"action"` 237 ItemID string `json:"item_id"` 238 } 239 240 // ModifyLink change the state of the link 241 func (client *Client) ModifyLink(action Action, itemID string) (ok bool, err error) { 242 243 actions := []actionParams{ 244 { 245 Action: action, 246 ItemID: itemID, 247 }, 248 } 249 250 urlActionParm, err := json.Marshal(actions) 251 if err != nil { 252 return false, err 253 } 254 url := fmt.Sprintf("%s/send?consumer_key=%s&access_token=%s&actions=%s", client.baseURL, client.consumerKey, *client.accessToken, urlActionParm) 255 256 req := request{ 257 method: "GET", 258 url: url, 259 } 260 261 err = client.request(req, nil) 262 263 if err != nil { 264 return false, err 265 } 266 267 return true, nil 268 269 }