diff --git a/search.go b/search.go index f7df24a..121fec0 100644 --- a/search.go +++ b/search.go @@ -3,7 +3,6 @@ package geddit import ( "context" "fmt" - "net/http" "strings" ) @@ -12,10 +11,12 @@ import ( // IMPORTANT: for searches to include NSFW results, the // user must check the following in their preferences: // "include not safe for work (NSFW) search results in searches" +// Note: The "limit" parameter in searches is prone to inconsistent +// behaviour. type SearchService interface { - Posts(query string) *PostSearchBuilder - Subreddits(query string) *SubredditSearchBuilder - Users(query string) *UserSearchBuilder + Posts(query string, opts ...SearchOpt) *PostSearcher + Subreddits(query string, opts ...SearchOpt) *SubredditSearcher + Users(query string, opts ...SearchOpt) *UserSearcher } // SearchServiceOp implements the VoteService interface @@ -27,115 +28,58 @@ var _ SearchService = &SearchServiceOp{} // Posts searches for posts. // By default, it searches for the most relevant posts of all time. -// To change the sorting, use PostSearchBuilder.Sort(). -// Possible sort options: relevance, hot, top, new, comments. -// To change the timespan, use PostSearchBuilder.Timespan(). -// Possible timespan options: hour, day, week, month, year, all. -func (s *SearchServiceOp) Posts(query string) *PostSearchBuilder { - b := new(PostSearchBuilder) - b.client = s.client - b.opts.Query = query - b.opts.Type = "link" - return b.Sort(SortRelevance).Timespan(TimespanAll) +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 } // Subreddits searches for subreddits. -func (s *SearchServiceOp) Subreddits(query string) *SubredditSearchBuilder { - b := new(SubredditSearchBuilder) - b.client = s.client - b.opts.Query = query - b.opts.Type = "sr" - return b +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) *UserSearchBuilder { - b := new(UserSearchBuilder) - b.client = s.client - b.opts.Query = query - b.opts.Type = "user" - return b +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 } -type searchOpts struct { - Query string `url:"q"` - 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"` -} - -// PostSearchBuilder helps conducts searches that return posts. -type PostSearchBuilder struct { - client *Client +// PostSearcher helps conducts searches that return posts. +type PostSearcher struct { + clientSearcher subreddits []string - opts searchOpts + after string + Results []Link } -// After sets the after option. -func (b *PostSearchBuilder) After(after string) *PostSearchBuilder { - b.opts.After = after - return b -} - -// Before sets the before option. -func (b *PostSearchBuilder) Before(before string) *PostSearchBuilder { - b.opts.Before = before - return b -} - -// Limit sets the limit option. -func (b *PostSearchBuilder) Limit(limit int) *PostSearchBuilder { - b.opts.Limit = limit - return b -} - -// FromSubreddits restricts the search to happen in the specified subreddits only. -func (b *PostSearchBuilder) FromSubreddits(subreddits ...string) *PostSearchBuilder { - b.subreddits = subreddits - b.opts.RestrictSubreddits = len(subreddits) > 0 - return b -} - -// FromAll runs the search against r/all. -func (b *PostSearchBuilder) FromAll() *PostSearchBuilder { - return b.FromSubreddits() -} - -// Sort sets the sort option. -func (b *PostSearchBuilder) Sort(sort Sort) *PostSearchBuilder { - b.opts.Sort = sort.String() - return b -} - -// Timespan sets the timespan option. -func (b *PostSearchBuilder) Timespan(timespan Timespan) *PostSearchBuilder { - b.opts.Timespan = timespan.String() - return b -} - -// Do conducts the search. -func (b *PostSearchBuilder) Do(ctx context.Context) (*Links, *Response, error) { +func (s *PostSearcher) search(ctx context.Context) (*Links, *Response, error) { path := "search" - if len(b.subreddits) > 0 { - path = fmt.Sprintf("r/%s/search", strings.Join(b.subreddits, "+")) + if len(s.subreddits) > 0 { + path = fmt.Sprintf("r/%s/search", strings.Join(s.subreddits, "+")) } - path, err := addOptions(path, b.opts) - if err != nil { - return nil, nil, err - } - - req, err := b.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(rootListing) - resp, err := b.client.Do(ctx, req, root) + root, resp, err := s.clientSearcher.Do(ctx, path) if err != nil { return nil, resp, err } @@ -143,94 +87,232 @@ func (b *PostSearchBuilder) Do(ctx context.Context) (*Links, *Response, error) { return root.getLinks(), resp, nil } -// SubredditSearchBuilder helps conducts searches that return subreddits. -type SubredditSearchBuilder struct { - client *Client - opts searchOpts +// 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 + } + + 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 } -// After sets the after option. -func (b *SubredditSearchBuilder) After(after string) *SubredditSearchBuilder { - b.opts.After = after - return b +// 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 } -// Before sets the before option. -func (b *SubredditSearchBuilder) Before(before string) *SubredditSearchBuilder { - b.opts.Before = before - return b +// 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 } -// Limit sets the limit option. -func (b *SubredditSearchBuilder) Limit(limit int) *SubredditSearchBuilder { - b.opts.Limit = limit - return b +// SubredditSearcher helps conducts searches that return subreddits. +type SubredditSearcher struct { + clientSearcher + after string + Results []Subreddit } -// Do conducts the search. -func (b *SubredditSearchBuilder) Do(ctx context.Context) (*Subreddits, *Response, error) { +func (s *SubredditSearcher) search(ctx context.Context) (*Subreddits, *Response, error) { path := "search" - path, err := addOptions(path, b.opts) - if err != nil { - return nil, nil, err - } - - req, err := b.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(rootListing) - resp, err := b.client.Do(ctx, req, root) + root, resp, err := s.clientSearcher.Do(ctx, path) if err != nil { return nil, resp, err } - return root.getSubreddits(), resp, nil } -// UserSearchBuilder helps conducts searches that return posts. -type UserSearchBuilder struct { - client *Client - opts searchOpts +// 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 + } + + 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 } -// After sets the after option. -func (b *UserSearchBuilder) After(after string) *UserSearchBuilder { - b.opts.After = after - return b +// 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 } -// Before sets the before option. -func (b *UserSearchBuilder) Before(before string) *UserSearchBuilder { - b.opts.Before = before - return b +// 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 } -// Limit sets the limit option. -func (b *UserSearchBuilder) Limit(limit int) *UserSearchBuilder { - b.opts.Limit = limit - return b +// UserSearcher helps conducts searches that return users. +type UserSearcher struct { + clientSearcher + after string + Results []User } -// Do conducts the search. -func (b *UserSearchBuilder) Do(ctx context.Context) (*Users, *Response, error) { +func (s *UserSearcher) search(ctx context.Context) (*Users, *Response, error) { path := "search" - path, err := addOptions(path, b.opts) - if err != nil { - return nil, nil, err - } - - req, err := b.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(rootListing) - resp, err := b.client.Do(ctx, req, root) + root, resp, err := s.clientSearcher.Do(ctx, path) 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 new file mode 100644 index 0000000..6a26b9d --- /dev/null +++ b/searcher.go @@ -0,0 +1,134 @@ +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 7608246..2c93dd3 100644 --- a/user.go +++ b/user.go @@ -55,6 +55,7 @@ type User struct { IsEmployee bool `json:"is_employee"` HasVerifiedEmail bool `json:"has_verified_email"` NSFW bool `json:"over_18"` + IsSuspended bool `json:"is_suspended"` } // UserShort represents a Reddit user, but contains fewer pieces of information