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  }