diff --git a/comment.go b/comment.go index 994d788..2f8e517 100644 --- a/comment.go +++ b/comment.go @@ -78,14 +78,14 @@ func (s *CommentService) LoadMoreReplies(ctx context.Context, comment *Comment) postID := comment.PostID commentIDs := comment.Replies.More.Children - type query struct { + type params struct { PostID string `url:"link_id"` IDs []string `url:"children,comma"` APIType string `url:"api_type"` } path := "api/morechildren" - path, err := addOptions(path, query{postID, commentIDs, "json"}) + path, err := addOptions(path, params{postID, commentIDs, "json"}) if err != nil { return nil, err } diff --git a/post.go b/post.go index 35370d8..4967652 100644 --- a/post.go +++ b/post.go @@ -81,6 +81,33 @@ func (s *PostService) Get(ctx context.Context, id string) (*PostAndComments, *Re return root, resp, nil } +// Duplicates returns the post with the id, and a list of its duplicates. +// id is the ID36 of the post, not its full id. +// Example: instead of t3_abc123, use abc123. +func (s *PostService) Duplicates(ctx context.Context, id string, opts *ListDuplicatePostOptions) (*Post, *Posts, *Response, error) { + path := fmt.Sprintf("duplicates/%s", id) + path, err := addOptions(path, opts) + if err != nil { + return nil, nil, nil, err + } + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, nil, err + } + + var root [2]rootListing + resp, err := s.client.Do(ctx, req, &root) + if err != nil { + return nil, nil, resp, err + } + + post := root[0].Data.Things.Posts[0] + duplicates := root[1].getPosts() + + return post, duplicates, resp, nil +} + func (s *PostService) submit(ctx context.Context, v interface{}) (*Submitted, *Response, error) { path := "api/submit" diff --git a/post_test.go b/post_test.go index 6864df7..b574dc6 100644 --- a/post_test.go +++ b/post_test.go @@ -130,6 +130,86 @@ var expectedEditedPost = &Post{ IsSelfPost: true, } +var expectedPost2 = &Post{ + ID: "i2gvs1", + FullID: "t3_i2gvs1", + Created: &Timestamp{time.Date(2020, 8, 2, 18, 23, 37, 0, time.UTC)}, + Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, + + Permalink: "https://www.reddit.com/r/test/comments/i2gvs1/this_is_a_title/", + URL: "http://example.com", + + Title: "This is a title", + + Likes: Bool(true), + + Score: 1, + UpvoteRatio: 1, + NumberOfComments: 0, + + SubredditID: "t5_2qh23", + SubredditName: "test", + SubredditNamePrefixed: "r/test", + + AuthorID: "t2_164ab8", + AuthorName: "v_95", +} + +var expectedPostDuplicates = &Posts{ + Posts: []*Post{ + { + ID: "8kbs85", + FullID: "t3_8kbs85", + Created: &Timestamp{time.Date(2018, 5, 18, 9, 10, 18, 0, time.UTC)}, + Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, + + Permalink: "https://www.reddit.com/r/test/comments/8kbs85/test/", + URL: "http://example.com", + + Title: "test", + + Likes: nil, + + Score: 1, + UpvoteRatio: 0.66, + NumberOfComments: 1, + + SubredditID: "t5_2qh23", + SubredditName: "test", + SubredditNamePrefixed: "r/test", + + AuthorID: "t2_d2v1r90", + AuthorName: "GarlicoinAccount", + }, + { + ID: "le1tc", + FullID: "t3_le1tc", + Created: &Timestamp{time.Date(2011, 10, 16, 13, 26, 40, 0, time.UTC)}, + Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, + + Permalink: "https://www.reddit.com/r/test/comments/le1tc/test_to_see_if_this_fixes_the_problem_of_my_likes/", + URL: "http://www.example.com", + + Title: "Test to see if this fixes the problem of my \"likes\" from the last 7 months vanishing.", + + Likes: nil, + + Score: 2, + UpvoteRatio: 1, + NumberOfComments: 1, + + SubredditID: "t5_2qh23", + SubredditName: "test", + SubredditNamePrefixed: "r/test", + + AuthorID: "t2_8dyo", + AuthorName: "prog101", + }, + }, + After: "t3_le1tc", + Before: "", +} + func TestPostService_Get(t *testing.T) { setup() defer teardown() @@ -137,16 +217,48 @@ func TestPostService_Get(t *testing.T) { blob, err := readFileContents("testdata/post/post.json") require.NoError(t, err) - mux.HandleFunc("/comments/test", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/comments/abc123", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodGet, r.Method) fmt.Fprint(w, blob) }) - postAndComments, _, err := client.Post.Get(ctx, "test") + postAndComments, _, err := client.Post.Get(ctx, "abc123") require.NoError(t, err) require.Equal(t, expectedPostAndComments, postAndComments) } +func TestPostService_Duplicates(t *testing.T) { + setup() + defer teardown() + + blob, err := readFileContents("testdata/post/duplicates.json") + require.NoError(t, err) + + mux.HandleFunc("/duplicates/abc123", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + + form := url.Values{} + form.Set("limit", "2") + form.Set("sr", "test") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.Form) + + fmt.Fprint(w, blob) + }) + + post, postDuplicates, _, err := client.Post.Duplicates(ctx, "abc123", &ListDuplicatePostOptions{ + ListOptions: ListOptions{ + Limit: 2, + }, + Subreddit: "test", + }) + require.NoError(t, err) + require.Equal(t, expectedPost2, post) + require.Equal(t, expectedPostDuplicates, postDuplicates) +} + func TestPostService_SubmitText(t *testing.T) { setup() defer teardown() diff --git a/reddit.go b/reddit.go index 5b578e0..035858b 100644 --- a/reddit.go +++ b/reddit.go @@ -389,6 +389,19 @@ type ListUserOverviewOptions struct { Time string `url:"t,omitempty"` } +// ListDuplicatePostOptions defines possible options used when getting duplicates of a post, i.e. +// other submissions of the same URL. +type ListDuplicatePostOptions struct { + ListOptions + // If empty, it'll search for duplicates in all subreddits. + Subreddit string `url:"sr,omitempty"` + // One of: num_comments, new. + Sort string `url:"sort,omitempty"` + // If true, the search will only return duplicates that are + // crossposts of the original post. + CrosspostsOnly bool `url:"crossposts_only,omitempty"` +} + // ListModActionOptions defines possible options used when getting moderation actions in a subreddit. type ListModActionOptions struct { // The max for the limit parameter here is 500. diff --git a/subreddit.go b/subreddit.go index 82f078e..a881ffc 100644 --- a/subreddit.go +++ b/subreddit.go @@ -183,13 +183,13 @@ func (s *SubredditService) Moderated(ctx context.Context, opts *ListSubredditOpt } // GetSticky1 returns the first stickied post on a subreddit (if it exists). -func (s *SubredditService) GetSticky1(ctx context.Context, name string) (*PostAndComments, *Response, error) { - return s.getSticky(ctx, name, 1) +func (s *SubredditService) GetSticky1(ctx context.Context, subreddit string) (*PostAndComments, *Response, error) { + return s.getSticky(ctx, subreddit, 1) } // GetSticky2 returns the second stickied post on a subreddit (if it exists). -func (s *SubredditService) GetSticky2(ctx context.Context, name string) (*PostAndComments, *Response, error) { - return s.getSticky(ctx, name, 2) +func (s *SubredditService) GetSticky2(ctx context.Context, subreddit string) (*PostAndComments, *Response, error) { + return s.getSticky(ctx, subreddit, 2) } func (s *SubredditService) handleSubscription(ctx context.Context, form url.Values) (*Response, error) { @@ -379,12 +379,12 @@ func (s *SubredditService) getSubreddits(ctx context.Context, path string, opts // getSticky returns one of the 2 stickied posts of the subreddit (if they exist). // Num should be equal to 1 or 2, depending on which one you want. func (s *SubredditService) getSticky(ctx context.Context, subreddit string, num int) (*PostAndComments, *Response, error) { - type query struct { + type params struct { Num int `url:"num"` } path := fmt.Sprintf("r/%s/about/sticky", subreddit) - path, err := addOptions(path, query{num}) + path, err := addOptions(path, params{num}) if err != nil { return nil, nil, err } @@ -410,12 +410,12 @@ func (s *SubredditService) random(ctx context.Context, nsfw bool) (*Subreddit, * path = "r/randnsfw" } - type query struct { + type params struct { ExpandSubreddit bool `url:"sr_detail"` Limit int `url:"limit,omitempty"` } - path, err := addOptions(path, query{true, 1}) + path, err := addOptions(path, params{true, 1}) if err != nil { return nil, nil, err } @@ -425,7 +425,7 @@ func (s *SubredditService) random(ctx context.Context, nsfw bool) (*Subreddit, * return nil, nil, err } - type rootResponse struct { + root := new(struct { Data struct { Children []struct { Data struct { @@ -433,9 +433,7 @@ func (s *SubredditService) random(ctx context.Context, nsfw bool) (*Subreddit, * } `json:"data"` } `json:"children"` } `json:"data"` - } - - root := new(rootResponse) + }) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err @@ -472,10 +470,9 @@ func (s *SubredditService) SubmissionText(ctx context.Context, name string) (str return "", nil, err } - type response struct { + root := new(struct { Text string `json:"submit_text"` - } - root := new(response) + }) resp, err := s.client.Do(ctx, req, root) if err != nil { return "", resp, err @@ -498,14 +495,14 @@ func (s *SubredditService) Banned(ctx context.Context, subreddit string, opts *L return nil, nil, err } - var root struct { + root := new(struct { Data struct { Bans []*Ban `json:"children"` After string `json:"after"` Before string `json:"before"` } `json:"data"` - } - resp, err := s.client.Do(ctx, req, &root) + }) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/testdata/post/duplicates.json b/testdata/post/duplicates.json new file mode 100644 index 0000000..1c01c6a --- /dev/null +++ b/testdata/post/duplicates.json @@ -0,0 +1,356 @@ +[ + { + "kind": "Listing", + "data": { + "modhash": null, + "dist": 1, + "children": [ + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "test", + "selftext": "", + "user_reports": [], + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "This is a title", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/test", + "hidden": false, + "pwls": 6, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "parent_whitelist_status": "all_ads", + "hide_score": false, + "name": "t3_i2gvs1", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 1.0, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 1, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "author_fullname": "t2_164ab8", + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 1, + "approved_by": null, + "author_premium": false, + "thumbnail": "default", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": false, + "mod_note": null, + "created": 1596421417.0, + "link_flair_type": "text", + "wls": 6, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "example.com", + "allow_live_comments": false, + "selftext_html": null, + "likes": true, + "suggested_sort": null, + "banned_at_utc": null, + "url_overridden_by_dest": "http://example.com", + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": true, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "rte_mode": "markdown", + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2qh23", + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "i2gvs1", + "is_robot_indexable": true, + "num_duplicates": 195, + "report_reasons": null, + "author": "v_95", + "discussion_type": null, + "num_comments": 0, + "send_replies": false, + "media": null, + "contest_mode": false, + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/test/comments/i2gvs1/this_is_a_title/", + "whitelist_status": "all_ads", + "stickied": false, + "url": "http://example.com", + "subreddit_subscribers": 8278, + "created_utc": 1596392617.0, + "num_crossposts": 0, + "mod_reports": [], + "is_video": false + } + } + ], + "after": null, + "before": null + } + }, + { + "kind": "Listing", + "data": { + "modhash": null, + "dist": 2, + "children": [ + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "test", + "selftext": "", + "author_fullname": "t2_d2v1r90", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "test", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/test", + "hidden": false, + "pwls": 6, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_8kbs85", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.66, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 1, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 1, + "approved_by": null, + "author_premium": false, + "thumbnail": "default", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": false, + "mod_note": null, + "created": 1526663418.0, + "link_flair_type": "text", + "wls": 6, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "example.com", + "allow_live_comments": false, + "selftext_html": null, + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "url_overridden_by_dest": "http://example.com", + "view_count": null, + "archived": true, + "no_follow": true, + "is_crosspostable": true, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": true, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2qh23", + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "8kbs85", + "is_robot_indexable": true, + "report_reasons": null, + "author": "GarlicoinAccount", + "discussion_type": null, + "num_comments": 1, + "send_replies": true, + "whitelist_status": "all_ads", + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/test/comments/8kbs85/test/", + "parent_whitelist_status": "all_ads", + "stickied": false, + "url": "http://example.com", + "subreddit_subscribers": 8278, + "created_utc": 1526634618.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "test", + "selftext": "", + "author_fullname": "t2_8dyo", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "Test to see if this fixes the problem of my \"likes\" from the last 7 months vanishing.", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/test", + "hidden": false, + "pwls": 6, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_le1tc", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 1.0, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 2, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 2, + "approved_by": null, + "author_premium": false, + "thumbnail": "default", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": false, + "mod_note": null, + "created": 1318800400.0, + "link_flair_type": "text", + "wls": 6, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "example.com", + "allow_live_comments": false, + "selftext_html": null, + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "url_overridden_by_dest": "http://www.example.com", + "view_count": null, + "archived": true, + "no_follow": true, + "is_crosspostable": true, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": true, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2qh23", + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "le1tc", + "is_robot_indexable": true, + "report_reasons": null, + "author": "prog101", + "discussion_type": null, + "num_comments": 1, + "send_replies": true, + "whitelist_status": "all_ads", + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/test/comments/le1tc/test_to_see_if_this_fixes_the_problem_of_my_likes/", + "parent_whitelist_status": "all_ads", + "stickied": false, + "url": "http://www.example.com", + "subreddit_subscribers": 8278, + "created_utc": 1318771600.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + } + ], + "after": "t3_le1tc", + "before": null + } + } +] diff --git a/things.go b/things.go index ac97171..456dafc 100644 --- a/things.go +++ b/things.go @@ -2,8 +2,6 @@ package reddit import ( "encoding/json" - "fmt" - "strings" ) const ( @@ -291,18 +289,6 @@ type Post struct { Stickied bool `json:"stickied"` } -func (p Post) String() string { - chunks := []string{ - fmt.Sprintf("[%d]", p.Score), - p.SubredditNamePrefixed, - "-", - p.Title, - "-", - string(p.Permalink), - } - return strings.Join(chunks, " ") -} - // Subreddit holds information about a subreddit type Subreddit struct { ID string `json:"id,omitempty"` diff --git a/user.go b/user.go index 92358b0..1d68257 100644 --- a/user.go +++ b/user.go @@ -95,12 +95,12 @@ func (s *UserService) Get(ctx context.Context, username string) (*User, *Respons // 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 *UserService) GetMultipleByID(ctx context.Context, ids ...string) (map[string]*UserSummary, *Response, error) { - type query struct { + type params struct { IDs []string `url:"ids,omitempty,comma"` } path := "api/user_data_by_account_ids" - path, err := addOptions(path, query{ids}) + path, err := addOptions(path, params{ids}) if err != nil { return nil, nil, err } @@ -121,12 +121,12 @@ func (s *UserService) GetMultipleByID(ctx context.Context, ids ...string) (map[s // UsernameAvailable checks whether a username is available for registration. func (s *UserService) UsernameAvailable(ctx context.Context, username string) (bool, *Response, error) { - type query struct { - User string `url:"user,omitempty"` + type params struct { + User string `url:"user"` } path := "api/username_available" - path, err := addOptions(path, query{username}) + path, err := addOptions(path, params{username}) if err != nil { return false, nil, err }