User service done

Signed-off-by: Vartan Benohanian <vartanbeno@gmail.com>
This commit is contained in:
Vartan Benohanian 2020-06-18 21:41:17 -04:00
parent 4c33ea3896
commit 5f56273e2e
8 changed files with 499 additions and 69 deletions

View file

@ -46,7 +46,7 @@ func cloneRequest(r *http.Request) *http.Request {
return r2
}
// sets the User-Agent header for requests
// Sets the User-Agent header for requests.
type userAgentTransport struct {
ua string
Base http.RoundTripper
@ -70,12 +70,12 @@ func (t *userAgentTransport) base() http.RoundTripper {
return http.DefaultTransport
}
// RequestCompletionCallback defines the type of the request callback function
// RequestCompletionCallback defines the type of the request callback function.
type RequestCompletionCallback func(*http.Request, *http.Response)
// Client manages communication with the Reddit API
// Client manages communication with the Reddit API.
type Client struct {
// HTTP client used to communicate with the Reddit API
// HTTP client used to communicate with the Reddit API.
client *http.Client
BaseURL *url.URL
@ -88,6 +88,9 @@ type Client struct {
Username string
Password string
// This is the client's user ID in Reddit's database.
redditID string
Comment CommentService
Flair FlairService
Link LinkService
@ -102,7 +105,7 @@ type Client struct {
onRequestCompleted RequestCompletionCallback
}
// OnRequestCompleted sets the client's request completion callback
// OnRequestCompleted sets the client's request completion callback.
func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
c.onRequestCompleted = rc
}
@ -129,7 +132,7 @@ func newClient(httpClient *http.Client) *Client {
return c
}
// NewClient returns a client that can make requests to the Reddit API
// NewClient returns a client that can make requests to the Reddit API.
func NewClient(httpClient *http.Client, opts ...Opt) (c *Client, err error) {
c = newClient(httpClient)
@ -158,9 +161,9 @@ func NewClient(httpClient *http.Client, opts ...Opt) (c *Client, err error) {
return
}
// NewRequest creates an API request
// The path is the relative URL which will be resolves to the BaseURL of the Client
// It should always be specified without a preceding slash
// NewRequest creates an API request.
// The path is the relative URL which will be resolves to the BaseURL of the Client.
// It should always be specified without a preceding slash.
func (c *Client) NewRequest(method, path string, body interface{}) (*http.Request, error) {
u, err := c.BaseURL.Parse(path)
if err != nil {
@ -187,9 +190,9 @@ func (c *Client) NewRequest(method, path string, body interface{}) (*http.Reques
return req, nil
}
// NewPostForm creates an API request with a POST form
// The path is the relative URL which will be resolves to the BaseURL of the Client
// It should always be specified without a preceding slash
// NewPostForm creates an API request with a POST form.
// The path is the relative URL which will be resolves to the BaseURL of the Client.
// It should always be specified without a preceding slash.
func (c *Client) NewPostForm(path string, form url.Values) (*http.Request, error) {
u, err := c.BaseURL.Parse(path)
if err != nil {
@ -212,7 +215,7 @@ type Response struct {
*http.Response
}
// newResponse creates a new Response for the provided http.Response
// newResponse creates a new Response for the provided http.Response.
func newResponse(r *http.Response) *Response {
response := Response{Response: r}
return &response
@ -260,6 +263,21 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Res
return response, err
}
// GetRedditID returns the client's Reddit ID.
func (c *Client) GetRedditID(ctx context.Context) (string, error) {
if c.redditID != "" {
return c.redditID, nil
}
self, _, err := c.User.Get(ctx, c.Username)
if err != nil {
return "", err
}
c.redditID = fmt.Sprintf("%s_%s", kindAccount, self.ID)
return c.redditID, nil
}
// DoRequest submits an HTTP request.
func DoRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
return DoRequestWithClient(ctx, http.DefaultClient, req)
@ -304,7 +322,7 @@ func CheckResponse(r *http.Response) error {
return errorResponse
}
// ListOptions are the optional parameters to the various endpoints that return lists
// ListOptions are the optional parameters to the various endpoints that return lists.
type ListOptions struct {
// For getting submissions
// all, year, month, week, day, hour

6
testdata/user/block.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"date": 1592326190.0,
"icon_img": "https://www.redditstatic.com/avatars/avatar_default_08_C18D42.png",
"id": "t2_3v9o1yoi",
"name": "test123"
}

6
testdata/user/friend.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"date": 1592512594.0,
"rel_id": "r9_tqfqp8",
"name": "test123",
"id": "t2_7b8q1eob"
}

31
testdata/user/trophies.json vendored Normal file
View file

@ -0,0 +1,31 @@
{
"kind": "TrophyList",
"data": {
"trophies": [
{
"kind": "t6",
"data": {
"icon_70": "https://www.redditstatic.com/awards2/3_year_club-70.png",
"name": "Three-Year Club",
"url": null,
"icon_40": "https://www.redditstatic.com/awards2/3_year_club-40.png",
"award_id": null,
"id": null,
"description": null
}
},
{
"kind": "t6",
"data": {
"icon_70": "https://www.redditstatic.com/awards2/verified_email-70.png",
"name": "Verified Email",
"url": null,
"icon_40": "https://www.redditstatic.com/awards2/verified_email-40.png",
"award_id": "o",
"id": "1q1tez",
"description": null
}
}
]
}
}

View file

@ -6,15 +6,16 @@ import (
)
const (
kindComment = "t1"
kindAccount = "t2"
kindLink = "t3"
kindMessage = "t4"
kindSubreddit = "t5"
kindAward = "t6"
kindListing = "Listing"
kindUserList = "UserList"
kindMode = "more"
kindComment = "t1"
kindAccount = "t2"
kindLink = "t3"
kindMessage = "t4"
kindSubreddit = "t5"
kindAward = "t6"
kindListing = "Listing"
kingTrophyList = "TrophyList"
kindUserList = "UserList"
kindMode = "more"
)
// Sort is a sorting option.

252
user.go
View file

@ -2,6 +2,8 @@ package geddit
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
@ -29,9 +31,17 @@ type UserService interface {
Hidden(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error)
Gilded(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error)
Friend(ctx context.Context, username string, note string) (interface{}, *Response, error)
Unblock(ctx context.Context, username string) (*Response, error)
GetFriendship(ctx context.Context, username string) (*Friendship, *Response, error)
Friend(ctx context.Context, username string) (*Friendship, *Response, error)
Unfriend(ctx context.Context, username string) (*Response, error)
Block(ctx context.Context, username string) (*Blocked, *Response, error)
// BlockByID(ctx context.Context, id string) (*Blocked, *Response, error)
Unblock(ctx context.Context, username string) (*Response, error)
// UnblockByID(ctx context.Context, id string) (*Response, error)
GetTrophies(ctx context.Context) (Trophies, *Response, error)
GetTrophiesOf(ctx context.Context, username string) (Trophies, *Response, error)
}
// UserServiceOp implements the UserService interface
@ -70,6 +80,79 @@ type UserShort struct {
NSFW bool `json:"profile_over_18"`
}
// Friendship represents a friend relationship.
type Friendship struct {
ID string `json:"rel_id,omitempty"`
Friend string `json:"name,omitempty"`
FriendID string `json:"id,omitempty"`
Created *Timestamp `json:"date,omitempty"`
}
// Blocked represents a blocked relationship.
type Blocked struct {
Blocked string `json:"name,omitempty"`
BlockedID string `json:"id,omitempty"`
Created *Timestamp `json:"date,omitempty"`
}
type rootTrophyListing struct {
Kind string `json:"kind,omitempty"`
Trophies Trophies `json:"data"`
}
// Trophy is a Reddit award.
type Trophy struct {
Name string `json:"name"`
}
// Trophies is a list of trophies.
type Trophies []Trophy
// UnmarshalJSON implements the json.Unmarshaler interface.
func (t *Trophies) UnmarshalJSON(b []byte) error {
var data map[string]interface{}
if err := json.Unmarshal(b, &data); err != nil {
return err
}
trophies, ok := data["trophies"]
if !ok {
return errors.New("data does not contain trophies")
}
trophyList, ok := trophies.([]interface{})
if !ok {
return errors.New("unexpected type for list of trophies")
}
for _, trophyData := range trophyList {
trophyInfo, ok := trophyData.(map[string]interface{})
if !ok {
continue
}
info, ok := trophyInfo["data"]
if !ok {
continue
}
infoBytes, err := json.Marshal(info)
if err != nil {
continue
}
var trophy Trophy
err = json.Unmarshal(infoBytes, &trophy)
if err != nil {
continue
}
*t = append(*t, trophy)
}
return nil
}
// Get returns information about the user
func (s *UserServiceOp) Get(ctx context.Context, username string) (*User, *Response, error) {
path := fmt.Sprintf("user/%s/about", username)
@ -324,58 +407,113 @@ func (s *UserServiceOp) Gilded(ctx context.Context, opts ...SearchOptionSetter)
return root.getLinks(), resp, nil
}
// // Friend creates or updates a "friend" relationship
// // Request body contains JSON data with:
// // name: existing Reddit username
// // note: a string no longer than 300 characters
// func (s *UserServiceOp) Friend(ctx context.Context, username string, note string) (interface{}, *Response, error) {
// type request struct {
// Username string `url:"name"`
// Note string `url:"note"`
// }
// GetFriendship returns friendship details with the specified user.
// If the user is not your friend, it will return an error.
func (s *UserServiceOp) GetFriendship(ctx context.Context, username string) (*Friendship, *Response, error) {
path := fmt.Sprintf("api/v1/me/friends/%s", username)
// path := fmt.Sprintf("api/v1/me/friends/%s", username)
// body := request{Username: username, Note: note}
req, err := s.client.NewRequest(http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
// _, err := s.client.NewRequest(http.MethodPut, path, body)
// if err != nil {
// return false, nil, err
// }
root := new(Friendship)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
// // todo: requires gold
// return nil, nil, nil
// }
return root, resp, nil
}
// Friend creates or updates a "friend" relationship
// Request body contains JSON data with:
// name: existing Reddit username
// note: a string no longer than 300 characters
func (s *UserServiceOp) Friend(ctx context.Context, username string, note string) (interface{}, *Response, error) {
// Friend friends a user.
func (s *UserServiceOp) Friend(ctx context.Context, username string) (*Friendship, *Response, error) {
type request struct {
Username string `url:"name"`
Note string `url:"note"`
Username string `json:"name"`
}
path := fmt.Sprintf("api/v1/me/friends/%s", username)
body := request{Username: username, Note: note}
body := request{username}
_, err := s.client.NewRequest(http.MethodPut, path, body)
req, err := s.client.NewRequest(http.MethodPut, path, body)
if err != nil {
return false, nil, err
return nil, nil, err
}
// todo: requires gold
return nil, nil, nil
root := new(Friendship)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root, resp, nil
}
// Unblock unblocks a user
// Unfriend unfriends a user.
func (s *UserServiceOp) Unfriend(ctx context.Context, username string) (*Response, error) {
path := fmt.Sprintf("api/v1/me/friends/%s", username)
req, err := s.client.NewRequest(http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
return s.client.Do(ctx, req, nil)
}
// Block blocks a user.
func (s *UserServiceOp) Block(ctx context.Context, username string) (*Blocked, *Response, error) {
path := "api/block_user"
form := url.Values{}
form.Set("name", username)
req, err := s.client.NewPostForm(path, form)
if err != nil {
return nil, nil, err
}
root := new(Blocked)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root, resp, nil
}
// // BlockByID blocks a user via their full id.
// func (s *UserServiceOp) BlockByID(ctx context.Context, id string) (*Blocked, *Response, error) {
// path := "api/block_user"
// form := url.Values{}
// form.Set("account_id", id)
// req, err := s.client.NewPostForm(path, form)
// if err != nil {
// return nil, nil, err
// }
// root := new(Blocked)
// resp, err := s.client.Do(ctx, req, root)
// if err != nil {
// return nil, resp, err
// }
// return root, resp, nil
// }
// Unblock unblocks a user.
func (s *UserServiceOp) Unblock(ctx context.Context, username string) (*Response, error) {
selfID, err := s.client.GetRedditID(ctx)
if err != nil {
return nil, err
}
path := "api/unfriend"
form := url.Values{}
form.Set("name", username)
form.Set("type", "enemy")
form.Set("container", "todo: this should be the current user's full id")
form.Set("container", selfID)
req, err := s.client.NewPostForm(path, form)
if err != nil {
@ -385,13 +523,47 @@ func (s *UserServiceOp) Unblock(ctx context.Context, username string) (*Response
return s.client.Do(ctx, req, nil)
}
// Unfriend unfriends a user
func (s *UserServiceOp) Unfriend(ctx context.Context, username string) (*Response, error) {
path := fmt.Sprintf("api/v1/me/friends/%s", username)
req, err := s.client.NewRequest(http.MethodDelete, path, nil)
// // UnblockByID unblocks a user via their full id.
// func (s *UserServiceOp) UnblockByID(ctx context.Context, id string) (*Response, error) {
// selfID, err := s.client.GetRedditID(ctx)
// if err != nil {
// return nil, err
// }
// path := "api/unfriend"
// form := url.Values{}
// form.Set("id", id)
// form.Set("type", "enemy")
// form.Set("container", selfID)
// req, err := s.client.NewPostForm(path, form)
// if err != nil {
// return nil, err
// }
// return s.client.Do(ctx, req, nil)
// }
// GetTrophies returns a list of your trophies.
func (s *UserServiceOp) GetTrophies(ctx context.Context) (Trophies, *Response, error) {
return s.GetTrophiesOf(ctx, s.client.Username)
}
// GetTrophiesOf returns a list of the specified user's trophies.
func (s *UserServiceOp) GetTrophiesOf(ctx context.Context, username string) (Trophies, *Response, error) {
path := fmt.Sprintf("api/v1/user/%s/trophies", username)
req, err := s.client.NewRequest(http.MethodGet, path, nil)
if err != nil {
return nil, err
return nil, nil, err
}
return s.client.Do(ctx, req, nil)
root := new(rootTrophyListing)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root.Trophies, resp, nil
}

View file

@ -1,6 +1,7 @@
package geddit
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
@ -102,6 +103,24 @@ var expectedComment = Comment{
LinkNumComments: 89751,
}
var expectedFriendship = &Friendship{
ID: "r9_tqfqp8",
Friend: "test123",
FriendID: "t2_7b8q1eob",
Created: &Timestamp{time.Date(2020, 6, 18, 20, 36, 34, 0, time.UTC)},
}
var expectedBlocked = &Blocked{
Blocked: "test123",
BlockedID: "t2_3v9o1yoi",
Created: &Timestamp{time.Date(2020, 6, 16, 16, 49, 50, 0, time.UTC)},
}
var expectedTrophies = Trophies{
{Name: "Three-Year Club"},
{Name: "Verified Email"},
}
func TestUserService_Get(t *testing.T) {
setup()
defer teardown()
@ -564,3 +583,185 @@ func TestUserService_Gilded(t *testing.T) {
assert.Equal(t, "t3_gczwql", posts.After)
assert.Equal(t, "", posts.Before)
}
func TestUserService_GetFriendship(t *testing.T) {
setup()
defer teardown()
blob := readFileContents(t, "testdata/user/friend.json")
mux.HandleFunc("/api/v1/me/friends/test123", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
fmt.Fprint(w, blob)
})
friendship, _, err := client.User.GetFriendship(ctx, "test123")
assert.NoError(t, err)
assert.Equal(t, expectedFriendship, friendship)
}
func TestUserService_Friend(t *testing.T) {
setup()
defer teardown()
blob := readFileContents(t, "testdata/user/friend.json")
mux.HandleFunc("/api/v1/me/friends/test123", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPut, r.Method)
type request struct {
Username string `json:"name"`
}
var req request
err := json.NewDecoder(r.Body).Decode(&req)
assert.NoError(t, err)
assert.Equal(t, "test123", req.Username)
fmt.Fprint(w, blob)
})
friendship, _, err := client.User.Friend(ctx, "test123")
assert.NoError(t, err)
assert.Equal(t, expectedFriendship, friendship)
}
func TestUserService_Unfriend(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/api/v1/me/friends/test123", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodDelete, r.Method)
w.WriteHeader(http.StatusNoContent)
})
res, err := client.User.Unfriend(ctx, "test123")
assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, res.StatusCode)
}
func TestUserService_Block(t *testing.T) {
setup()
defer teardown()
blob := readFileContents(t, "testdata/user/block.json")
mux.HandleFunc("/api/block_user", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
form := url.Values{}
form.Set("name", "test123")
err := r.ParseForm()
assert.NoError(t, err)
assert.Equal(t, form, r.Form)
fmt.Fprint(w, blob)
})
blocked, _, err := client.User.Block(ctx, "test123")
assert.NoError(t, err)
assert.Equal(t, expectedBlocked, blocked)
}
// func TestUserService_BlockByID(t *testing.T) {
// setup()
// defer teardown()
// blob := readFileContents(t, "testdata/user/block.json")
// mux.HandleFunc("/api/block_user", func(w http.ResponseWriter, r *http.Request) {
// assert.Equal(t, http.MethodPost, r.Method)
// form := url.Values{}
// form.Set("account_id", "abc123")
// err := r.ParseForm()
// assert.NoError(t, err)
// assert.Equal(t, form, r.Form)
// fmt.Fprint(w, blob)
// })
// blocked, _, err := client.User.BlockByID(ctx, "abc123")
// assert.NoError(t, err)
// assert.Equal(t, expectedBlocked, blocked)
// }
func TestUserService_Unblock(t *testing.T) {
setup()
defer teardown()
client.redditID = "self123"
mux.HandleFunc("/api/unfriend", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
form := url.Values{}
form.Set("name", "test123")
form.Set("type", "enemy")
form.Set("container", client.redditID)
err := r.ParseForm()
assert.NoError(t, err)
assert.Equal(t, form, r.Form)
})
_, err := client.User.Unblock(ctx, "test123")
assert.NoError(t, err)
}
// func TestUserService_UnblockByID(t *testing.T) {
// setup()
// defer teardown()
// client.redditID = "self123"
// mux.HandleFunc("/api/unfriend", func(w http.ResponseWriter, r *http.Request) {
// assert.Equal(t, http.MethodPost, r.Method)
// form := url.Values{}
// form.Set("id", "abc123")
// form.Set("type", "enemy")
// form.Set("container", client.redditID)
// err := r.ParseForm()
// assert.NoError(t, err)
// assert.Equal(t, form, r.Form)
// })
// _, err := client.User.UnblockByID(ctx, "abc123")
// assert.NoError(t, err)
// }
func TestUserService_GetTrophies(t *testing.T) {
setup()
defer teardown()
blob := readFileContents(t, "testdata/user/trophies.json")
mux.HandleFunc("/api/v1/user/user1/trophies", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
fmt.Fprint(w, blob)
})
trophies, _, err := client.User.GetTrophies(ctx)
assert.NoError(t, err)
assert.Equal(t, expectedTrophies, trophies)
}
func TestUserService_GetTrophiesOf(t *testing.T) {
setup()
defer teardown()
blob := readFileContents(t, "testdata/user/trophies.json")
mux.HandleFunc("/api/v1/user/test123/trophies", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
fmt.Fprint(w, blob)
})
trophies, _, err := client.User.GetTrophiesOf(ctx, "test123")
assert.NoError(t, err)
assert.Equal(t, expectedTrophies, trophies)
}

View file

@ -43,12 +43,7 @@ func (s *VoteServiceOp) vote(ctx context.Context, id string, vote vote) (*Respon
return nil, err
}
resp, err := s.client.Do(ctx, req, nil)
if err != nil {
return resp, err
}
return resp, nil
return s.client.Do(ctx, req, nil)
}
// Up upvotes a link or a comment