From a0b06ed651608be1e55b359ea2bfba3f7a144039 Mon Sep 17 00:00:00 2001 From: Vartan Benohanian Date: Sun, 12 Jul 2020 22:53:19 -0400 Subject: [PATCH] Add ModerationService, tweak structs, delete unneeded ones Signed-off-by: Vartan Benohanian --- account.go | 5 ++- geddit.go | 22 ++++----- moderation.go | 67 +++++++++++++++++++++++++++ moderation_test.go | 65 +++++++++++++++++++++++++++ search.go | 25 +++++------ subreddit.go | 15 ++++--- testdata/moderation/actions.json | 51 +++++++++++++++++++++ things.go | 77 ++++++++++++++++++-------------- user.go | 15 +++++-- 9 files changed, 273 insertions(+), 69 deletions(-) create mode 100644 moderation.go create mode 100644 moderation_test.go create mode 100644 testdata/moderation/actions.json diff --git a/account.go b/account.go index b9b34f0..1b765db 100644 --- a/account.go +++ b/account.go @@ -303,10 +303,11 @@ func (s *AccountService) Trophies(ctx context.Context) ([]Trophy, *Response, err return nil, resp, err } - // todo: use Things struct var trophies []Trophy for _, trophy := range root.Data.Trophies { - trophies = append(trophies, trophy.Data) + if trophy.Data != nil { + trophies = append(trophies, *trophy.Data) + } } return trophies, resp, nil diff --git a/geddit.go b/geddit.go index 90cb304..864cdd9 100644 --- a/geddit.go +++ b/geddit.go @@ -94,16 +94,17 @@ type Client struct { // Reuse a single struct instead of allocating one for each service on the heap. common service - Account *AccountService - Comment *CommentService - Flair *FlairService - Listings *ListingsService - Multi *MultiService - Post *PostService - Search *SearchService - Subreddit *SubredditService - User *UserService - Vote *VoteService + Account *AccountService + Comment *CommentService + Flair *FlairService + Listings *ListingsService + Moderation *ModerationService + Multi *MultiService + Post *PostService + Search *SearchService + Subreddit *SubredditService + User *UserService + Vote *VoteService oauth2Transport *oauth2.Transport @@ -134,6 +135,7 @@ func newClient(httpClient *http.Client) *Client { c.Comment = (*CommentService)(&c.common) c.Flair = (*FlairService)(&c.common) c.Listings = (*ListingsService)(&c.common) + c.Moderation = (*ModerationService)(&c.common) c.Multi = (*MultiService)(&c.common) c.Post = (*PostService)(&c.common) c.Search = (*SearchService)(&c.common) diff --git a/moderation.go b/moderation.go new file mode 100644 index 0000000..9822306 --- /dev/null +++ b/moderation.go @@ -0,0 +1,67 @@ +package reddit + +import ( + "context" + "fmt" + "net/http" +) + +// ModerationService handles communication with the moderation +// related methods of the Reddit API. +// +// Reddit API docs: https://www.reddit.com/dev/api/#section_moderation +type ModerationService service + +// ModAction is an action executed by a moderator of a subreddit, such +// as inviting another user to be a mod, or setting permissions. +type ModAction struct { + ID string `json:"id,omitempty"` + Action string `json:"action,omitempty"` + Created *Timestamp `json:"created_utc,omitempty"` + + Moderator string `json:"mod,omitempty"` + // Not the full ID, just the ID36. + ModeratorID string `json:"mod_id36,omitempty"` + + // The author of whatever the action was produced on, e.g. a user, post, comment, etc. + TargetAuthor string `json:"target_author,omitempty"` + // This is the full ID of whatever the target was. + TargetID string `json:"target_fullname,omitempty"` + TargetTitle string `json:"target_title,omitempty"` + TargetPermalink string `json:"target_permalink,omitempty"` + TargetBody string `json:"target_body,omitempty"` + + Subreddit string `json:"subreddit,omitempty"` + // Not the full ID, just the ID36. + SubredditID string `json:"sr_id36,omitempty"` +} + +// GetActions gets a list of moderator actions on a subreddit. +func (s *ModerationService) GetActions(ctx context.Context, subreddit string, opts ...SearchOptionSetter) (*ModActions, *Response, error) { + form := newSearchOptions(opts...) + + path := fmt.Sprintf("r/%s/about/log", subreddit) + path = addQuery(path, form) + + 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.getModeratorActions(), resp, nil +} + +/* +type rootTrophyListing struct { + Kind string `json:"kind,omitempty"` + Data struct { + Trophies []rootTrophy `json:"trophies"` + } `json:"data"` +} +*/ diff --git a/moderation_test.go b/moderation_test.go new file mode 100644 index 0000000..fc949a5 --- /dev/null +++ b/moderation_test.go @@ -0,0 +1,65 @@ +package reddit + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var expectedModActionsResult = &ModActions{ + ModActions: []ModAction{ + { + ID: "ModAction_b4e7979a-c4ad-11ea-8440-0ea1b7c2b8f9", + Action: "spamcomment", + Created: &Timestamp{time.Date(2020, 7, 13, 2, 8, 14, 0, time.UTC)}, + + Moderator: "v_95", + ModeratorID: "164ab8", + + TargetAuthor: "testuser", + TargetID: "t1_fxw10aa", + TargetPermalink: "/r/helloworldtestt/comments/hq6r3t/yo/fxw10aa/", + TargetBody: "hi", + + Subreddit: "helloworldtestt", + SubredditID: "2uquw1", + }, + { + ID: "ModAction_a0408162-c4ad-11ea-8239-0e3b48262e8b", + Action: "sticky", + Created: &Timestamp{time.Date(2020, 7, 13, 2, 7, 38, 0, time.UTC)}, + + Moderator: "v_95", + ModeratorID: "164ab8", + + TargetAuthor: "testuser", + TargetID: "t3_hq6r3t", + TargetTitle: "yo", + TargetPermalink: "/r/helloworldtestt/comments/hq6r3t/yo/", + + Subreddit: "helloworldtestt", + SubredditID: "2uquw1", + }, + }, + After: "ModAction_a0408162-c4ad-11ea-8239-0e3b48262e8b", + Before: "", +} + +func TestModerationService_GetActions(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/moderation/actions.json") + + mux.HandleFunc("/r/testsubreddit/about/log", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + result, _, err := client.Moderation.GetActions(ctx, "testsubreddit") + assert.NoError(t, err) + assert.Equal(t, expectedModActionsResult, result) +} diff --git a/search.go b/search.go index f82ac34..b2f2cc1 100644 --- a/search.go +++ b/search.go @@ -20,11 +20,8 @@ import ( // Reddit API docs: https://www.reddit.com/dev/api/#section_search type SearchService service -// SearchOptions define options used in search queries. -type SearchOptions = url.Values - -func newSearchOptions(opts ...SearchOptionSetter) SearchOptions { - searchOptions := make(SearchOptions) +func newSearchOptions(opts ...SearchOptionSetter) url.Values { + searchOptions := make(url.Values) for _, opt := range opts { opt(searchOptions) } @@ -32,18 +29,18 @@ func newSearchOptions(opts ...SearchOptionSetter) SearchOptions { } // SearchOptionSetter sets values for the options. -type SearchOptionSetter func(opts SearchOptions) +type SearchOptionSetter func(opts url.Values) // SetAfter sets the after option. func SetAfter(v string) SearchOptionSetter { - return func(opts SearchOptions) { + return func(opts url.Values) { opts.Set("after", v) } } // SetBefore sets the before option. func SetBefore(v string) SearchOptionSetter { - return func(opts SearchOptions) { + return func(opts url.Values) { opts.Set("before", v) } } @@ -51,41 +48,41 @@ func SetBefore(v string) SearchOptionSetter { // SetLimit sets the limit option. // Warning: It seems like setting the limit to 1 sometimes returns 0 results. func SetLimit(v int) SearchOptionSetter { - return func(opts SearchOptions) { + return func(opts url.Values) { opts.Set("limit", fmt.Sprint(v)) } } // SetSort sets the sort option. func SetSort(v Sort) SearchOptionSetter { - return func(opts SearchOptions) { + return func(opts url.Values) { opts.Set("sort", v.String()) } } // SetTimespan sets the timespan option. func SetTimespan(v Timespan) SearchOptionSetter { - return func(opts SearchOptions) { + return func(opts url.Values) { opts.Set("timespan", v.String()) } } // setType sets the type option. func setType(v string) SearchOptionSetter { - return func(opts SearchOptions) { + return func(opts url.Values) { opts.Set("type", v) } } // setQuery sets the q option. func setQuery(v string) SearchOptionSetter { - return func(opts SearchOptions) { + return func(opts url.Values) { opts.Set("q", v) } } // setRestrict sets the restrict_sr option. -func setRestrict(opts SearchOptions) { +func setRestrict(opts url.Values) { opts.Set("restrict_sr", "true") } diff --git a/subreddit.go b/subreddit.go index 92455ae..a80036f 100644 --- a/subreddit.go +++ b/subreddit.go @@ -15,11 +15,16 @@ import ( // Reddit API docs: https://www.reddit.com/dev/api/#section_subreddits type SubredditService service -type subredditNamesRoot struct { +type rootSubreddit struct { + Kind string `json:"kind,omitempty"` + Data *Subreddit `json:"data,omitempty"` +} + +type rootSubredditNames struct { Names []string `json:"names,omitempty"` } -type subredditShortsRoot struct { +type rootSubredditShorts struct { Subreddits []SubredditShort `json:"subreddits,omitempty"` } @@ -66,7 +71,7 @@ func (s *SubredditService) GetByName(ctx context.Context, subreddit string) (*Su return nil, nil, err } - root := new(subredditRoot) + root := new(rootSubreddit) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err @@ -161,7 +166,7 @@ func (s *SubredditService) SearchSubredditNames(ctx context.Context, query strin return nil, nil, err } - root := new(subredditNamesRoot) + root := new(rootSubredditNames) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err @@ -180,7 +185,7 @@ func (s *SubredditService) SearchSubredditInfo(ctx context.Context, query string return nil, nil, err } - root := new(subredditShortsRoot) + root := new(rootSubredditShorts) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err diff --git a/testdata/moderation/actions.json b/testdata/moderation/actions.json new file mode 100644 index 0000000..02129a0 --- /dev/null +++ b/testdata/moderation/actions.json @@ -0,0 +1,51 @@ +{ + "kind": "Listing", + "data": { + "modhash": null, + "dist": null, + "children": [ + { + "kind": "modaction", + "data": { + "description": null, + "target_body": "hi", + "mod_id36": "164ab8", + "created_utc": 1594606094.0, + "subreddit": "helloworldtestt", + "target_title": null, + "target_permalink": "/r/helloworldtestt/comments/hq6r3t/yo/fxw10aa/", + "subreddit_name_prefixed": "r/helloworldtestt", + "details": "spam", + "action": "spamcomment", + "target_author": "testuser", + "target_fullname": "t1_fxw10aa", + "sr_id36": "2uquw1", + "id": "ModAction_b4e7979a-c4ad-11ea-8440-0ea1b7c2b8f9", + "mod": "v_95" + } + }, + { + "kind": "modaction", + "data": { + "description": null, + "target_body": null, + "mod_id36": "164ab8", + "created_utc": 1594606058.0, + "subreddit": "helloworldtestt", + "target_title": "yo", + "target_permalink": "/r/helloworldtestt/comments/hq6r3t/yo/", + "subreddit_name_prefixed": "r/helloworldtestt", + "details": null, + "action": "sticky", + "target_author": "testuser", + "target_fullname": "t3_hq6r3t", + "sr_id36": "2uquw1", + "id": "ModAction_a0408162-c4ad-11ea-8239-0e3b48262e8b", + "mod": "v_95" + } + } + ], + "after": "ModAction_a0408162-c4ad-11ea-8239-0e3b48262e8b", + "before": null + } +} diff --git a/things.go b/things.go index 0af4c07..e1be2f8 100644 --- a/things.go +++ b/things.go @@ -18,7 +18,8 @@ const ( kindKarmaList = "KarmaList" kindTrophyList = "TrophyList" kindUserList = "UserList" - kindMode = "more" + kindMore = "more" + kindModAction = "modaction" ) // Sort is a sorting option. @@ -123,29 +124,10 @@ type Things struct { Users []User `json:"users,omitempty"` Posts []Post `json:"posts,omitempty"` Subreddits []Subreddit `json:"subreddits,omitempty"` + ModActions []ModAction `json:"moderationActions,omitempty"` // todo: add the other kinds of things } -type commentRoot struct { - Kind string `json:"kind,omitempty"` - Data *Comment `json:"data,omitempty"` -} - -type userRoot struct { - Kind string `json:"kind,omitempty"` - Data *User `json:"data,omitempty"` -} - -type postRoot struct { - Kind string `json:"kind,omitempty"` - Data *Post `json:"data,omitempty"` -} - -type subredditRoot struct { - Kind string `json:"kind,omitempty"` - Data *Subreddit `json:"data,omitempty"` -} - func (t *Things) init() { if t.Comments == nil { t.Comments = make([]Comment, 0) @@ -159,6 +141,9 @@ func (t *Things) init() { if t.Subreddits == nil { t.Subreddits = make([]Subreddit, 0) } + if t.ModActions == nil { + t.ModActions = make([]ModAction, 0) + } } // UnmarshalJSON implements the json.Unmarshaler interface. @@ -171,31 +156,38 @@ func (t *Things) UnmarshalJSON(b []byte) error { } for _, child := range children { - byteValue, _ := json.Marshal(child) + data := child["data"] + byteValue, _ := json.Marshal(data) + switch child["kind"] { // todo: kindMore case kindComment: - root := new(commentRoot) - if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { - t.Comments = append(t.Comments, *root.Data) + v := new(Comment) + if err := json.Unmarshal(byteValue, v); err == nil && v != nil { + t.Comments = append(t.Comments, *v) } case kindAccount: - root := new(userRoot) - if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { - t.Users = append(t.Users, *root.Data) + v := new(User) + if err := json.Unmarshal(byteValue, v); err == nil && v != nil { + t.Users = append(t.Users, *v) } case kindLink: - root := new(postRoot) - if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { - t.Posts = append(t.Posts, *root.Data) + v := new(Post) + if err := json.Unmarshal(byteValue, v); err == nil && v != nil { + t.Posts = append(t.Posts, *v) } case kindMessage: case kindSubreddit: - root := new(subredditRoot) - if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { - t.Subreddits = append(t.Subreddits, *root.Data) + v := new(Subreddit) + if err := json.Unmarshal(byteValue, v); err == nil && v != nil { + t.Subreddits = append(t.Subreddits, *v) } case kindAward: + case kindModAction: + v := new(ModAction) + if err := json.Unmarshal(byteValue, v); err == nil && v != nil { + t.ModActions = append(t.ModActions, *v) + } } } @@ -394,6 +386,16 @@ func (rl *rootListing) getSubreddits() *Subreddits { return v } +func (rl *rootListing) getModeratorActions() *ModActions { + v := new(ModActions) + if rl != nil && rl.Data != nil { + v.ModActions = rl.Data.Things.ModActions + v.After = rl.Data.After + v.Before = rl.Data.Before + } + return v +} + // Comments is a list of comments type Comments struct { Comments []Comment `json:"comments"` @@ -422,6 +424,13 @@ type Posts struct { Before string `json:"before"` } +// ModActions is a list of moderator action. +type ModActions struct { + ModActions []ModAction `json:"moderator_actions"` + After string `json:"after"` + Before string `json:"before"` +} + // PostAndComments is a post and its comments type PostAndComments struct { Post Post `json:"post"` diff --git a/user.go b/user.go index 6729eea..057dd0b 100644 --- a/user.go +++ b/user.go @@ -13,6 +13,11 @@ import ( // Reddit API docs: https://www.reddit.com/dev/api/#section_users type UserService service +type rootUser struct { + Kind string `json:"kind,omitempty"` + Data *User `json:"data,omitempty"` +} + // User represents a Reddit user. type User struct { // this is not the full ID, watch out. @@ -65,8 +70,8 @@ type rootTrophyListing struct { } type rootTrophy struct { - Kind string `json:"kind,omitempty"` - Data Trophy `json:"data"` + Kind string `json:"kind,omitempty"` + Data *Trophy `json:"data,omitempty"` } // Trophy is a Reddit award. @@ -84,7 +89,7 @@ func (s *UserService) Get(ctx context.Context, username string) (*User, *Respons return nil, nil, err } - root := new(userRoot) + root := new(rootUser) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err @@ -501,7 +506,9 @@ func (s *UserService) TrophiesOf(ctx context.Context, username string) ([]Trophy var trophies []Trophy for _, trophy := range root.Data.Trophies { - trophies = append(trophies, trophy.Data) + if trophy.Data != nil { + trophies = append(trophies, *trophy.Data) + } } return trophies, resp, nil