Skip to content

Commit

Permalink
fix: retrieve user info using trakt api instead of scraping
Browse files Browse the repository at this point in the history
resolves cecobask#60
  • Loading branch information
cecobask committed Aug 20, 2024
1 parent d5b82a5 commit 1584180
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 34 deletions.
9 changes: 9 additions & 0 deletions internal/entities/trakt.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,12 @@ type TraktList struct {
ListItems TraktItems
IsWatchlist bool
}

type TraktUserInfo struct {
Username string `json:"username"`
Private bool `json:"private"`
Name string `json:"name"`
Vip bool `json:"vip"`
VipEp bool `json:"vip_ep"`
IDMeta TraktIDMeta `json:"ids"`
}
13 changes: 13 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type TraktClientInterface interface {
HistoryGet(itemType, itemID string) (entities.TraktItems, error)
HistoryAdd(items entities.TraktItems) error
HistoryRemove(items entities.TraktItems) error
UserInfoGet() (*entities.TraktUserInfo, error)
}

type requestFields struct {
Expand Down Expand Up @@ -77,6 +78,18 @@ func (r reusableReader) Read(p []byte) (int, error) {
return n, err
}

func selectorExists(body io.ReadCloser, selector string) error {
defer body.Close()
doc, err := goquery.NewDocumentFromReader(body)
if err != nil {
return fmt.Errorf("failure creating goquery document from response: %w", err)
}
if doc.Find(selector).Length() == 0 {
return fmt.Errorf("failure finding selector %s", selector)
}
return nil
}

func selectorAttributeScrape(body io.ReadCloser, selector, attribute string) (*string, error) {
defer body.Close()
doc, err := goquery.NewDocumentFromReader(body)
Expand Down
31 changes: 21 additions & 10 deletions pkg/client/trakt.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const (
traktPathHistoryRemove = "/sync/history/remove"
traktPathRatings = "/sync/ratings"
traktPathRatingsRemove = "/sync/ratings/remove"
traktPathUserInfo = "/users/me"
traktPathUserList = "/users/%s/lists/%s"
traktPathUserListItems = "/users/%s/lists/%s/items"
traktPathUserListItemsRemove = "/users/%s/lists/%s/items/remove"
Expand Down Expand Up @@ -115,6 +116,11 @@ func (tc *TraktClient) hydrate() error {
return fmt.Errorf("failure exchanging trakt device code for access token: %w", err)
}
tc.config.accessToken = authTokens.AccessToken
userInfo, err := tc.UserInfoGet()
if err != nil {
return fmt.Errorf("failure getting trakt user info: %w", err)
}
tc.config.username = userInfo.Username
return nil
}

Expand Down Expand Up @@ -208,16 +214,7 @@ func (tc *TraktClient) ActivateAuthorize(authenticityToken string) error {
if err != nil {
return err
}
value, err := selectorAttributeScrape(response.Body, "a.visible-xs", "href")
if err != nil {
return err
}
hrefPieces := strings.Split(*value, "/")
if len(hrefPieces) != 3 {
return fmt.Errorf("failure scraping trakt username")
}
tc.config.username = hrefPieces[2]
return nil
return selectorExists(response.Body, "a[href='/logout']")
}

func (tc *TraktClient) GetAccessToken(deviceCode string) (*entities.TraktAuthTokensResponse, error) {
Expand Down Expand Up @@ -330,6 +327,20 @@ func (tc *TraktClient) doRequest(requestFields requestFields) (*http.Response, e
return nil, fmt.Errorf("reached max retry attempts for %s %s", request.Method, request.URL)
}

func (tc *TraktClient) UserInfoGet() (*entities.TraktUserInfo, error) {
response, err := tc.doRequest(requestFields{
Method: http.MethodGet,
BasePath: traktPathBaseAPI,
Endpoint: traktPathUserInfo,
Body: http.NoBody,
Headers: tc.defaultApiHeaders(),
})
if err != nil {
return nil, err
}
return decodeReader[*entities.TraktUserInfo](response.Body)
}

func (tc *TraktClient) WatchlistGet() (*entities.TraktList, error) {
response, err := tc.doRequest(requestFields{
Method: http.MethodGet,
Expand Down
84 changes: 60 additions & 24 deletions pkg/client/trakt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1804,7 +1804,7 @@ func TestTraktClient_ActivateAuthorize(t *testing.T) {
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseBrowser+traktPathActivateAuthorize,
httpmock.NewStringResponder(http.StatusOK, `<a class="visible-xs" href="/users/cecobask">Profile</a>`),
httpmock.NewStringResponder(http.StatusOK, `<a href="/logout">Sign Out</a>`),
)
},
assertions: func(assertions *assert.Assertions, err error) {
Expand All @@ -1831,37 +1831,19 @@ func TestTraktClient_ActivateAuthorize(t *testing.T) {
},
},
{
name: "failure scraping username",
name: "failure finding logout selector",
args: args{
authenticityToken: dummyAuthenticityToken,
},
requirements: func() {
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseBrowser+traktPathActivateAuthorize,
httpmock.NewJsonResponderOrPanic(http.StatusOK, nil),
)
},
assertions: func(assertions *assert.Assertions, err error) {
assertions.Error(err)
assertions.Contains(err.Error(), "failure scraping")
},
},
{
name: "failure parsing scrape result to username",
args: args{
authenticityToken: dummyAuthenticityToken,
},
requirements: func() {
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseBrowser+traktPathActivateAuthorize,
httpmock.NewStringResponder(http.StatusOK, `<a class="visible-xs" href="invalid">Profile</a>`),
httpmock.NewStringResponder(http.StatusOK, ""),
)
},
assertions: func(assertions *assert.Assertions, err error) {
assertions.Error(err)
assertions.Contains(err.Error(), "failure scraping")
assertions.Contains(err.Error(), "failure finding selector")
},
},
}
Expand Down Expand Up @@ -2027,13 +2009,18 @@ func TestTraktClient_Hydrate(t *testing.T) {
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseBrowser+traktPathActivateAuthorize,
httpmock.NewStringResponder(http.StatusOK, `<a class="visible-xs" href="/users/cecobask">Profile</a>`),
httpmock.NewStringResponder(http.StatusOK, `<a href="/logout">Sign Out</a>`),
)
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseAPI+traktPathAuthTokens,
httpmock.NewStringResponder(http.StatusOK, `{"access_token":"access-token-value"}`),
)
httpmock.RegisterResponder(
http.MethodGet,
traktPathBaseAPI+traktPathUserInfo,
httpmock.NewStringResponder(http.StatusOK, `{"username":"cecobask"}`),
)
},
assertions: func(assertions *assert.Assertions, err error) {
assertions.NoError(err)
Expand Down Expand Up @@ -2229,7 +2216,7 @@ func TestTraktClient_Hydrate(t *testing.T) {
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseBrowser+traktPathActivateAuthorize,
httpmock.NewStringResponder(http.StatusOK, `<a class="visible-xs" href="/users/cecobask">Profile</a>`),
httpmock.NewStringResponder(http.StatusOK, `<a href="/logout">Sign Out</a>`),
)
httpmock.RegisterResponder(
http.MethodPost,
Expand All @@ -2242,6 +2229,55 @@ func TestTraktClient_Hydrate(t *testing.T) {
assertions.Contains(err.Error(), "failure exchanging trakt device code for access token")
},
},
{
name: "failure getting user info",
requirements: func() {
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseAPI+traktPathAuthCodes,
httpmock.NewStringResponder(http.StatusOK, `{"device_code":"`+dummyDeviceCode+`","user_code":"`+dummyUserCode+`"}`),
)
httpmock.RegisterResponder(
http.MethodGet,
traktPathBaseBrowser+traktPathAuthSignIn,
httpmock.NewStringResponder(http.StatusOK, `<div id="new_user"><input type="hidden" name="authenticity_token" value="authenticity-token-value"></div>`),
)
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseBrowser+traktPathAuthSignIn,
httpmock.NewJsonResponderOrPanic(http.StatusOK, nil),
)
httpmock.RegisterResponder(
http.MethodGet,
traktPathBaseBrowser+traktPathActivate,
httpmock.NewStringResponder(http.StatusOK, `<div id="auth-form-wrapper"><form class="form-signin"><input type="hidden" name="authenticity_token" value="authenticity-token-value"></form></div>`),
)
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseBrowser+traktPathActivate,
httpmock.NewStringResponder(http.StatusOK, `<div id="auth-form-wrapper"><div class="form-signin less-top"><div><form><input type="hidden" name="authenticity_token" value="authenticity-token-value"></form></div></div></div>`),
)
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseBrowser+traktPathActivateAuthorize,
httpmock.NewStringResponder(http.StatusOK, `<a href="/logout">Sign Out</a>`),
)
httpmock.RegisterResponder(
http.MethodPost,
traktPathBaseAPI+traktPathAuthTokens,
httpmock.NewStringResponder(http.StatusOK, `{"access_token":"access-token-value"}`),
)
httpmock.RegisterResponder(
http.MethodGet,
traktPathBaseAPI+traktPathUserInfo,
httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, nil),
)
},
assertions: func(assertions *assert.Assertions, err error) {
assertions.Error(err)
assertions.Contains(err.Error(), "failure getting trakt user info")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down

0 comments on commit 1584180

Please sign in to comment.