946318c27b
Signed-off-by: Vartan Benohanian <vartanbeno@gmail.com>
600 lines
16 KiB
Go
600 lines
16 KiB
Go
package geddit
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
)
|
|
|
|
// UserService handles communication with the user
|
|
// related methods of the Reddit API
|
|
type UserService interface {
|
|
Get(ctx context.Context, username string) (*User, *Response, error)
|
|
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
|
|
|
|
Posts(opts ...SearchOpt) *UserPostSearcher
|
|
PostsOf(username string, opts ...SearchOpt) *UserPostSearcher
|
|
|
|
Comments(opts ...SearchOpt) *UserCommentSearcher
|
|
CommentsOf(username string, opts ...SearchOpt) *UserCommentSearcher
|
|
|
|
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)
|
|
|
|
Friend(ctx context.Context, username string, note string) (interface{}, *Response, error)
|
|
Unblock(ctx context.Context, username string) (*Response, error)
|
|
Unfriend(ctx context.Context, username string) (*Response, error)
|
|
}
|
|
|
|
// UserServiceOp implements the UserService interface
|
|
type UserServiceOp struct {
|
|
client *Client
|
|
}
|
|
|
|
var _ UserService = &UserServiceOp{}
|
|
|
|
// User represents a Reddit user
|
|
type User struct {
|
|
// is not the full ID, watch out
|
|
ID string `json:"id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Created *Timestamp `json:"created_utc,omitempty"`
|
|
|
|
LinkKarma int `json:"link_karma"`
|
|
CommentKarma int `json:"comment_karma"`
|
|
|
|
IsFriend bool `json:"is_friend"`
|
|
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
|
|
// It is returned from the GET /api/user_data_by_account_ids endpoint
|
|
type UserShort struct {
|
|
Name string `json:"name,omitempty"`
|
|
Created *Timestamp `json:"created_utc,omitempty"`
|
|
|
|
LinkKarma int `json:"link_karma"`
|
|
CommentKarma int `json:"comment_karma"`
|
|
|
|
NSFW bool `json:"profile_over_18"`
|
|
}
|
|
|
|
// 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)
|
|
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
root := new(userRoot)
|
|
resp, err := s.client.Do(ctx, req, root)
|
|
if err != nil {
|
|
return nil, resp, err
|
|
}
|
|
|
|
return root.Data, resp, nil
|
|
}
|
|
|
|
// GetMultipleByID returns multiple users from their full IDs
|
|
// The response body is a map where the keys are the IDs (if they exist), and the value is the user
|
|
func (s *UserServiceOp) GetMultipleByID(ctx context.Context, ids ...string) (map[string]*UserShort, *Response, error) {
|
|
type query struct {
|
|
IDs []string `url:"ids,omitempty,comma"`
|
|
}
|
|
|
|
path := "api/user_data_by_account_ids"
|
|
path, err := addOptions(path, query{ids})
|
|
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(map[string]*UserShort)
|
|
resp, err := s.client.Do(ctx, req, root)
|
|
if err != nil {
|
|
return nil, resp, err
|
|
}
|
|
|
|
return *root, resp, nil
|
|
}
|
|
|
|
// UsernameAvailable checks whether a username is available for registration
|
|
// If a valid username is provided, this endpoint returns a body with just "true" or "false"
|
|
func (s *UserServiceOp) UsernameAvailable(ctx context.Context, username string) (bool, *Response, error) {
|
|
type query struct {
|
|
User string `url:"user,omitempty"`
|
|
}
|
|
|
|
path := "api/username_available"
|
|
path, err := addOptions(path, query{username})
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
root := new(bool)
|
|
resp, err := s.client.Do(ctx, req, root)
|
|
if err != nil {
|
|
return false, resp, err
|
|
}
|
|
|
|
return *root, resp, nil
|
|
}
|
|
|
|
// 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...)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
return sr
|
|
}
|
|
|
|
// Posts returns a list of the client's posts.
|
|
func (s *UserServiceOp) Posts(opts ...SearchOpt) *UserPostSearcher {
|
|
return s.PostsOf(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)
|
|
}
|
|
return sr
|
|
}
|
|
|
|
// Comments returns a list of the client's comments.
|
|
func (s *UserServiceOp) Comments(opts ...SearchOpt) *UserCommentSearcher {
|
|
return s.CommentsOf(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)
|
|
}
|
|
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)
|
|
}
|
|
return sr
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// GetGilded returns a list of the client's gilded comments and links.
|
|
func (s *UserServiceOp) GetGilded(ctx context.Context, opts *ListOptions) (*CommentsLinks, *Response, error) {
|
|
path := fmt.Sprintf("user/%s/gilded", s.client.Username)
|
|
return s.getCommentsAndLinks(ctx, path, opts)
|
|
}
|
|
|
|
// 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 {
|
|
Name string `url:"name"`
|
|
Note string `url:"note"`
|
|
}
|
|
|
|
path := fmt.Sprintf("api/v1/me/friends/%s", username)
|
|
body := request{Name: username, Note: note}
|
|
|
|
_, err := s.client.NewRequest(http.MethodPut, path, body)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
// todo: requires gold
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// Unblock unblocks a user
|
|
func (s *UserServiceOp) Unblock(ctx context.Context, username string) (*Response, error) {
|
|
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")
|
|
|
|
req, err := s.client.NewPostForm(path, form)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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
|
|
}
|