From 7922711d51e578563889eb5e7187c3ba1c146d80 Mon Sep 17 00:00:00 2001 From: Vartan Benohanian Date: Sun, 3 May 2020 17:31:35 -0400 Subject: [PATCH] Handle listings better by using custom unmarshaling Signed-off-by: Vartan Benohanian --- comment.go | 55 ------ comment_test.go | 5 +- geddit.go | 192 ++++++++++++++++++- listings.go | 54 +----- private-messages.go | 34 ++++ subreddit.go | 439 +++++++++++++++++++++++++++----------------- timestamp.go | 52 ++++++ timestamp_test.go | 182 ++++++++++++++++++ user.go | 320 +++++++++++++++++++++++++------- 9 files changed, 988 insertions(+), 345 deletions(-) create mode 100644 private-messages.go create mode 100644 timestamp.go create mode 100644 timestamp_test.go diff --git a/comment.go b/comment.go index 31bc03a..d5baf3d 100644 --- a/comment.go +++ b/comment.go @@ -25,61 +25,6 @@ type CommentServiceOp struct { var _ CommentService = &CommentServiceOp{} -type commentRoot struct { - Kind *string `json:"kind,omitempty"` - Data *Comment `json:"data,omitempty"` -} - -type commentRootListing struct { - Kind *string `json:"kind,omitempty"` - Data *struct { - Dist int `json:"dist"` - Roots []commentRoot `json:"children,omitempty"` - After string `json:"after,omitempty"` - Before string `json:"before,omitempty"` - } `json:"data,omitempty"` -} - -// Comment is a comment posted by a user -type Comment struct { - ID string `json:"id,omitempty"` - FullID string `json:"name,omitempty"` - ParentID string `json:"parent_id,omitempty"` - Permalink string `json:"permalink,omitempty"` - - Body string `json:"body,omitempty"` - BodyHTML string `json:"body_html,omitempty"` - Author string `json:"author,omitempty"` - AuthorID string `json:"author_fullname,omitempty"` - AuthorFlairText string `json:"author_flair_text,omitempty"` - - Subreddit string `json:"subreddit,omitempty"` - SubredditNamePrefixed string `json:"subreddit_name_prefixed,omitempty"` - SubredditID string `json:"subreddit_id,omitempty"` - - Score int `json:"score"` - Controversiality int `json:"controversiality"` - - Created float64 `json:"created"` - CreatedUTC float64 `json:"created_utc"` - - LinkID string `json:"link_id,omitempty"` - - // These don't appear when submitting a comment - LinkTitle string `json:"link_title,omitempty"` - LinkPermalink string `json:"link_permalink,omitempty"` - LinkAuthor string `json:"link_author,omitempty"` - LinkNumComments int `json:"num_comments"` - - IsSubmitter bool `json:"is_submitter"` - ScoreHidden bool `json:"score_hidden"` - Saved bool `json:"saved"` - Stickied bool `json:"stickied"` - Locked bool `json:"locked"` - CanGild bool `json:"can_gild"` - NSFW bool `json:"over_18"` -} - // CommentList holds information about a list of comments // The after and before fields help decide the anchor point for a subsequent // call that returns a list diff --git a/comment_test.go b/comment_test.go index 1a20770..5a06043 100644 --- a/comment_test.go +++ b/comment_test.go @@ -6,6 +6,7 @@ import ( "net/url" "reflect" "testing" + "time" ) var expectedCommentSubmitOrEdit = &Comment{ @@ -15,7 +16,6 @@ var expectedCommentSubmitOrEdit = &Comment{ Permalink: "/r/subreddit/comments/test1/some_thread/test2/", Body: "test comment", - BodyHTML: "

test comment

\n
", Author: "reddit_username", AuthorID: "t2_user1", AuthorFlairText: "Flair", @@ -27,8 +27,7 @@ var expectedCommentSubmitOrEdit = &Comment{ Score: 1, Controversiality: 0, - Created: 1588147787, - CreatedUTC: 1588118987, + Created: &Timestamp{time.Date(2020, 4, 28, 20, 9, 47, 0, time.UTC)}, LinkID: "t3_link1", } diff --git a/geddit.go b/geddit.go index 37a9fc6..5f26b45 100644 --- a/geddit.go +++ b/geddit.go @@ -343,7 +343,6 @@ func CheckResponse(r *http.Response) error { // ListOptions are the optional parameters to the various endpoints that return lists type ListOptions struct { - Sort string `url:"sort,omitempty"` Type string `url:"type,omitempty"` // links or comments // For getting submissions @@ -354,7 +353,6 @@ type ListOptions struct { After string `url:"after,omitempty"` Before string `url:"before,omitempty"` Limit int `url:"limit,omitempty"` // default: 25 - Count int `url:"count,omitempty"` // default: 0 } func addOptions(s string, opt interface{}) (string, error) { @@ -388,6 +386,61 @@ type root struct { Data interface{} `json:"data,omitempty"` } +type rootListing struct { + Kind string `json:"kind,omitempty"` + Data *Listing `json:"data"` +} + +// Listing holds things coming from the Reddit API +// It also contains the after/before anchors useful for subsequent requests +type Listing struct { + Things Things `json:"children"` + After string `json:"after"` + Before string `json:"before"` +} + +// Things are stuff! +type Things struct { + Comments []Comment `json:"comments,omitempty"` + Links []Link `json:"links,omitempty"` + Subreddits []Subreddit `json:"subreddits,omitempty"` + // todo: add the other kinds of things +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (l *Things) UnmarshalJSON(b []byte) error { + var children []map[string]interface{} + if err := json.Unmarshal(b, &children); err != nil { + return err + } + + for _, child := range children { + byteValue, _ := json.Marshal(child) + switch child["kind"] { + case kindComment: + root := new(commentRoot) + if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { + l.Comments = append(l.Comments, *root.Data) + } + case kindAccount: + case kindLink: + root := new(linkRoot) + if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { + l.Links = append(l.Links, *root.Data) + } + case kindMessage: + case kindSubreddit: + root := new(subredditRoot) + if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { + l.Subreddits = append(l.Subreddits, *root.Data) + } + case kindAward: + } + } + + return nil +} + const ( kindListing string = "Listing" kindComment string = "t1" @@ -397,3 +450,138 @@ const ( kindSubreddit string = "t5" kindAward string = "t6" ) + +type commentRoot struct { + Kind string `json:"kind,omitempty"` + Data *Comment `json:"data,omitempty"` +} + +type linkRoot struct { + Kind string `json:"kind,omitempty"` + Data *Link `json:"data,omitempty"` +} + +type subredditRoot struct { + Kind string `json:"kind,omitempty"` + Data *Subreddit `json:"data,omitempty"` +} + +// Comment is a comment posted by a user +type Comment struct { + ID string `json:"id,omitempty"` + FullID string `json:"name,omitempty"` + ParentID string `json:"parent_id,omitempty"` + Permalink string `json:"permalink,omitempty"` + + Body string `json:"body,omitempty"` + Author string `json:"author,omitempty"` + AuthorID string `json:"author_fullname,omitempty"` + AuthorFlairText string `json:"author_flair_text,omitempty"` + AuthorFlairID string `json:"author_flair_template_id,omitempty"` + + Subreddit string `json:"subreddit,omitempty"` + SubredditNamePrefixed string `json:"subreddit_name_prefixed,omitempty"` + SubredditID string `json:"subreddit_id,omitempty"` + + Score int `json:"score"` + Controversiality int `json:"controversiality"` + + Created *Timestamp `json:"created_utc,omitempty"` + Edited *Timestamp `json:"edited,omitempty"` + + LinkID string `json:"link_id,omitempty"` + + // These don't appear when submitting a comment + LinkTitle string `json:"link_title,omitempty"` + LinkPermalink string `json:"link_permalink,omitempty"` + LinkAuthor string `json:"link_author,omitempty"` + LinkNumComments int `json:"num_comments"` + + IsSubmitter bool `json:"is_submitter"` + ScoreHidden bool `json:"score_hidden"` + Saved bool `json:"saved"` + Stickied bool `json:"stickied"` + Locked bool `json:"locked"` + CanGild bool `json:"can_gild"` + NSFW bool `json:"over_18"` + + // If a comment has no replies, its "replies" value is "", + // which the unmarshaler doesn't like + // So we capture this varying field in RepliesRaw, and then + // fill it in Replies + RepliesRaw json.RawMessage `json:"replies,omitempty"` + Replies []commentRoot `json:"-"` +} + +// Link is a submitted post on Reddit +type Link struct { + ID string `json:"id,omitempty"` + FullID string `json:"name,omitempty"` + Created *Timestamp `json:"created_utc,omitempty"` + Edited *Timestamp `json:"edited,omitempty"` + + Permalink string `json:"permalink,omitempty"` + URL string `json:"url,omitempty"` + + Title string `json:"title,omitempty"` + Body string `json:"selftext,omitempty"` + + Score int `json:"score"` + NumberOfComments int `json:"num_comments"` + + SubredditID string `json:"subreddit_id,omitempty"` + SubredditName string `json:"subreddit,omitempty"` + SubredditNamePrefixed string `json:"subreddit_name_prefixed,omitempty"` + + AuthorID string `json:"author_fullname,omitempty"` + AuthorName string `json:"author,omitempty"` + + Spoiler bool `json:"spoiler"` + Locked bool `json:"locked"` + NSFW bool `json:"over_18"` + IsSelfPost bool `json:"is_self"` + Saved bool `json:"saved"` + Stickied bool `json:"stickied"` +} + +// Subreddit holds information about a subreddit +type Subreddit struct { + ID string `json:"id,omitempty"` + FullID string `json:"name,omitempty"` + Created *Timestamp `json:"created_utc,omitempty"` + + URL string `json:"url,omitempty"` + Name string `json:"display_name,omitempty"` + NamePrefixed string `json:"display_name_prefixed,omitempty"` + Title string `json:"title,omitempty"` + PublicDescription string `json:"public_description,omitempty"` + Type string `json:"subreddit_type,omitempty"` + SuggestedCommentSort string `json:"suggested_comment_sort,omitempty"` + + Subscribers int `json:"subscribers"` + ActiveUserCount *int `json:"active_user_count,omitempty"` + NSFW bool `json:"over18"` + UserIsMod bool `json:"user_is_moderator"` +} + +func (rl *rootListing) getLinks() *LinkList { + if rl == nil || rl.Data == nil { + return nil + } + return &LinkList{ + Links: rl.Data.Things.Links, + After: rl.Data.After, + Before: rl.Data.Before, + } +} + +func (rl *rootListing) getComments() *CommentList { + if rl == nil || rl.Data == nil { + return nil + } + return &CommentList{ + Comments: rl.Data.Things.Comments, + After: rl.Data.After, + Before: rl.Data.Before, + } +} diff --git a/listings.go b/listings.go index 76c6ccb..28a00f5 100644 --- a/listings.go +++ b/listings.go @@ -2,7 +2,6 @@ package geddit import ( "context" - "encoding/json" "net/http" ) @@ -30,14 +29,15 @@ type listingRoot struct { } // Listing holds various types of things that all come from the Reddit API -type Listing struct { - Links []*Submission `json:"links,omitempty"` - Comments []*Comment `json:"comments,omitempty"` - Subreddits []*Subreddit `json:"subreddits,omitempty"` -} +// type Listing struct { +// Links []*Submission `json:"links,omitempty"` +// Comments []*Comment `json:"comments,omitempty"` +// Subreddits []*Subreddit `json:"subreddits,omitempty"` +// } // Get gets a list of things based on their IDs // Only links, comments, and subreddits are allowed +// todo: only links, comments, subreddits func (s *ListingsServiceOp) Get(ctx context.Context, ids ...string) (*Listing, *Response, error) { type query struct { IDs []string `url:"id,comma"` @@ -54,51 +54,13 @@ func (s *ListingsServiceOp) Get(ctx context.Context, ids ...string) (*Listing, * return nil, nil, err } - root := new(listingRoot) + root := new(rootListing) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } - if root.Data == nil { - return nil, resp, nil - } - - l := new(Listing) - - for _, result := range root.Data.Children { - kind, ok1 := result["kind"].(string) - data, ok2 := result["data"] - - if ok1 && ok2 { - byteValue, err := json.Marshal(data) - if err != nil { - return nil, resp, err - } - - var v interface{} - switch kind { - case kindComment: - v = new(Comment) - l.Comments = append(l.Comments, v.(*Comment)) - case kindLink: - v = new(Submission) - l.Links = append(l.Links, v.(*Submission)) - case kindSubreddit: - v = new(Subreddit) - l.Subreddits = append(l.Subreddits, v.(*Subreddit)) - default: - continue - } - - err = json.Unmarshal(byteValue, v) - if err != nil { - return nil, resp, err - } - } - } - - return l, resp, nil + return root.Data, resp, nil } // todo: do by_id next diff --git a/private-messages.go b/private-messages.go new file mode 100644 index 0000000..be4d752 --- /dev/null +++ b/private-messages.go @@ -0,0 +1,34 @@ +package geddit + +import ( + "context" + "net/url" +) + +// PrivateMessageService handles communication with the private message +// related methods of the Reddit API +type PrivateMessageService interface { + BlockUser(ctx context.Context, messageID string) (*Response, error) +} + +// PrivateMessageServiceOp implements the PrivateMessageService interface +type PrivateMessageServiceOp struct { + client *Client +} + +var _ PrivateMessageService = &PrivateMessageServiceOp{} + +// BlockUser blocks a user based on the ID of the private message +func (s *PrivateMessageServiceOp) BlockUser(ctx context.Context, messageID string) (*Response, error) { + path := "api/block" + + form := url.Values{} + form.Set("id", messageID) + + req, err := s.client.NewPostForm(path, form) + if err != nil { + return nil, nil + } + + return s.client.Do(ctx, req, nil) +} diff --git a/subreddit.go b/subreddit.go index 0d1b286..668074a 100644 --- a/subreddit.go +++ b/subreddit.go @@ -5,7 +5,9 @@ import ( "errors" "fmt" "net/http" + "net/url" "strings" + "time" ) // SubredditService handles communication with the subreddit @@ -23,14 +25,22 @@ type SubredditService interface { GetMineWhereModerator(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) GetMineWhereStreams(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) - GetHotPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) - GetNewPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) - GetRisingPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) - GetControversialPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) - GetTopPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) + GetHotLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) + GetBestLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) + GetNewLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) + GetRisingLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) + GetControversialLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) + GetTopLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) - GetSticky1(ctx context.Context, name string) (interface{}, *Response, error) - GetSticky2(ctx context.Context, name string) (interface{}, *Response, error) + // GetSticky1(ctx context.Context, name string) (interface{}, *Response, error) + // GetSticky2(ctx context.Context, name string) (interface{}, *Response, error) + + Subscribe(ctx context.Context, names ...string) (*Response, error) + SubscribeByID(ctx context.Context, ids ...string) (*Response, error) + Unsubscribe(ctx context.Context, names ...string) (*Response, error) + UnsubscribeByID(ctx context.Context, ids ...string) (*Response, error) + + StreamLinks(ctx context.Context, names ...string) (<-chan Link, chan<- bool, error) } // SubredditServiceOp implements the SubredditService interface @@ -40,42 +50,6 @@ type SubredditServiceOp struct { var _ SubredditService = &SubredditServiceOp{} -type subredditRoot struct { - Kind *string `json:"kind,omitempty"` - Data *Subreddit `json:"data,omitempty"` -} - -type subredditRootListing struct { - Kind *string `json:"kind,omitempty"` - Data *struct { - Dist int `json:"dist"` - Roots []subredditRoot `json:"children,omitempty"` - After string `json:"after,omitempty"` - Before string `json:"before,omitempty"` - } `json:"data,omitempty"` -} - -// Subreddit holds information about a subreddit -type Subreddit struct { - ID string `json:"id,omitempty"` - FullID string `json:"name,omitempty"` - Created float64 `json:"created"` - CreatedUTC float64 `json:"created_utc"` - - URL string `json:"url,omitempty"` - DisplayName string `json:"display_name,omitempty"` - DisplayNamePrefixed string `json:"display_name_prefixed,omitempty"` - Title string `json:"title,omitempty"` - PublicDescription string `json:"public_description,omitempty"` - Type string `json:"subreddit_type,omitempty"` - SuggestedCommentSort string `json:"suggested_comment_sort,omitempty"` - - Subscribers int `json:"subscribers"` - ActiveUserCount *int `json:"active_user_count,omitempty"` - NSFW bool `json:"over18"` - UserIsMod bool `json:"user_is_moderator"` -} - // SubredditList holds information about a list of subreddits // The after and before fields help decide the anchor point for a subsequent // call that returns a list @@ -85,58 +59,14 @@ type SubredditList struct { Before string `json:"before,omitempty"` } -type submissionRoot struct { - Kind *string `json:"kind,omitempty"` - Data *Submission `json:"data,omitempty"` -} - -type submissionRootListing struct { - Kind *string `json:"kind,omitempty"` - Data *struct { - Dist int `json:"dist"` - Roots []submissionRoot `json:"children,omitempty"` - After string `json:"after,omitempty"` - Before string `json:"before,omitempty"` - } `json:"data,omitempty"` -} - -// Submission is a submitted post on Reddit -type Submission struct { - ID string `json:"id,omitempty"` - FullID string `json:"name,omitempty"` - Created float64 `json:"created"` - CreatedUTC float64 `json:"created_utc"` - - Permalink string `json:"permalink,omitempty"` - URL string `json:"url,omitempty"` - - Title string `json:"title,omitempty"` - Body string `json:"selftext,omitempty"` - Score int `json:"score"` - NumberOfComments int `json:"num_comments"` - - SubredditID string `json:"t5_2qo4s,omitempty"` - SubredditName string `json:"subreddit,omitempty"` - SubredditNamePrefixed string `json:"subreddit_name_prefixed,omitempty"` - - AuthorID string `json:"author_fullname,omitempty"` - AuthorName string `json:"author,omitempty"` - - Spoiler bool `json:"spoiler"` - Locked bool `json:"locked"` - NSFW bool `json:"over_18"` - IsSelfPost bool `json:"is_self"` - Saved bool `json:"saved"` - Stickied bool `json:"stickied"` -} - -// SubmissionList holds information about a list of subreddits +// LinkList holds information about a list of links // The after and before fields help decide the anchor point for a subsequent // call that returns a list -type SubmissionList struct { - Submissions []Submission `json:"submissions,omitempty"` - After string `json:"after,omitempty"` - Before string `json:"before,omitempty"` +// Note: not to be confused with linked lists +type LinkList struct { + Links []Link `json:"submissions,omitempty"` + After string `json:"after,omitempty"` + Before string `json:"before,omitempty"` } // GetByName gets a subreddit by name @@ -220,44 +150,44 @@ var sorts = [...]string{ "top", } -// GetHotPosts returns the hot posts -// If no subreddit names are provided, then it runs the search against /r/all +// 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) // PLUS the number of posts from the limit parameter (which is 25 by default) -func (s *SubredditServiceOp) GetHotPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { - return s.getPosts(ctx, sortHot, opts, names...) +func (s *SubredditServiceOp) GetHotLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) { + return s.getLinks(ctx, sortHot, opts, names...) } -// GetBestPosts returns the best posts -// If no subreddit names are provided, then it runs the search against /r/all +// GetBestLinks returns the best 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) // PLUS the number of posts from the limit parameter (which is 25 by default) -func (s *SubredditServiceOp) GetBestPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { - return s.getPosts(ctx, sortBest, opts, names...) +func (s *SubredditServiceOp) GetBestLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) { + return s.getLinks(ctx, sortBest, opts, names...) } -// GetNewPosts returns the new posts -// If no subreddit names are provided, then it runs the search against /r/all -func (s *SubredditServiceOp) GetNewPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { - return s.getPosts(ctx, sortNew, opts, names...) +// GetNewLinks returns the new links +// If no subreddit names are provided, then it runs the search against all those the client is subscribed to +func (s *SubredditServiceOp) GetNewLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) { + return s.getLinks(ctx, sortNew, opts, names...) } -// GetRisingPosts returns the rising posts -// If no subreddit names are provided, then it runs the search against /r/all -func (s *SubredditServiceOp) GetRisingPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { - return s.getPosts(ctx, sortRising, opts, names...) +// GetRisingLinks returns the rising links +// If no subreddit names are provided, then it runs the search against all those the client is subscribed to +func (s *SubredditServiceOp) GetRisingLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) { + return s.getLinks(ctx, sortRising, opts, names...) } -// GetControversialPosts returns the controversial posts -// If no subreddit names are provided, then it runs the search against /r/all -func (s *SubredditServiceOp) GetControversialPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { - return s.getPosts(ctx, sortControversial, opts, names...) +// GetControversialLinks returns the controversial links +// If no subreddit names are provided, then it runs the search against all those the client is subscribed to +func (s *SubredditServiceOp) GetControversialLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) { + return s.getLinks(ctx, sortControversial, opts, names...) } -// GetTopPosts returns the top posts -// If no subreddit names are provided, then it runs the search against /r/all -func (s *SubredditServiceOp) GetTopPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { - return s.getPosts(ctx, sortTop, opts, names...) +// GetTopLinks returns the top links +// If no subreddit names are provided, then it runs the search against all those the client is subscribed to +func (s *SubredditServiceOp) GetTopLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) { + return s.getLinks(ctx, sortTop, opts, names...) } type sticky int @@ -267,14 +197,65 @@ const ( sticky2 ) -// GetSticky1 returns the first stickied post on a subreddit (if it exists) -func (s *SubredditServiceOp) GetSticky1(ctx context.Context, name string) (interface{}, *Response, error) { - return s.getSticky(ctx, name, sticky1) +// // GetSticky1 returns the first stickied post on a subreddit (if it exists) +// func (s *SubredditServiceOp) GetSticky1(ctx context.Context, name string) (interface{}, *Response, error) { +// return s.getSticky(ctx, name, sticky1) +// } + +// // GetSticky2 returns the second stickied post on a subreddit (if it exists) +// func (s *SubredditServiceOp) GetSticky2(ctx context.Context, name string) (interface{}, *Response, error) { +// return s.getSticky(ctx, name, sticky2) +// } + +// Subscribe subscribes to subreddits based on their name +// Returns {} on success +func (s *SubredditServiceOp) Subscribe(ctx context.Context, names ...string) (*Response, error) { + form := url.Values{} + form.Set("action", "sub") + form.Set("sr_name", strings.Join(names, ",")) + return s.handleSubscription(ctx, form) } -// GetSticky2 returns the second stickied post on a subreddit (if it exists) -func (s *SubredditServiceOp) GetSticky2(ctx context.Context, name string) (interface{}, *Response, error) { - return s.getSticky(ctx, name, sticky2) +// SubscribeByID subscribes to subreddits based on their id +// Returns {} on success +func (s *SubredditServiceOp) SubscribeByID(ctx context.Context, ids ...string) (*Response, error) { + form := url.Values{} + form.Set("action", "sub") + form.Set("sr", strings.Join(ids, ",")) + return s.handleSubscription(ctx, form) +} + +// Unsubscribe unsubscribes from subreddits +// Returns {} on success +func (s *SubredditServiceOp) Unsubscribe(ctx context.Context, names ...string) (*Response, error) { + form := url.Values{} + form.Set("action", "unsub") + form.Set("sr_name", strings.Join(names, ",")) + return s.handleSubscription(ctx, form) +} + +// UnsubscribeByID unsubscribes from subreddits based on their id +// Returns {} on success +func (s *SubredditServiceOp) UnsubscribeByID(ctx context.Context, ids ...string) (*Response, error) { + form := url.Values{} + form.Set("action", "unsub") + form.Set("sr", strings.Join(ids, ",")) + return s.handleSubscription(ctx, form) +} + +func (s *SubredditServiceOp) handleSubscription(ctx context.Context, form url.Values) (*Response, error) { + path := "api/subscribe" + req, err := s.client.NewPostForm(path, form) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil } func (s *SubredditServiceOp) getSubreddits(ctx context.Context, path string, opts *ListOptions) (*SubredditList, *Response, error) { @@ -288,30 +269,24 @@ func (s *SubredditServiceOp) getSubreddits(ctx context.Context, path string, opt return nil, nil, err } - root := new(subredditRootListing) + root := new(rootListing) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } - if root.Data == nil { - return nil, resp, nil + l := new(SubredditList) + + if root.Data != nil { + l.Subreddits = root.Data.Things.Subreddits + l.After = root.Data.After + l.Before = root.Data.Before } - sl := new(SubredditList) - var subreddits []Subreddit - - for _, child := range root.Data.Roots { - subreddits = append(subreddits, *child.Data) - } - sl.Subreddits = subreddits - sl.After = root.Data.After - sl.Before = root.Data.Before - - return sl, resp, nil + return l, resp, nil } -func (s *SubredditServiceOp) getPosts(ctx context.Context, sort sort, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { +func (s *SubredditServiceOp) getLinks(ctx context.Context, sort sort, opts *ListOptions, names ...string) (*LinkList, *Response, error) { path := sorts[sort] if len(names) > 0 { path = fmt.Sprintf("r/%s/%s", strings.Join(names, "+"), sorts[sort]) @@ -327,27 +302,21 @@ func (s *SubredditServiceOp) getPosts(ctx context.Context, sort sort, opts *List return nil, nil, err } - root := new(submissionRootListing) + root := new(rootListing) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } - if root.Data == nil { - return nil, resp, nil + l := new(LinkList) + + if root.Data != nil { + l.Links = root.Data.Things.Links + l.After = root.Data.After + l.Before = root.Data.Before } - sl := new(SubmissionList) - var submissions []Submission - - for _, child := range root.Data.Roots { - submissions = append(submissions, *child.Data) - } - sl.Submissions = submissions - sl.After = root.Data.After - sl.Before = root.Data.Before - - return sl, resp, nil + return l, resp, nil } // getSticky returns one of the 2 stickied posts of the subreddit @@ -355,28 +324,154 @@ func (s *SubredditServiceOp) getPosts(ctx context.Context, sort sort, opts *List // If it's <= 1, it's 1 // If it's >= 2, it's 2 // todo -func (s *SubredditServiceOp) getSticky(ctx context.Context, name string, num sticky) (interface{}, *Response, error) { - // type query struct { - // Num sticky `url:"num"` - // } +// func (s *SubredditServiceOp) getSticky(ctx context.Context, name string, num sticky) (interface{}, *Response, error) { +// type query struct { +// Num sticky `url:"num"` +// } - // path := fmt.Sprintf("r/%s/about/sticky", name) - // path, err := addOptions(path, query{num}) - // if err != nil { - // return nil, nil, err - // } +// path := fmt.Sprintf("r/%s/about/sticky", name) +// path, err := addOptions(path, query{num}) +// if err != nil { +// return nil, nil, err +// } - // req, err := s.client.NewRequest(http.MethodGet, path, nil) - // 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(submissionRootListing) - // resp, err := s.client.Do(ctx, req, root) - // if err != nil { - // return nil, resp, err - // } +// var root []rootListing +// resp, err := s.client.Do(ctx, req, &root) +// if err != nil { +// return nil, resp, err +// } - // return nil, resp, nil - return nil, nil, nil +// // test, _ := json.MarshalIndent(root, "", " ") +// // fmt.Println(string(test)) + +// linkRoot := new(linkRoot) + +// link := root[0].Data.Children[0] +// byteValue, err := json.Marshal(link) +// if err != nil { +// return nil, resp, err +// } + +// err = json.Unmarshal(byteValue, linkRoot) +// if err != nil { +// return nil, resp, err +// } + +// // these are all the comments in the post +// comments := root[1].Data.Children + +// var commentsRoot []commentRoot +// byteValue, err = json.Marshal(comments) +// if err != nil { +// return nil, resp, err +// } + +// err = json.Unmarshal(byteValue, &commentsRoot) +// if err != nil { +// return nil, resp, err +// } + +// test, _ := json.MarshalIndent(commentsRoot, "", " ") +// fmt.Println(string(test)) + +// for _, comment := range commentsRoot { +// if string(comment.Data.RepliesRaw) == `""` { +// comment.Data.Replies = nil +// continue +// } + +// // var +// } + +// return commentsRoot, resp, nil +// } + +// func handleComments(comments []commentRoot) { +// for _, comment := range comments { +// if string(comment.Data.RepliesRaw) == `""` { +// comment.Data.Replies = nil +// continue +// } + +// } +// } + +// StreamLinks returns a channel that receives new submissions from the subreddits +// To stop the stream, simply send a bool value to the stop channel +func (s *SubredditServiceOp) StreamLinks(ctx context.Context, names ...string) (<-chan Link, chan<- bool, error) { + if len(names) == 0 { + return nil, nil, errors.New("must specify at least one subreddit") + } + + submissionCh := make(chan Link) + stop := make(chan bool, 1) + + go func() { + // todo: if the post with the before gets deleted, you keep getting 0 posts + var last *Timestamp + for { + select { + case <-stop: + close(submissionCh) + return + default: + sl, _, err := s.GetNewLinks(ctx, nil, names...) + if err != nil { + continue + } + + var newest *Timestamp + for i, submission := range sl.Links { + if i == 0 { + newest = submission.Created + } + if last == nil { + submissionCh <- submission + continue + } + if last.Before(*submission.Created) { + submissionCh <- submission + } + } + last = newest + } + <-time.After(time.Second * 3) + fmt.Println() + } + }() + + // go func() { + // var before string + // for { + // select { + // case <-stop: + // close(submissionCh) + // return + // default: + // sl, _, err := s.GetSubmissions(ctx, SortNew, &ListOptions{Before: before}, names...) + // if err != nil { + // continue + // } + // fmt.Printf("Received %d posts\n", len(sl.Submissions)) + + // if len(sl.Submissions) == 0 { + // continue + // } + + // for _, submission := range sl.Submissions { + // submissionCh <- submission + // } + // before = sl.Submissions[0].FullID + // } + // <-time.After(time.Second * 5) + // fmt.Println() + // } + // }() + + return submissionCh, stop, nil } diff --git a/timestamp.go b/timestamp.go new file mode 100644 index 0000000..4e42308 --- /dev/null +++ b/timestamp.go @@ -0,0 +1,52 @@ +package geddit + +import ( + "strconv" + "time" +) + +// Timestamp represents a time that can be unmarshalled from a JSON string +// formatted as either an RFC3339 or Unix timestamp. +type Timestamp struct { + time.Time +} + +// MarshalJSON implements the json.Marshaler interface. +func (t *Timestamp) MarshalJSON() ([]byte, error) { + if t == nil || t.Time.IsZero() { + return []byte(`false`), nil + } + + parsed := t.Time.Format(time.RFC3339) + return []byte(`"` + parsed + `"`), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// Time is expected in RFC3339 or Unix format. +func (t *Timestamp) UnmarshalJSON(data []byte) (err error) { + str := string(data) + + // "edited" for comments and links is either false, or a timestamp + if str == "false" { + return + } + + f, err := strconv.ParseFloat(str, 64) + if err == nil { + t.Time = time.Unix(int64(f), 0).UTC() + } else { + t.Time, err = time.Parse(`"`+time.RFC3339+`"`, str) + } + + return +} + +// Equal reports whether t and u are equal based on time.Equal +func (t Timestamp) Equal(u Timestamp) bool { + return t.Time.Equal(u.Time) +} + +// Before reports whether u is before t based on time.Before +func (t Timestamp) Before(u Timestamp) bool { + return t.Time.Before(u.Time) +} diff --git a/timestamp_test.go b/timestamp_test.go new file mode 100644 index 0000000..e4458c1 --- /dev/null +++ b/timestamp_test.go @@ -0,0 +1,182 @@ +package geddit + +import ( + "encoding/json" + "fmt" + "testing" + "time" +) + +const ( + emptyTimeStr = `"0001-01-01T00:00:00Z"` + referenceTimeStr = `"2006-01-02T15:04:05Z"` + referenceUnixTimeStr = `1136214245` +) + +var ( + referenceTime = time.Date(2006, time.January, 02, 15, 04, 05, 0, time.UTC) + unixOrigin = time.Unix(0, 0).In(time.UTC) +) + +func TestTimestamp_Marshal(t *testing.T) { + testCases := []struct { + desc string + data Timestamp + want string + wantErr bool + equal bool + }{ + {"Reference", Timestamp{referenceTime}, referenceTimeStr, false, true}, + {"Empty", Timestamp{}, emptyTimeStr, false, true}, + {"Mismatch", Timestamp{}, referenceTimeStr, false, false}, + } + for _, tc := range testCases { + out, err := json.Marshal(tc.data) + if gotErr := err != nil; gotErr != tc.wantErr { + t.Fatalf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err) + } + got := string(out) + equal := got == tc.want + if (got == tc.want) != tc.equal { + t.Fatalf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal) + } + } +} + +func TestTimestamp_Unmarshal(t *testing.T) { + testCases := []struct { + desc string + data string + want Timestamp + wantErr bool + equal bool + }{ + {"Reference", referenceTimeStr, Timestamp{referenceTime}, false, true}, + {"ReferenceUnix", referenceUnixTimeStr, Timestamp{referenceTime}, false, true}, + {"Empty", emptyTimeStr, Timestamp{}, false, true}, + {"UnixStart", `0`, Timestamp{unixOrigin}, false, true}, + {"Mismatch", referenceTimeStr, Timestamp{}, false, false}, + {"MismatchUnix", `0`, Timestamp{}, false, false}, + {"Invalid", `"asdf"`, Timestamp{referenceTime}, true, false}, + } + for _, tc := range testCases { + var got Timestamp + err := json.Unmarshal([]byte(tc.data), &got) + if gotErr := err != nil; gotErr != tc.wantErr { + t.Fatalf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err) + continue + } + equal := got.Equal(tc.want) + if equal != tc.equal { + t.Fatalf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal) + } + } +} + +func TestTimstamp_MarshalReflexivity(t *testing.T) { + testCases := []struct { + desc string + data Timestamp + }{ + {"Reference", Timestamp{referenceTime}}, + {"Empty", Timestamp{}}, + } + for _, tc := range testCases { + data, err := json.Marshal(tc.data) + if err != nil { + t.Fatalf("%s: Marshal err=%v", tc.desc, err) + } + var got Timestamp + err = json.Unmarshal(data, &got) + if err != nil { + t.Fatalf("%s: Unmarshal err=%v", tc.desc, err) + } + if !got.Equal(tc.data) { + t.Fatalf("%s: %+v != %+v", tc.desc, got, data) + } + } +} + +type WrappedTimestamp struct { + A int + Time Timestamp +} + +func TestWrappedTimstamp_Marshal(t *testing.T) { + testCases := []struct { + desc string + data WrappedTimestamp + want string + wantErr bool + equal bool + }{ + {"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, true}, + {"Empty", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, emptyTimeStr), false, true}, + {"Mismatch", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, false}, + } + for _, tc := range testCases { + out, err := json.Marshal(tc.data) + if gotErr := err != nil; gotErr != tc.wantErr { + t.Fatalf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err) + } + got := string(out) + equal := got == tc.want + if equal != tc.equal { + t.Fatalf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal) + } + } +} + +func TestWrappedTimstamp_Unmarshal(t *testing.T) { + testCases := []struct { + desc string + data string + want WrappedTimestamp + wantErr bool + equal bool + }{ + {"Reference", referenceTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true}, + {"ReferenceUnix", referenceUnixTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true}, + {"Empty", emptyTimeStr, WrappedTimestamp{0, Timestamp{}}, false, true}, + {"UnixStart", `0`, WrappedTimestamp{0, Timestamp{unixOrigin}}, false, true}, + {"Mismatch", referenceTimeStr, WrappedTimestamp{0, Timestamp{}}, false, false}, + {"MismatchUnix", `0`, WrappedTimestamp{0, Timestamp{}}, false, false}, + {"Invalid", `"asdf"`, WrappedTimestamp{0, Timestamp{referenceTime}}, true, false}, + } + for _, tc := range testCases { + var got Timestamp + err := json.Unmarshal([]byte(tc.data), &got) + if gotErr := err != nil; gotErr != tc.wantErr { + t.Fatalf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err) + continue + } + equal := got.Time.Equal(tc.want.Time.Time) + if equal != tc.equal { + t.Fatalf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal) + } + } +} + +func TestWrappedTimstamp_MarshalReflexivity(t *testing.T) { + testCases := []struct { + desc string + data WrappedTimestamp + }{ + {"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}}, + {"Empty", WrappedTimestamp{0, Timestamp{}}}, + } + for _, tc := range testCases { + bytes, err := json.Marshal(tc.data) + if err != nil { + t.Fatalf("%s: Marshal err=%v", tc.desc, err) + } + var got WrappedTimestamp + err = json.Unmarshal(bytes, &got) + if err != nil { + t.Fatalf("%s: Unmarshal err=%v", tc.desc, err) + } + if !got.Time.Equal(tc.data.Time) { + t.Fatalf("%s: %+v != %+v", tc.desc, got, tc.data) + } + } +} diff --git a/user.go b/user.go index b2bd3aa..6516d18 100644 --- a/user.go +++ b/user.go @@ -2,9 +2,9 @@ package geddit import ( "context" - "encoding/json" "fmt" "net/http" + "net/url" ) // UserService handles communication with the user @@ -14,8 +14,36 @@ type UserService interface { GetMultipleByID(ctx context.Context, ids ...string) (map[string]*UserShort, *Response, error) UsernameAvailable(ctx context.Context, username string) (bool, *Response, error) - GetComments(ctx context.Context, sort sort, opts *ListOptions) (*CommentList, *Response, error) - GetCommentsOf(ctx context.Context, username string, sort sort, opts *ListOptions) (*CommentList, *Response, error) + // returns the client's links + GetHotLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) + GetNewLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) + GetTopLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) + GetControversialLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) + + // returns the links of the user with the username + GetHotLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) + GetNewLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) + GetTopLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) + GetControversialLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) + + GetUpvoted(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) + GetDownvoted(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) + GetHidden(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) + + // returns the client's comments + GetHotComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) + GetNewComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) + GetTopComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) + GetControversialComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) + + // returns the comments of the user with the username + GetHotCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) + GetNewCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) + GetTopCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) + GetControversialCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) + + Unblock(ctx context.Context, username string) (*Response, error) + Unfriend(ctx context.Context, username string) (*Response, error) } // UserServiceOp implements the UserService interface @@ -32,10 +60,10 @@ type userRoot struct { // User represents a Reddit user type User struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Created float64 `json:"created"` - CreatedUTC float64 `json:"created_utc"` + // 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"` @@ -49,8 +77,8 @@ type User struct { // 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"` - CreatedUTC float64 `json:"created_utc"` + Name string `json:"name,omitempty"` + Created *Timestamp `json:"created_utc,omitempty"` LinkKarma int `json:"link_karma"` CommentKarma int `json:"comment_karma"` @@ -102,63 +130,8 @@ func (s *UserServiceOp) GetMultipleByID(ctx context.Context, ids ...string) (map return *root, resp, nil } -// GetComments returns a list of the client's comments -func (s *UserServiceOp) GetComments(ctx context.Context, sort sort, opts *ListOptions) (*CommentList, *Response, error) { - return s.GetCommentsOf(ctx, s.client.Username, sort, opts) -} - -// GetCommentsOf returns a list of the user's comments -func (s *UserServiceOp) GetCommentsOf(ctx context.Context, username string, sort sort, opts *ListOptions) (*CommentList, *Response, error) { - path := fmt.Sprintf("user/%s/comments?sort=%s", username, sorts[sort]) - 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(commentRootListing) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - if root.Data == nil { - return nil, resp, nil - } - - cl := new(CommentList) - var comments []Comment - - for _, child := range root.Data.Roots { - comments = append(comments, *child.Data) - } - cl.Comments = comments - cl.After = root.Data.After - cl.Before = root.Data.Before - - return cl, 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" -// If an invalid username is provided, it returns the following: -/* -{ - "json": { - "errors": [ - [ - "BAD_USERNAME", - "invalid username", - "user" - ] - ] - } -} -*/ func (s *UserServiceOp) UsernameAvailable(ctx context.Context, username string) (bool, *Response, error) { type query struct { User string `url:"user,omitempty"` @@ -177,9 +150,6 @@ func (s *UserServiceOp) UsernameAvailable(ctx context.Context, username string) root := new(bool) resp, err := s.client.Do(ctx, req, root) - if _, ok := err.(*json.UnmarshalTypeError); ok { - return false, resp, fmt.Errorf("must provide username conforming to Reddit's criteria; username provided: %q", username) - } if err != nil { return false, resp, err } @@ -208,3 +178,219 @@ func (s *UserServiceOp) Friend(ctx context.Context, username string, note string // 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) +} + +// GetUpvoted returns a list of the client's upvoted submissions +func (s *UserServiceOp) GetUpvoted(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) { + path := fmt.Sprintf("user/%s/upvoted", s.client.Username) + 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.getLinks(), resp, nil +} + +// GetDownvoted returns a list of the client's downvoted submissions +func (s *UserServiceOp) GetDownvoted(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) { + path := fmt.Sprintf("user/%s/downvoted", s.client.Username) + 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.getLinks(), resp, nil +} + +// GetHidden returns a list of the client's hidden submissions +func (s *UserServiceOp) GetHidden(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) { + path := fmt.Sprintf("user/%s/hidden", s.client.Username) + 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.getLinks(), resp, nil +} + +// GetHotLinks returns a list of the client's hottest submissions +func (s *UserServiceOp) GetHotLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) { + return s.getLinks(ctx, s.client.Username, sortHot, opts) +} + +// GetNewLinks returns a list of the client's newest submissions +func (s *UserServiceOp) GetNewLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) { + return s.getLinks(ctx, s.client.Username, sortNew, opts) +} + +// GetTopLinks returns a list of the client's top submissions +func (s *UserServiceOp) GetTopLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) { + return s.getLinks(ctx, s.client.Username, sortTop, opts) +} + +// GetControversialLinks returns a list of the client's most controversial submissions +func (s *UserServiceOp) GetControversialLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) { + return s.getLinks(ctx, s.client.Username, sortControversial, opts) +} + +// GetHotLinksOf returns a list of the user's hottest submissions +func (s *UserServiceOp) GetHotLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) { + return s.getLinks(ctx, username, sortHot, opts) +} + +// GetNewLinksOf returns a list of the user's newest submissions +func (s *UserServiceOp) GetNewLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) { + return s.getLinks(ctx, username, sortNew, opts) +} + +// GetTopLinksOf returns a list of the user's top submissions +func (s *UserServiceOp) GetTopLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) { + return s.getLinks(ctx, username, sortTop, opts) +} + +// GetControversialLinksOf returns a list of the user's most controversial submissions +func (s *UserServiceOp) GetControversialLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) { + return s.getLinks(ctx, username, sortControversial, opts) +} + +// GetHotComments returns a list of the client's hottest comments +func (s *UserServiceOp) GetHotComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) { + return s.getComments(ctx, s.client.Username, sortHot, opts) +} + +// GetNewComments returns a list of the client's newest comments +func (s *UserServiceOp) GetNewComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) { + return s.getComments(ctx, s.client.Username, sortNew, opts) +} + +// GetTopComments returns a list of the client's top comments +func (s *UserServiceOp) GetTopComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) { + return s.getComments(ctx, s.client.Username, sortTop, opts) +} + +// GetControversialComments returns a list of the client's most controversial comments +func (s *UserServiceOp) GetControversialComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) { + return s.getComments(ctx, s.client.Username, sortControversial, opts) +} + +// GetHotCommentsOf returns a list of the user's hottest comments +func (s *UserServiceOp) GetHotCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) { + return s.getComments(ctx, username, sortHot, opts) +} + +// GetNewCommentsOf returns a list of the user's newest comments +func (s *UserServiceOp) GetNewCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) { + return s.getComments(ctx, username, sortNew, opts) +} + +// GetTopCommentsOf returns a list of the user's top comments +func (s *UserServiceOp) GetTopCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) { + return s.getComments(ctx, username, sortTop, opts) +} + +// GetControversialCommentsOf returns a list of the user's most controversial comments +func (s *UserServiceOp) GetControversialCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) { + return s.getComments(ctx, username, sortControversial, opts) +} + +func (s *UserServiceOp) getLinks(ctx context.Context, username string, sort sort, opts *ListOptions) (*LinkList, *Response, error) { + path := fmt.Sprintf("user/%s/submitted?sort=%s", username, sorts[sort]) + 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.getLinks(), resp, nil +} + +func (s *UserServiceOp) getComments(ctx context.Context, username string, sort sort, opts *ListOptions) (*CommentList, *Response, error) { + path := fmt.Sprintf("user/%s/comments?sort=%s", username, sorts[sort]) + 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.getComments(), resp, nil +}