diff --git a/geddit.go b/geddit.go index 119e5c4..4392631 100644 --- a/geddit.go +++ b/geddit.go @@ -341,3 +341,10 @@ func addOptions(s string, opt interface{}) (string, error) { origURL.RawQuery = origValues.Encode() return origURL.String(), nil } + +func addQuery(url string, query url.Values) string { + if query == nil || len(query) == 0 { + return url + } + return url + "?" + query.Encode() +} diff --git a/search.go b/search.go index 121fec0..e562426 100644 --- a/search.go +++ b/search.go @@ -3,6 +3,8 @@ package geddit import ( "context" "fmt" + "net/http" + "net/url" "strings" ) @@ -14,9 +16,9 @@ import ( // Note: The "limit" parameter in searches is prone to inconsistent // behaviour. type SearchService interface { - Posts(query string, opts ...SearchOpt) *PostSearcher - Subreddits(query string, opts ...SearchOpt) *SubredditSearcher - Users(query string, opts ...SearchOpt) *UserSearcher + Posts(ctx context.Context, query string, subreddits []string, opts ...SearchOptionSetter) (*Links, *Response, error) + Subreddits(ctx context.Context, query string, opts ...SearchOptionSetter) (*Subreddits, *Response, error) + Users(ctx context.Context, query string, opts ...SearchOptionSetter) (*Users, *Response, error) } // SearchServiceOp implements the VoteService interface @@ -26,60 +28,96 @@ type SearchServiceOp struct { var _ SearchService = &SearchServiceOp{} +// SearchOptions define options used in search queries. +type SearchOptions = url.Values + +func newSearchOptions(opts ...SearchOptionSetter) SearchOptions { + searchOptions := make(SearchOptions) + for _, opt := range opts { + opt(searchOptions) + } + return searchOptions +} + +// SearchOptionSetter sets values for the options. +type SearchOptionSetter func(opts SearchOptions) + +// SetAfter sets the after option. +func SetAfter(v string) SearchOptionSetter { + return func(opts SearchOptions) { + opts.Set("after", v) + } +} + +// SetBefore sets the before option. +func SetBefore(v string) SearchOptionSetter { + return func(opts SearchOptions) { + opts.Set("before", v) + } +} + +// SetLimit sets the limit option. +// Warning: It seems like setting the limit to 1 sometimes returns 0 results. +func SetLimit(v int) SearchOptionSetter { + return func(opts SearchOptions) { + opts.Set("limit", fmt.Sprint(v)) + } +} + +// SetSort sets the sort option. +func SetSort(v Sort) SearchOptionSetter { + return func(opts SearchOptions) { + opts.Set("sort", v.String()) + } +} + +// SetTimespan sets the timespan option. +func SetTimespan(v Timespan) SearchOptionSetter { + return func(opts SearchOptions) { + opts.Set("timespan", v.String()) + } +} + +// setType sets the type option. +func setType(v string) SearchOptionSetter { + return func(opts SearchOptions) { + opts.Set("type", v) + } +} + +// setQuery sets the q option. +func setQuery(v string) SearchOptionSetter { + return func(opts SearchOptions) { + opts.Set("q", v) + } +} + +// setRestrict sets the restrict_sr option. +func setRestrict(opts SearchOptions) { + opts.Set("restruct_sr", "true") +} + // Posts searches for posts. -// By default, it searches for the most relevant posts of all time. -func (s *SearchServiceOp) Posts(query string, opts ...SearchOpt) *PostSearcher { - sr := new(PostSearcher) - sr.client = s.client - sr.opts.Query = query - sr.opts.Type = "link" - sr.opts.Sort = SortRelevance.String() - sr.opts.Timespan = TimespanAll.String() - for _, opt := range opts { - opt(sr) - } - return sr -} +// If the list of subreddits is empty, the search is run against r/all. +func (s *SearchServiceOp) Posts(ctx context.Context, query string, subreddits []string, opts ...SearchOptionSetter) (*Links, *Response, error) { + opts = append(opts, setType("link"), setQuery(query)) -// Subreddits searches for subreddits. -func (s *SearchServiceOp) Subreddits(query string, opts ...SearchOpt) *SubredditSearcher { - sr := new(SubredditSearcher) - sr.client = s.client - sr.opts.Query = query - sr.opts.Type = "sr" - for _, opt := range opts { - opt(sr) - } - return sr -} - -// Users searches for users. -func (s *SearchServiceOp) Users(query string, opts ...SearchOpt) *UserSearcher { - sr := new(UserSearcher) - sr.client = s.client - sr.opts.Query = query - sr.opts.Type = "user" - for _, opt := range opts { - opt(sr) - } - return sr -} - -// PostSearcher helps conducts searches that return posts. -type PostSearcher struct { - clientSearcher - subreddits []string - after string - Results []Link -} - -func (s *PostSearcher) search(ctx context.Context) (*Links, *Response, error) { path := "search" - if len(s.subreddits) > 0 { - path = fmt.Sprintf("r/%s/search", strings.Join(s.subreddits, "+")) + if len(subreddits) > 0 { + path = fmt.Sprintf("r/%s/search", strings.Join(subreddits, "+")) + opts = append(opts, setRestrict) } - root, resp, err := s.clientSearcher.Do(ctx, path) + form := newSearchOptions(opts...) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -87,232 +125,48 @@ func (s *PostSearcher) search(ctx context.Context) (*Links, *Response, error) { return root.getLinks(), resp, nil } -// Search runs the searcher. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *PostSearcher) Search(ctx context.Context) (bool, *Response, error) { - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } +// Subreddits searches for subreddits. +// The Sort and Timespan options don't affect the results for this search. +func (s *SearchServiceOp) Subreddits(ctx context.Context, query string, opts ...SearchOptionSetter) (*Subreddits, *Response, error) { + opts = append(opts, setType("sr"), setQuery(query)) + form := newSearchOptions(opts...) - s.Results = root.Links - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// More runs the searcher again and adds to the results. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *PostSearcher) More(ctx context.Context) (bool, *Response, error) { - if s.after == "" { - return s.Search(ctx) - } - - s.setAfter(s.after) - - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results = append(s.Results, root.Links...) - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// All runs the searcher until it yields no more results. -// The limit is set to 100, just to make the least amount -// of requests possible. It is reset to its original value after. -func (s *PostSearcher) All(ctx context.Context) error { - limit := s.opts.Limit - - s.setLimit(100) - defer s.setLimit(limit) - - var ok = true - var err error - - for ok { - ok, _, err = s.More(ctx) - if err != nil { - return err - } - } - - return nil -} - -// SubredditSearcher helps conducts searches that return subreddits. -type SubredditSearcher struct { - clientSearcher - after string - Results []Subreddit -} - -func (s *SubredditSearcher) search(ctx context.Context) (*Subreddits, *Response, error) { path := "search" - root, resp, err := s.clientSearcher.Do(ctx, path) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } + return root.getSubreddits(), resp, nil } -// Search runs the searcher. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *SubredditSearcher) Search(ctx context.Context) (bool, *Response, error) { - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } +// Users searches for users. +// The Sort and Timespan options don't affect the results for this search. +func (s *SearchServiceOp) Users(ctx context.Context, query string, opts ...SearchOptionSetter) (*Users, *Response, error) { + opts = append(opts, setType("user"), setQuery(query)) + form := newSearchOptions(opts...) - s.Results = root.Subreddits - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// More runs the searcher again and adds to the results. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *SubredditSearcher) More(ctx context.Context) (bool, *Response, error) { - if s.after == "" { - return s.Search(ctx) - } - - s.setAfter(s.after) - - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results = append(s.Results, root.Subreddits...) - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// All runs the searcher until it yields no more results. -// The limit is set to 100, just to make the least amount -// of requests possible. It is reset to its original value after. -func (s *SubredditSearcher) All(ctx context.Context) error { - limit := s.opts.Limit - - s.setLimit(100) - defer s.setLimit(limit) - - var ok = true - var err error - - for ok { - ok, _, err = s.More(ctx) - if err != nil { - return err - } - } - - return nil -} - -// UserSearcher helps conducts searches that return users. -type UserSearcher struct { - clientSearcher - after string - Results []User -} - -func (s *UserSearcher) search(ctx context.Context) (*Users, *Response, error) { path := "search" - root, resp, err := s.clientSearcher.Do(ctx, path) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } + return root.getUsers(), resp, nil } - -// Search runs the searcher. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *UserSearcher) Search(ctx context.Context) (bool, *Response, error) { - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results = root.Users - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// More runs the searcher again and adds to the results. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *UserSearcher) More(ctx context.Context) (bool, *Response, error) { - if s.after == "" { - return s.Search(ctx) - } - - s.setAfter(s.after) - - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results = append(s.Results, root.Users...) - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// All runs the searcher until it yields no more results. -// The limit is set to 100, just to make the least amount -// of requests possible. It is reset to its original value after. -func (s *UserSearcher) All(ctx context.Context) error { - limit := s.opts.Limit - - s.setLimit(100) - defer s.setLimit(limit) - - var ok = true - var err error - - for ok { - ok, _, err = s.More(ctx) - if err != nil { - return err - } - } - - return nil -} diff --git a/searcher.go b/searcher.go deleted file mode 100644 index 6a26b9d..0000000 --- a/searcher.go +++ /dev/null @@ -1,134 +0,0 @@ -package geddit - -import ( - "context" - "net/http" -) - -// todo: query parameter "show" = "all" - -// Searcher defines some parameters common to all requests -// used to conduct searches against the Reddit API. -type Searcher interface { - setAfter(string) - setBefore(string) - setLimit(int) - setSort(Sort) - setTimespan(Timespan) -} - -// Contains all options used for searching. -// Not all are used for every search endpoint. -// For example, for getting a user's posts, "q" is not used. -// After/Before are used as the anchor points for subsequent searches. -// Limit is the maximum number of items to be returned (default: 25, max: 100). -// Sort: hot, new, top, controversial, etc. -// Timespan: hour, day, week, month, year, all. -type searchOpts struct { - Query string `url:"q,omitempty"` - Type string `url:"type,omitempty"` - After string `url:"after,omitempty"` - Before string `url:"before,omitempty"` - Limit int `url:"limit,omitempty"` - RestrictSubreddits bool `url:"restrict_sr,omitempty"` - Sort string `url:"sort,omitempty"` - Timespan string `url:"t,omitempty"` -} - -type clientSearcher struct { - client *Client - opts searchOpts -} - -var _ Searcher = &clientSearcher{} - -func (s *clientSearcher) setAfter(v string) { - s.opts.After = v -} - -func (s *clientSearcher) setBefore(v string) { - s.opts.Before = v -} - -func (s *clientSearcher) setLimit(v int) { - s.opts.Limit = v -} - -func (s *clientSearcher) setSort(v Sort) { - s.opts.Sort = v.String() -} - -func (s *clientSearcher) setTimespan(v Timespan) { - s.opts.Timespan = v.String() -} - -func (s *clientSearcher) Do(ctx context.Context, path string) (*rootListing, *Response, error) { - path, err := addOptions(path, s.opts) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(rootListing) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root, resp, nil -} - -// SearchOpt sets search options. -type SearchOpt func(s Searcher) - -// SetAfter sets the after option. -func SetAfter(v string) SearchOpt { - return func(s Searcher) { - s.setAfter(v) - } -} - -// SetBefore sets the before option. -func SetBefore(v string) SearchOpt { - return func(s Searcher) { - s.setBefore(v) - } -} - -// SetLimit sets the limit option. -func SetLimit(v int) SearchOpt { - return func(s Searcher) { - s.setLimit(v) - } -} - -// SetSort sets the sort option. -func SetSort(v Sort) SearchOpt { - return func(s Searcher) { - s.setSort(v) - } -} - -// SetTimespan sets the timespan option. -func SetTimespan(v Timespan) SearchOpt { - return func(s Searcher) { - s.setTimespan(v) - } -} - -// FromSubreddits is an option that restricts the -// search to happen in the specified subreddits. -// If none are specified, it's like searching r/all. -// This option is only applicable to the PostSearcher. -func FromSubreddits(subreddits ...string) SearchOpt { - return func(s Searcher) { - if ps, ok := s.(*PostSearcher); ok { - ps.subreddits = subreddits - ps.opts.RestrictSubreddits = len(subreddits) > 0 - } - } -} diff --git a/user.go b/user.go index 06070f1..6485017 100644 --- a/user.go +++ b/user.go @@ -14,20 +14,20 @@ type UserService interface { GetMultipleByID(ctx context.Context, ids ...string) (map[string]*UserShort, *Response, error) UsernameAvailable(ctx context.Context, username string) (bool, *Response, error) - Overview(opts ...SearchOpt) *UserCommentPostSearcher - OverviewOf(username string, opts ...SearchOpt) *UserCommentPostSearcher + Overview(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Comments, *Response, error) + OverviewOf(ctx context.Context, username string, opts ...SearchOptionSetter) (*Links, *Comments, *Response, error) - Posts(opts ...SearchOpt) *UserPostSearcher - PostsOf(username string, opts ...SearchOpt) *UserPostSearcher + Posts(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error) + PostsOf(ctx context.Context, username string, opts ...SearchOptionSetter) (*Links, *Response, error) - Comments(opts ...SearchOpt) *UserCommentSearcher - CommentsOf(username string, opts ...SearchOpt) *UserCommentSearcher + Comments(ctx context.Context, opts ...SearchOptionSetter) (*Comments, *Response, error) + CommentsOf(ctx context.Context, username string, opts ...SearchOptionSetter) (*Comments, *Response, error) - GetUpvoted(opts ...SearchOpt) *UserPostSearcher - GetDownvoted(opts ...SearchOpt) *UserPostSearcher - GetHidden(opts ...SearchOpt) *UserPostSearcher - GetSaved(ctx context.Context, opts *ListOptions) (*CommentsLinks, *Response, error) - GetGilded(ctx context.Context, opts *ListOptions) (*CommentsLinks, *Response, error) + Saved(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Comments, *Response, error) + Upvoted(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error) + Downvoted(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error) + 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) @@ -105,13 +105,13 @@ func (s *UserServiceOp) GetMultipleByID(ctx context.Context, ids ...string) (map return nil, nil, err } - root := new(map[string]*UserShort) + root := make(map[string]*UserShort) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } - return *root, resp, nil + return root, resp, nil } // UsernameAvailable checks whether a username is available for registration @@ -142,101 +142,186 @@ func (s *UserServiceOp) UsernameAvailable(ctx context.Context, username string) } // Overview returns a list of the client's comments and links -func (s *UserServiceOp) Overview(opts ...SearchOpt) *UserCommentPostSearcher { - return s.OverviewOf(s.client.Username, opts...) +func (s *UserServiceOp) Overview(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Comments, *Response, error) { + return s.OverviewOf(ctx, s.client.Username, opts...) } // OverviewOf returns a list of the user's comments and links -func (s *UserServiceOp) OverviewOf(username string, opts ...SearchOpt) *UserCommentPostSearcher { - sr := new(UserCommentPostSearcher) - sr.client = s.client - sr.username = username - sr.where = "overview" - for _, opt := range opts { - opt(sr) +func (s *UserServiceOp) OverviewOf(ctx context.Context, username string, opts ...SearchOptionSetter) (*Links, *Comments, *Response, error) { + form := newSearchOptions(opts...) + + path := fmt.Sprintf("user/%s/overview", username) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, nil, err } - return sr + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, nil, resp, err + } + + return root.getLinks(), root.getComments(), resp, nil } // Posts returns a list of the client's posts. -func (s *UserServiceOp) Posts(opts ...SearchOpt) *UserPostSearcher { - return s.PostsOf(s.client.Username, opts...) +func (s *UserServiceOp) Posts(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error) { + return s.PostsOf(ctx, s.client.Username, opts...) } // PostsOf returns a list of the user's posts. -func (s *UserServiceOp) PostsOf(username string, opts ...SearchOpt) *UserPostSearcher { - sr := new(UserPostSearcher) - sr.client = s.client - sr.username = username - sr.where = "submitted" - for _, opt := range opts { - opt(sr) +func (s *UserServiceOp) PostsOf(ctx context.Context, username string, opts ...SearchOptionSetter) (*Links, *Response, error) { + form := newSearchOptions(opts...) + + path := fmt.Sprintf("user/%s/submitted", username) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err } - return sr + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.getLinks(), resp, nil } // Comments returns a list of the client's comments. -func (s *UserServiceOp) Comments(opts ...SearchOpt) *UserCommentSearcher { - return s.CommentsOf(s.client.Username, opts...) +func (s *UserServiceOp) Comments(ctx context.Context, opts ...SearchOptionSetter) (*Comments, *Response, error) { + return s.CommentsOf(ctx, s.client.Username, opts...) } -// CommentsOf returns a list of a user's comments. -func (s *UserServiceOp) CommentsOf(username string, opts ...SearchOpt) *UserCommentSearcher { - sr := new(UserCommentSearcher) - sr.client = s.client - sr.username = username - for _, opt := range opts { - opt(sr) +// CommentsOf returns a list of the user's comments. +func (s *UserServiceOp) CommentsOf(ctx context.Context, username string, opts ...SearchOptionSetter) (*Comments, *Response, error) { + form := newSearchOptions(opts...) + + path := fmt.Sprintf("user/%s/comments", username) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err } - return sr -} -// GetUpvoted returns a list of the client's upvoted submissions. -func (s *UserServiceOp) GetUpvoted(opts ...SearchOpt) *UserPostSearcher { - sr := new(UserPostSearcher) - sr.client = s.client - sr.username = s.client.Username - sr.where = "upvoted" - for _, opt := range opts { - opt(sr) + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err } - return sr + + return root.getComments(), resp, nil } -// GetDownvoted returns a list of the client's downvoted submissions. -func (s *UserServiceOp) GetDownvoted(opts ...SearchOpt) *UserPostSearcher { - sr := new(UserPostSearcher) - sr.client = s.client - sr.username = s.client.Username - sr.where = "downvoted" - for _, opt := range opts { - opt(sr) - } - return sr -} +// Saved returns a list of the user's saved posts and comments. +func (s *UserServiceOp) Saved(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Comments, *Response, error) { + form := newSearchOptions(opts...) -// GetHidden returns a list of the client's hidden submissions. -func (s *UserServiceOp) GetHidden(opts ...SearchOpt) *UserPostSearcher { - sr := new(UserPostSearcher) - sr.client = s.client - sr.username = s.client.Username - sr.where = "hidden" - for _, opt := range opts { - opt(sr) - } - return sr -} - -// GetSaved returns a list of the client's saved comments and links. -func (s *UserServiceOp) GetSaved(ctx context.Context, opts *ListOptions) (*CommentsLinks, *Response, error) { path := fmt.Sprintf("user/%s/saved", s.client.Username) - return s.getCommentsAndLinks(ctx, path, opts) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, nil, err + } + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, nil, resp, err + } + + return root.getLinks(), root.getComments(), resp, nil } -// GetGilded returns a list of the client's gilded comments and links. -func (s *UserServiceOp) GetGilded(ctx context.Context, opts *ListOptions) (*CommentsLinks, *Response, error) { +// Upvoted returns a list of the user's upvoted posts. +func (s *UserServiceOp) Upvoted(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error) { + form := newSearchOptions(opts...) + + path := fmt.Sprintf("user/%s/upvoted", s.client.Username) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.getLinks(), resp, nil +} + +// Downvoted returns a list of the user's downvoted posts. +func (s *UserServiceOp) Downvoted(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error) { + form := newSearchOptions(opts...) + + path := fmt.Sprintf("user/%s/downvoted", s.client.Username) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.getLinks(), resp, nil +} + +// Hidden returns a list of the user's hidden posts. +func (s *UserServiceOp) Hidden(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error) { + form := newSearchOptions(opts...) + + path := fmt.Sprintf("user/%s/hidden", s.client.Username) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.getLinks(), resp, nil +} + +// Gilded returns a list of the user's gilded posts. +func (s *UserServiceOp) Gilded(ctx context.Context, opts ...SearchOptionSetter) (*Links, *Response, error) { + form := newSearchOptions(opts...) + path := fmt.Sprintf("user/%s/gilded", s.client.Username) - return s.getCommentsAndLinks(ctx, path, opts) + path = addQuery(path, form) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.getLinks(), resp, nil } // Friend creates or updates a "friend" relationship @@ -245,12 +330,12 @@ func (s *UserServiceOp) GetGilded(ctx context.Context, opts *ListOptions) (*Comm // 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 { - Name string `url:"name"` - Note string `url:"note"` + Username string `url:"name"` + Note string `url:"note"` } path := fmt.Sprintf("api/v1/me/friends/%s", username) - body := request{Name: username, Note: note} + body := request{Username: username, Note: note} _, err := s.client.NewRequest(http.MethodPut, path, body) if err != nil { @@ -288,312 +373,3 @@ func (s *UserServiceOp) Unfriend(ctx context.Context, username string) (*Respons return s.client.Do(ctx, req, nil) } - -func (s *UserServiceOp) getLinks(ctx context.Context, path string, opts *ListOptions) (*Links, *Response, error) { - listing, resp, err := s.getListing(ctx, path, opts) - if err != nil { - return nil, resp, err - } - return listing.getLinks(), resp, nil -} - -func (s *UserServiceOp) getComments(ctx context.Context, path string, opts *ListOptions) (*Comments, *Response, error) { - listing, resp, err := s.getListing(ctx, path, opts) - if err != nil { - return nil, resp, err - } - return listing.getComments(), resp, nil -} - -func (s *UserServiceOp) getCommentsAndLinks(ctx context.Context, path string, opts *ListOptions) (*CommentsLinks, *Response, error) { - listing, resp, err := s.getListing(ctx, path, opts) - if err != nil { - return nil, resp, err - } - - v := new(CommentsLinks) - v.Comments = listing.getComments().Comments - v.Links = listing.getLinks().Links - v.After = listing.getAfter() - v.Before = listing.getBefore() - - return v, resp, nil -} - -func (s *UserServiceOp) getListing(ctx context.Context, path string, opts *ListOptions) (*rootListing, *Response, error) { - path, err := addOptions(path, opts) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(rootListing) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root, resp, err -} - -// UserPostSearcher finds the posts of a user. -type UserPostSearcher struct { - clientSearcher - username string - // where can be submitted, upvoted, downvoted, hidden - // https://www.reddit.com/dev/api/#GET_user_{username}_{where} - where string - after string - Results []Link -} - -func (s *UserPostSearcher) search(ctx context.Context) (*Links, *Response, error) { - path := fmt.Sprintf("user/%s/%s", s.username, s.where) - root, resp, err := s.clientSearcher.Do(ctx, path) - if err != nil { - return nil, resp, err - } - return root.getLinks(), resp, nil -} - -// Search runs the searcher. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *UserPostSearcher) Search(ctx context.Context) (bool, *Response, error) { - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results = root.Links - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// More runs the searcher again and adds to the results. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *UserPostSearcher) More(ctx context.Context) (bool, *Response, error) { - if s.after == "" { - return s.Search(ctx) - } - - s.setAfter(s.after) - - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results = append(s.Results, root.Links...) - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// All runs the searcher until it yields no more results. -// The limit is set to 100, just to make the least amount -// of requests possible. It is reset to its original value after. -func (s *UserPostSearcher) All(ctx context.Context) error { - limit := s.opts.Limit - - s.setLimit(100) - defer s.setLimit(limit) - - var ok = true - var err error - - for ok { - ok, _, err = s.More(ctx) - if err != nil { - return err - } - } - - return nil -} - -// UserCommentSearcher finds the comments of a user. -type UserCommentSearcher struct { - clientSearcher - username string - after string - Results []Comment -} - -func (s *UserCommentSearcher) search(ctx context.Context) (*Comments, *Response, error) { - path := fmt.Sprintf("user/%s/comments", s.username) - root, resp, err := s.clientSearcher.Do(ctx, path) - if err != nil { - return nil, resp, err - } - return root.getComments(), resp, nil -} - -// Search runs the searcher. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *UserCommentSearcher) Search(ctx context.Context) (bool, *Response, error) { - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results = root.Comments - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// More runs the searcher again and adds to the results. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *UserCommentSearcher) More(ctx context.Context) (bool, *Response, error) { - if s.after == "" { - return s.Search(ctx) - } - - s.setAfter(s.after) - - root, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results = append(s.Results, root.Comments...) - s.after = root.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// All runs the searcher until it yields no more results. -// The limit is set to 100, just to make the least amount -// of requests possible. It is reset to its original value after. -func (s *UserCommentSearcher) All(ctx context.Context) error { - limit := s.opts.Limit - - s.setLimit(100) - defer s.setLimit(limit) - - var ok = true - var err error - - for ok { - ok, _, err = s.More(ctx) - if err != nil { - return err - } - } - - return nil -} - -// UserCommentPostSearcher finds the comments and posts of a user. -type UserCommentPostSearcher struct { - clientSearcher - username string - where string - after string - Results struct { - Comments []Comment `json:"comments"` - Posts []Link `json:"posts"` - } -} - -func (s *UserCommentPostSearcher) search(ctx context.Context) (*Comments, *Links, *Response, error) { - path := fmt.Sprintf("user/%s/%s", s.username, s.where) - root, resp, err := s.clientSearcher.Do(ctx, path) - if err != nil { - return nil, nil, resp, err - } - return root.getComments(), root.getLinks(), resp, nil -} - -// Search runs the searcher. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *UserCommentPostSearcher) Search(ctx context.Context) (bool, *Response, error) { - rootComments, rootPosts, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results.Comments = rootComments.Comments - s.Results.Posts = rootPosts.Links - s.after = rootComments.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// More runs the searcher again and adds to the results. -// The first return value tells the user if there are -// more results that were cut off (due to the limit). -func (s *UserCommentPostSearcher) More(ctx context.Context) (bool, *Response, error) { - if s.after == "" { - return s.Search(ctx) - } - - s.setAfter(s.after) - - rootComments, rootPosts, resp, err := s.search(ctx) - if err != nil { - return false, resp, err - } - - s.Results.Comments = append(s.Results.Comments, rootComments.Comments...) - s.Results.Posts = append(s.Results.Posts, rootPosts.Links...) - s.after = rootComments.After - - // if the "after" value is non-empty, it - // means there are more results to come. - moreResultsExist := s.after != "" - - return moreResultsExist, resp, nil -} - -// All runs the searcher until it yields no more results. -// The limit is set to 100, just to make the least amount -// of requests possible. It is reset to its original value after. -func (s *UserCommentPostSearcher) All(ctx context.Context) error { - limit := s.opts.Limit - - s.setLimit(100) - defer s.setLimit(limit) - - var ok = true - var err error - - for ok { - ok, _, err = s.More(ctx) - if err != nil { - return err - } - } - - return nil -}