diff --git a/geddit.go b/geddit.go index b7d08fe..5d167d5 100644 --- a/geddit.go +++ b/geddit.go @@ -92,6 +92,7 @@ type Client struct { Flair FlairService Link LinkService Listings ListingsService + Search SearchService Subreddit SubredditService User UserService Vote VoteService @@ -120,6 +121,7 @@ func newClient(httpClient *http.Client) *Client { c.Flair = &FlairServiceOp{client: c} c.Link = &LinkServiceOp{client: c} c.Listings = &ListingsServiceOp{client: c} + c.Search = &SearchServiceOp{client: c} c.Subreddit = &SubredditServiceOp{client: c} c.User = &UserServiceOp{client: c} c.Vote = &VoteServiceOp{client: c} diff --git a/search.go b/search.go new file mode 100644 index 0000000..9aa5116 --- /dev/null +++ b/search.go @@ -0,0 +1,198 @@ +package geddit + +import ( + "context" + "fmt" + "net/http" +) + +// SearchService handles communication with the search +// related methods of the Reddit API +// 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" +type SearchService interface { + SearchUsers(ctx context.Context, q string, opts *ListOptions) (*Users, *Response, error) + + SearchLinksByRelevance(ctx context.Context, q string, opts *ListOptions) (*Links, *Response, error) + SearchLinksByHottest(ctx context.Context, q string, opts *ListOptions) (*Links, *Response, error) + SearchLinksByTop(ctx context.Context, q string, opts *ListOptions) (*Links, *Response, error) + SearchLinksByComments(ctx context.Context, q string, opts *ListOptions) (*Links, *Response, error) + SearchLinksByRelevanceInSubreddit(ctx context.Context, subreddit, q string, opts *ListOptions) (*Links, *Response, error) + SearchLinksByHottestInSubreddit(ctx context.Context, subreddit, q string, opts *ListOptions) (*Links, *Response, error) + SearchLinksByTopInSubreddit(ctx context.Context, subreddit, q string, opts *ListOptions) (*Links, *Response, error) + SearchLinksByCommentsInSubreddit(ctx context.Context, subreddit, q string, opts *ListOptions) (*Links, *Response, error) + + SearchSubreddits(ctx context.Context, q string, opts *ListOptions) (*Subreddits, *Response, error) +} + +// SearchServiceOp implements the VoteService interface +type SearchServiceOp struct { + client *Client +} + +var _ SearchService = &SearchServiceOp{} + +type searchQuery struct { + ListOptions + Query string `url:"q,omitempty"` + Type string `url:"type,omitempty"` + Sort string `url:"sort,omitempty"` +} + +func newSearchQuery(query, _type, sort string, opts *ListOptions) *searchQuery { + if opts == nil { + opts = &ListOptions{} + } + return &searchQuery{ + ListOptions: *opts, + Query: query, + Type: _type, + Sort: sort, + } +} + +// SearchUsers searches for users +func (s *SearchServiceOp) SearchUsers(ctx context.Context, q string, opts *ListOptions) (*Users, *Response, error) { + query := newSearchQuery(q, "user", "", opts) + + root, resp, err := s.search(ctx, "", query) + if err != nil { + return nil, nil, err + } + + return root.getUsers(), resp, nil +} + +// SearchLinksByRelevance searches for link sorted by relevance to the search query +func (s *SearchServiceOp) SearchLinksByRelevance(ctx context.Context, q string, opts *ListOptions) (*Links, *Response, error) { + query := newSearchQuery(q, "link", sorts[sortRelevance], opts) + + root, resp, err := s.search(ctx, "", query) + if err != nil { + return nil, nil, err + } + + return root.getLinks(), resp, nil +} + +// SearchLinksByHottest searches for the hottest links +func (s *SearchServiceOp) SearchLinksByHottest(ctx context.Context, q string, opts *ListOptions) (*Links, *Response, error) { + query := newSearchQuery(q, "link", sorts[sortHot], opts) + + root, resp, err := s.search(ctx, "", query) + if err != nil { + return nil, nil, err + } + + return root.getLinks(), resp, nil +} + +// SearchLinksByTop searches for the top links +func (s *SearchServiceOp) SearchLinksByTop(ctx context.Context, q string, opts *ListOptions) (*Links, *Response, error) { + query := newSearchQuery(q, "link", sorts[sortTop], opts) + + root, resp, err := s.search(ctx, "", query) + if err != nil { + return nil, nil, err + } + + return root.getLinks(), resp, nil +} + +// SearchLinksByComments searches for links with the highest number of comments +func (s *SearchServiceOp) SearchLinksByComments(ctx context.Context, q string, opts *ListOptions) (*Links, *Response, error) { + query := newSearchQuery(q, "link", sorts[sortComments], opts) + + root, resp, err := s.search(ctx, "", query) + if err != nil { + return nil, nil, err + } + + return root.getLinks(), resp, nil +} + +// SearchLinksByRelevanceInSubreddit searches for link sorted by relevance to the search query in the specified subreddit +func (s *SearchServiceOp) SearchLinksByRelevanceInSubreddit(ctx context.Context, subreddit, q string, opts *ListOptions) (*Links, *Response, error) { + query := newSearchQuery(q, "link", sorts[sortRelevance], opts) + + root, resp, err := s.search(ctx, subreddit, query) + if err != nil { + return nil, nil, err + } + + return root.getLinks(), resp, nil +} + +// SearchLinksByHottestInSubreddit searches for the hottest links in the specified subreddit +func (s *SearchServiceOp) SearchLinksByHottestInSubreddit(ctx context.Context, subreddit, q string, opts *ListOptions) (*Links, *Response, error) { + query := newSearchQuery(q, "link", sorts[sortHot], opts) + + root, resp, err := s.search(ctx, subreddit, query) + if err != nil { + return nil, nil, err + } + + return root.getLinks(), resp, nil +} + +// SearchLinksByTopInSubreddit searches for the top links in the specified subreddit +func (s *SearchServiceOp) SearchLinksByTopInSubreddit(ctx context.Context, subreddit, q string, opts *ListOptions) (*Links, *Response, error) { + query := newSearchQuery(q, "link", sorts[sortTop], opts) + + root, resp, err := s.search(ctx, subreddit, query) + if err != nil { + return nil, nil, err + } + + return root.getLinks(), resp, nil +} + +// SearchLinksByCommentsInSubreddit searches for links with the highest number of comments in the specified subreddit +func (s *SearchServiceOp) SearchLinksByCommentsInSubreddit(ctx context.Context, subreddit, q string, opts *ListOptions) (*Links, *Response, error) { + query := newSearchQuery(q, "link", sorts[sortComments], opts) + + root, resp, err := s.search(ctx, subreddit, query) + if err != nil { + return nil, nil, err + } + + return root.getLinks(), resp, nil +} + +// SearchSubreddits searches for subreddits +func (s *SearchServiceOp) SearchSubreddits(ctx context.Context, q string, opts *ListOptions) (*Subreddits, *Response, error) { + query := newSearchQuery(q, "sr", "", opts) + + root, resp, err := s.search(ctx, "", query) + if err != nil { + return nil, resp, err + } + + return root.getSubreddits(), resp, nil +} + +func (s *SearchServiceOp) search(ctx context.Context, subreddit string, opts *searchQuery) (*rootListing, *Response, error) { + path := "search" + if subreddit != "" { + path = fmt.Sprintf("r/%s/search?restrict_sr=true", subreddit) + } + + 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, nil +} diff --git a/subreddit.go b/subreddit.go index 4b0bc6d..04dda58 100644 --- a/subreddit.go +++ b/subreddit.go @@ -111,26 +111,6 @@ func (s *SubredditServiceOp) GetMineWhereStreams(ctx context.Context, opts *List return s.getSubreddits(ctx, "subreddits/mine/contributor", opts) } -type sort int - -const ( - sortHot sort = iota - sortBest - sortNew - sortRising - sortControversial - sortTop -) - -var sorts = [...]string{ - "hot", - "best", - "new", - "rising", - "controversial", - "top", -} - // GetHotLinks returns the hot links // If no subreddit names are provided, then it runs the search against all those the client is subscribed to // IMPORTANT: for subreddits, this will include the stickied posts (if any) diff --git a/things.go b/things.go index 71a12ed..d736403 100644 --- a/things.go +++ b/things.go @@ -14,6 +14,32 @@ const ( kindMode = "more" ) +type sort int + +const ( + sortHot sort = iota + sortBest + sortNew + sortRising + sortControversial + sortTop + sortRelevance + sortComments +) + +var sorts = [...]string{ + "hot", + "best", + "new", + "rising", + "controversial", + "top", + + // for search queries + "relevance", + "comments", +} + type root struct { Kind string `json:"kind,omitempty"` Data interface{} `json:"data,omitempty"` @@ -35,6 +61,7 @@ type Listing struct { // Things are stuff! type Things struct { Comments []Comment `json:"comments,omitempty"` + Users []User `json:"users,omitempty"` Links []Link `json:"links,omitempty"` Subreddits []Subreddit `json:"subreddits,omitempty"` // todo: add the other kinds of things @@ -45,6 +72,11 @@ type commentRoot struct { Data *Comment `json:"data,omitempty"` } +type userRoot struct { + Kind string `json:"kind,omitempty"` + Data *User `json:"data,omitempty"` +} + type linkRoot struct { Kind string `json:"kind,omitempty"` Data *Link `json:"data,omitempty"` @@ -71,6 +103,10 @@ func (l *Things) UnmarshalJSON(b []byte) error { l.Comments = append(l.Comments, *root.Data) } case kindAccount: + root := new(userRoot) + if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { + l.Users = append(l.Users, *root.Data) + } case kindLink: root := new(linkRoot) if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { @@ -157,8 +193,9 @@ type Link struct { // If neither, it will be nil Likes *bool `json:"likes"` - Score int `json:"score"` - NumberOfComments int `json:"num_comments"` + Score int `json:"score"` + UpvoteRatio float32 `json:"upvote_ratio"` + NumberOfComments int `json:"num_comments"` SubredditID string `json:"subreddit_id,omitempty"` SubredditName string `json:"subreddit,omitempty"` @@ -219,6 +256,16 @@ func (rl *rootListing) getComments() *Comments { return v } +func (rl *rootListing) getUsers() *Users { + v := new(Users) + if rl != nil && rl.Data != nil { + v.Users = rl.Data.Things.Users + v.After = rl.Data.After + v.Before = rl.Data.Before + } + return v +} + func (rl *rootListing) getLinks() *Links { v := new(Links) if rl != nil && rl.Data != nil { @@ -246,6 +293,13 @@ type Comments struct { Before string `json:"before"` } +// Users is a list of users +type Users struct { + Users []User `json:"users,omitempty"` + After string `json:"after"` + Before string `json:"before"` +} + // Subreddits is a list of subreddits type Subreddits struct { Subreddits []Subreddit `json:"subreddits,omitempty"` diff --git a/user.go b/user.go index c3e501d..449d5c7 100644 --- a/user.go +++ b/user.go @@ -59,11 +59,6 @@ type UserServiceOp struct { var _ UserService = &UserServiceOp{} -type userRoot struct { - Kind *string `json:"kind,omitempty"` - Data *User `json:"data,omitempty"` -} - // User represents a Reddit user type User struct { // is not the full ID, watch out