diff --git a/go.mod b/go.mod index 556df35..7c67ee5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/vartanbeno/go-reddit -go 1.14 +go 1.15 require ( github.com/google/go-querystring v1.0.0 diff --git a/reddit/listings.go b/reddit/listings.go index e207d98..a7b8bb1 100644 --- a/reddit/listings.go +++ b/reddit/listings.go @@ -3,7 +3,6 @@ package reddit import ( "context" "fmt" - "net/http" "strings" ) @@ -17,38 +16,25 @@ type ListingsService struct { // Get posts, comments, and subreddits from their full IDs. func (s *ListingsService) Get(ctx context.Context, ids ...string) ([]*Post, []*Comment, []*Subreddit, *Response, error) { - path := fmt.Sprintf("api/info?id=%s", strings.Join(ids, ",")) + path := "api/info" + params := struct { + IDs []string `url:"id,omitempty,comma"` + }{ids} - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, nil, nil, err - } - - root := new(thing) - resp, err := s.client.Do(ctx, req, root) + l, resp, err := s.client.getListing(ctx, path, params) if err != nil { return nil, nil, nil, resp, err } - listing, _ := root.Listing() - return listing.Posts(), listing.Comments(), listing.Subreddits(), resp, nil + return l.Posts(), l.Comments(), l.Subreddits(), resp, nil } // GetPosts returns posts from their full IDs. func (s *ListingsService) GetPosts(ctx context.Context, ids ...string) ([]*Post, *Response, error) { path := fmt.Sprintf("by_id/%s", strings.Join(ids, ",")) - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(thing) - resp, err := s.client.Do(ctx, req, root) + l, resp, err := s.client.getListing(ctx, path, nil) if err != nil { return nil, resp, err } - - listing, _ := root.Listing() - return listing.Posts(), resp, nil + return l.Posts(), resp, nil } diff --git a/reddit/listings_test.go b/reddit/listings_test.go index 49824d0..a7a537f 100644 --- a/reddit/listings_test.go +++ b/reddit/listings_test.go @@ -31,6 +31,7 @@ var expectedListingPosts = []*Post{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8202, Author: "v_95", AuthorID: "t2_164ab8", @@ -106,6 +107,7 @@ var expectedListingPosts2 = []*Post{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8201, Author: "v_95", AuthorID: "t2_164ab8", @@ -131,6 +133,7 @@ var expectedListingPosts2 = []*Post{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8201, Author: "v_95", AuthorID: "t2_164ab8", diff --git a/reddit/moderation.go b/reddit/moderation.go index 11742c9..679f3aa 100644 --- a/reddit/moderation.go +++ b/reddit/moderation.go @@ -42,32 +42,71 @@ type ModAction struct { SubredditID string `json:"sr_id36,omitempty"` } +// ModPermissions are the different permissions moderators have or don't have on a subreddit. +// Read about them here: https://mods.reddithelp.com/hc/en-us/articles/360009381491-User-Management-moderators-and-permissions +type ModPermissions struct { + All bool `permission:"all"` + Access bool `permission:"access"` + ChatConfig bool `permission:"chat_config"` + ChatOperator bool `permission:"chat_operator"` + Config bool `permission:"config"` + Flair bool `permission:"flair"` + Mail bool `permission:"mail"` + Posts bool `permission:"posts"` + Wiki bool `permission:"wiki"` +} + +func (p *ModPermissions) String() (s string) { + if p == nil { + return "+all" + } + + t := reflect.TypeOf(*p) + v := reflect.ValueOf(*p) + + for i := 0; i < t.NumField(); i++ { + if v.Field(i).Kind() != reflect.Bool { + continue + } + + permission := t.Field(i).Tag.Get("permission") + permitted := v.Field(i).Bool() + + if permitted { + s += "+" + } else { + s += "-" + } + + s += permission + + if i != t.NumField()-1 { + s += "," + } + } + + return +} + +// BanConfig configures the ban of the user being banned. +type BanConfig struct { + Reason string `url:"reason,omitempty"` + // Not visible to the user being banned. + ModNote string `url:"note,omitempty"` + // How long the ban will last. 0-999. Leave nil for permanent. + Days *int `url:"duration,omitempty"` + // Note to include in the ban message to the user. + Message string `url:"ban_message,omitempty"` +} + // Actions gets a list of moderator actions on a subreddit. func (s *ModerationService) Actions(ctx context.Context, subreddit string, opts *ListModActionOptions) ([]*ModAction, *Response, error) { path := fmt.Sprintf("r/%s/about/log", subreddit) - path, err := addOptions(path, opts) - if err != nil { - return nil, nil, err - } - - 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(thing) - resp, err := s.client.Do(ctx, req, root) + l, resp, err := s.client.getListing(ctx, path, opts) if err != nil { return nil, resp, err } - - listing, _ := root.Listing() - return listing.ModActions(), resp, nil + return l.ModActions(), resp, nil } // AcceptInvite accepts a pending invite to moderate the specified subreddit. @@ -162,28 +201,55 @@ func (s *ModerationService) LeaveContributor(ctx context.Context, subredditID st return s.client.Do(ctx, req, nil) } +// Reported returns posts and comments that have been reported. +func (s *ModerationService) Reported(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, []*Comment, *Response, error) { + path := fmt.Sprintf("r/%s/about/reports", subreddit) + l, resp, err := s.client.getListing(ctx, path, opts) + if err != nil { + return nil, nil, resp, err + } + return l.Posts(), l.Comments(), resp, nil +} + +// Spam returns posts and comments marked as spam. +func (s *ModerationService) Spam(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, []*Comment, *Response, error) { + path := fmt.Sprintf("r/%s/about/spam", subreddit) + l, resp, err := s.client.getListing(ctx, path, opts) + if err != nil { + return nil, nil, resp, err + } + return l.Posts(), l.Comments(), resp, nil +} + +// Queue returns posts and comments requiring moderator reviews, such as one that have been +// reported or caught in the spam filter. +func (s *ModerationService) Queue(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, []*Comment, *Response, error) { + path := fmt.Sprintf("r/%s/about/modqueue", subreddit) + l, resp, err := s.client.getListing(ctx, path, opts) + if err != nil { + return nil, nil, resp, err + } + return l.Posts(), l.Comments(), resp, nil +} + +// Unmoderated returns posts that have yet to be approved/removed by a mod. +func (s *ModerationService) Unmoderated(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, *Response, error) { + path := fmt.Sprintf("r/%s/about/unmoderated", subreddit) + l, resp, err := s.client.getListing(ctx, path, opts) + if err != nil { + return nil, resp, err + } + return l.Posts(), resp, nil +} + // Edited gets posts and comments that have been edited recently. func (s *ModerationService) Edited(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, []*Comment, *Response, error) { path := fmt.Sprintf("r/%s/about/edited", subreddit) - - path, err := addOptions(path, opts) + l, resp, err := s.client.getListing(ctx, path, opts) if err != nil { - return nil, nil, nil, err + return nil, nil, resp, err } - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, nil, err - } - - root := new(thing) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, nil, nil, err - } - - listing, _ := root.Listing() - return listing.Posts(), listing.Comments(), resp, nil + return l.Posts(), l.Comments(), resp, nil } // IgnoreReports prevents reports on a post or comment from causing notifications. @@ -216,52 +282,6 @@ func (s *ModerationService) UnignoreReports(ctx context.Context, id string) (*Re return s.client.Do(ctx, req, nil) } -// ModPermissions are the different permissions moderators have or don't have on a subreddit. -// Read about them here: https://mods.reddithelp.com/hc/en-us/articles/360009381491-User-Management-moderators-and-permissions -type ModPermissions struct { - All bool `permission:"all"` - Access bool `permission:"access"` - ChatConfig bool `permission:"chat_config"` - ChatOperator bool `permission:"chat_operator"` - Config bool `permission:"config"` - Flair bool `permission:"flair"` - Mail bool `permission:"mail"` - Posts bool `permission:"posts"` - Wiki bool `permission:"wiki"` -} - -func (p *ModPermissions) String() (s string) { - if p == nil { - return "+all" - } - - t := reflect.TypeOf(*p) - v := reflect.ValueOf(*p) - - for i := 0; i < t.NumField(); i++ { - if v.Field(i).Kind() != reflect.Bool { - continue - } - - permission := t.Field(i).Tag.Get("permission") - permitted := v.Field(i).Bool() - - if permitted { - s += "+" - } else { - s += "-" - } - - s += permission - - if i != t.NumField()-1 { - s += "," - } - } - - return -} - // Invite a user to become a moderator of the subreddit. // If permissions is nil, all permissions will be granted. func (s *ModerationService) Invite(ctx context.Context, subreddit string, username string, permissions *ModPermissions) (*Response, error) { @@ -305,17 +325,6 @@ func (s *ModerationService) SetPermissions(ctx context.Context, subreddit string return s.client.Do(ctx, req, nil) } -// BanConfig configures the ban of the user being banned. -type BanConfig struct { - Reason string `url:"reason,omitempty"` - // Not visible to the user being banned. - ModNote string `url:"note,omitempty"` - // How long the ban will last. 0-999. Leave nil for permanent. - Days *int `url:"duration,omitempty"` - // Note to include in the ban message to the user. - Message string `url:"ban_message,omitempty"` -} - // Ban a user from the subreddit. func (s *ModerationService) Ban(ctx context.Context, subreddit string, username string, config *BanConfig) (*Response, error) { path := fmt.Sprintf("r/%s/api/friend", subreddit) diff --git a/reddit/moderation_test.go b/reddit/moderation_test.go index 4ccc2b0..7ed4806 100644 --- a/reddit/moderation_test.go +++ b/reddit/moderation_test.go @@ -193,6 +193,102 @@ func TestModerationService_LeaveContributor(t *testing.T) { require.NoError(t, err) } +func TestModerationService_Reported(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + // contains posts and comments + blob, err := readFileContents("../testdata/user/overview.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/about/reports", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + posts, comments, resp, err := client.Moderation.Reported(ctx, "testsubreddit", nil) + require.NoError(t, err) + + require.Len(t, posts, 1) + require.Equal(t, expectedPost, posts[0]) + require.Equal(t, "t1_f0zsa37", resp.After) + + require.Len(t, comments, 1) + require.Equal(t, expectedComment, comments[0]) + require.Equal(t, "t1_f0zsa37", resp.After) +} + +func TestModerationService_Spam(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + // contains posts and comments + blob, err := readFileContents("../testdata/user/overview.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/about/spam", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + posts, comments, resp, err := client.Moderation.Spam(ctx, "testsubreddit", nil) + require.NoError(t, err) + + require.Len(t, posts, 1) + require.Equal(t, expectedPost, posts[0]) + require.Equal(t, "t1_f0zsa37", resp.After) + + require.Len(t, comments, 1) + require.Equal(t, expectedComment, comments[0]) + require.Equal(t, "t1_f0zsa37", resp.After) +} + +func TestModerationService_Queue(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + // contains posts and comments + blob, err := readFileContents("../testdata/user/overview.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/about/modqueue", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + posts, comments, resp, err := client.Moderation.Queue(ctx, "testsubreddit", nil) + require.NoError(t, err) + + require.Len(t, posts, 1) + require.Equal(t, expectedPost, posts[0]) + require.Equal(t, "t1_f0zsa37", resp.After) + + require.Len(t, comments, 1) + require.Equal(t, expectedComment, comments[0]) + require.Equal(t, "t1_f0zsa37", resp.After) +} + +func TestModerationService_Unmoderated(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + // contains posts and comments + blob, err := readFileContents("../testdata/user/overview.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/about/unmoderated", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + posts, resp, err := client.Moderation.Unmoderated(ctx, "testsubreddit", nil) + require.NoError(t, err) + + require.Len(t, posts, 1) + require.Equal(t, expectedPost, posts[0]) + require.Equal(t, "t1_f0zsa37", resp.After) +} + func TestModerationService_Edited(t *testing.T) { client, mux, teardown := setup() defer teardown() diff --git a/reddit/post_test.go b/reddit/post_test.go index ecc3218..90de854 100644 --- a/reddit/post_test.go +++ b/reddit/post_test.go @@ -30,6 +30,7 @@ var expectedPostAndComments = &PostAndComments{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8077, Author: "testuser", AuthorID: "t2_testuser", @@ -122,6 +123,7 @@ var expectedEditedPost = &Post{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8128, Author: "v_95", AuthorID: "t2_164ab8", @@ -150,6 +152,7 @@ var expectedPost2 = &Post{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8278, Author: "v_95", AuthorID: "t2_164ab8", @@ -176,6 +179,7 @@ var expectedPostDuplicates = []*Post{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8278, Author: "GarlicoinAccount", AuthorID: "t2_d2v1r90", @@ -200,6 +204,7 @@ var expectedPostDuplicates = []*Post{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8278, Author: "prog101", AuthorID: "t2_8dyo", diff --git a/reddit/reddit.go b/reddit/reddit.go index af698bd..5d546a9 100644 --- a/reddit/reddit.go +++ b/reddit/reddit.go @@ -391,6 +391,27 @@ func CheckResponse(r *http.Response) error { return errorResponse } +func (c *Client) getListing(ctx context.Context, path string, opts interface{}) (*listing, *Response, error) { + path, err := addOptions(path, opts) + if err != nil { + return nil, nil, err + } + + req, err := c.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(thing) + resp, err := c.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + listing, _ := root.Listing() + return listing, resp, nil +} + // ListOptions specifies the optional parameters to various API calls that return a listing. type ListOptions struct { // Maximum number of items to be returned. diff --git a/reddit/subreddit_test.go b/reddit/subreddit_test.go index 30a3461..f610472 100644 --- a/reddit/subreddit_test.go +++ b/reddit/subreddit_test.go @@ -30,6 +30,7 @@ var expectedPosts = []*Post{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8154, Author: "kmiller0112", AuthorID: "t2_30a5ktgt", @@ -55,6 +56,7 @@ var expectedPosts = []*Post{ SubredditName: "test", SubredditNamePrefixed: "r/test", SubredditID: "t5_2qh23", + SubredditSubscribers: 8154, Author: "MuckleMcDuckle", AuthorID: "t2_6fqntbwq", @@ -165,6 +167,7 @@ var expectedSearchPosts = []*Post{ SubredditName: "WatchPeopleDieInside", SubredditNamePrefixed: "r/WatchPeopleDieInside", SubredditID: "t5_3h4zq", + SubredditSubscribers: 2599948, Author: "chocolat_ice_cream", AuthorID: "t2_3p32m02", @@ -187,6 +190,7 @@ var expectedSearchPosts = []*Post{ SubredditName: "worldnews", SubredditNamePrefixed: "r/worldnews", SubredditID: "t5_2qh13", + SubredditSubscribers: 24651441, Author: "Jeremy_Martin", AuthorID: "t2_wgrkg", diff --git a/reddit/things.go b/reddit/things.go index ad1e13d..c7f3ee9 100644 --- a/reddit/things.go +++ b/reddit/things.go @@ -486,7 +486,7 @@ type Post struct { Title string `json:"title,omitempty"` Body string `json:"selftext,omitempty"` - // Indicates if you've upvote/downvoted (true/false). + // Indicates if you've upvoted/downvoted (true/false). // If neither, it will be nil. Likes *bool `json:"likes"` @@ -497,6 +497,7 @@ type Post struct { SubredditName string `json:"subreddit,omitempty"` SubredditNamePrefixed string `json:"subreddit_name_prefixed,omitempty"` SubredditID string `json:"subreddit_id,omitempty"` + SubredditSubscribers int `json:"subreddit_subscribers"` Author string `json:"author,omitempty"` AuthorID string `json:"author_fullname,omitempty"` diff --git a/reddit/user_test.go b/reddit/user_test.go index e3afe06..bd1cd8c 100644 --- a/reddit/user_test.go +++ b/reddit/user_test.go @@ -67,6 +67,7 @@ var expectedPost = &Post{ SubredditName: "redditdev", SubredditNamePrefixed: "r/redditdev", SubredditID: "t5_2qizd", + SubredditSubscribers: 37829, Author: "v_95", AuthorID: "t2_164ab8", diff --git a/reddit/wiki_test.go b/reddit/wiki_test.go index 57b5a30..02a268a 100644 --- a/reddit/wiki_test.go +++ b/reddit/wiki_test.go @@ -69,6 +69,7 @@ var expectedWikiPageDiscussions = []*Post{ SubredditName: "helloworldtestt", SubredditNamePrefixed: "r/helloworldtestt", SubredditID: "t5_2uquw1", + SubredditSubscribers: 2, Author: "v_95", AuthorID: "t2_164ab8",