Add ModerationService, tweak structs, delete unneeded ones

Signed-off-by: Vartan Benohanian <vartanbeno@gmail.com>
This commit is contained in:
Vartan Benohanian 2020-07-12 22:53:19 -04:00
parent 10a5d5ac86
commit a0b06ed651
9 changed files with 273 additions and 69 deletions

View File

@ -303,10 +303,11 @@ func (s *AccountService) Trophies(ctx context.Context) ([]Trophy, *Response, err
return nil, resp, err return nil, resp, err
} }
// todo: use Things struct
var trophies []Trophy var trophies []Trophy
for _, trophy := range root.Data.Trophies { for _, trophy := range root.Data.Trophies {
trophies = append(trophies, trophy.Data) if trophy.Data != nil {
trophies = append(trophies, *trophy.Data)
}
} }
return trophies, resp, nil return trophies, resp, nil

View File

@ -94,16 +94,17 @@ type Client struct {
// Reuse a single struct instead of allocating one for each service on the heap. // Reuse a single struct instead of allocating one for each service on the heap.
common service common service
Account *AccountService Account *AccountService
Comment *CommentService Comment *CommentService
Flair *FlairService Flair *FlairService
Listings *ListingsService Listings *ListingsService
Multi *MultiService Moderation *ModerationService
Post *PostService Multi *MultiService
Search *SearchService Post *PostService
Subreddit *SubredditService Search *SearchService
User *UserService Subreddit *SubredditService
Vote *VoteService User *UserService
Vote *VoteService
oauth2Transport *oauth2.Transport oauth2Transport *oauth2.Transport
@ -134,6 +135,7 @@ func newClient(httpClient *http.Client) *Client {
c.Comment = (*CommentService)(&c.common) c.Comment = (*CommentService)(&c.common)
c.Flair = (*FlairService)(&c.common) c.Flair = (*FlairService)(&c.common)
c.Listings = (*ListingsService)(&c.common) c.Listings = (*ListingsService)(&c.common)
c.Moderation = (*ModerationService)(&c.common)
c.Multi = (*MultiService)(&c.common) c.Multi = (*MultiService)(&c.common)
c.Post = (*PostService)(&c.common) c.Post = (*PostService)(&c.common)
c.Search = (*SearchService)(&c.common) c.Search = (*SearchService)(&c.common)

67
moderation.go Normal file
View File

@ -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"`
}
*/

65
moderation_test.go Normal file
View File

@ -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)
}

View File

@ -20,11 +20,8 @@ import (
// Reddit API docs: https://www.reddit.com/dev/api/#section_search // Reddit API docs: https://www.reddit.com/dev/api/#section_search
type SearchService service type SearchService service
// SearchOptions define options used in search queries. func newSearchOptions(opts ...SearchOptionSetter) url.Values {
type SearchOptions = url.Values searchOptions := make(url.Values)
func newSearchOptions(opts ...SearchOptionSetter) SearchOptions {
searchOptions := make(SearchOptions)
for _, opt := range opts { for _, opt := range opts {
opt(searchOptions) opt(searchOptions)
} }
@ -32,18 +29,18 @@ func newSearchOptions(opts ...SearchOptionSetter) SearchOptions {
} }
// SearchOptionSetter sets values for the options. // SearchOptionSetter sets values for the options.
type SearchOptionSetter func(opts SearchOptions) type SearchOptionSetter func(opts url.Values)
// SetAfter sets the after option. // SetAfter sets the after option.
func SetAfter(v string) SearchOptionSetter { func SetAfter(v string) SearchOptionSetter {
return func(opts SearchOptions) { return func(opts url.Values) {
opts.Set("after", v) opts.Set("after", v)
} }
} }
// SetBefore sets the before option. // SetBefore sets the before option.
func SetBefore(v string) SearchOptionSetter { func SetBefore(v string) SearchOptionSetter {
return func(opts SearchOptions) { return func(opts url.Values) {
opts.Set("before", v) opts.Set("before", v)
} }
} }
@ -51,41 +48,41 @@ func SetBefore(v string) SearchOptionSetter {
// SetLimit sets the limit option. // SetLimit sets the limit option.
// Warning: It seems like setting the limit to 1 sometimes returns 0 results. // Warning: It seems like setting the limit to 1 sometimes returns 0 results.
func SetLimit(v int) SearchOptionSetter { func SetLimit(v int) SearchOptionSetter {
return func(opts SearchOptions) { return func(opts url.Values) {
opts.Set("limit", fmt.Sprint(v)) opts.Set("limit", fmt.Sprint(v))
} }
} }
// SetSort sets the sort option. // SetSort sets the sort option.
func SetSort(v Sort) SearchOptionSetter { func SetSort(v Sort) SearchOptionSetter {
return func(opts SearchOptions) { return func(opts url.Values) {
opts.Set("sort", v.String()) opts.Set("sort", v.String())
} }
} }
// SetTimespan sets the timespan option. // SetTimespan sets the timespan option.
func SetTimespan(v Timespan) SearchOptionSetter { func SetTimespan(v Timespan) SearchOptionSetter {
return func(opts SearchOptions) { return func(opts url.Values) {
opts.Set("timespan", v.String()) opts.Set("timespan", v.String())
} }
} }
// setType sets the type option. // setType sets the type option.
func setType(v string) SearchOptionSetter { func setType(v string) SearchOptionSetter {
return func(opts SearchOptions) { return func(opts url.Values) {
opts.Set("type", v) opts.Set("type", v)
} }
} }
// setQuery sets the q option. // setQuery sets the q option.
func setQuery(v string) SearchOptionSetter { func setQuery(v string) SearchOptionSetter {
return func(opts SearchOptions) { return func(opts url.Values) {
opts.Set("q", v) opts.Set("q", v)
} }
} }
// setRestrict sets the restrict_sr option. // setRestrict sets the restrict_sr option.
func setRestrict(opts SearchOptions) { func setRestrict(opts url.Values) {
opts.Set("restrict_sr", "true") opts.Set("restrict_sr", "true")
} }

View File

@ -15,11 +15,16 @@ import (
// Reddit API docs: https://www.reddit.com/dev/api/#section_subreddits // Reddit API docs: https://www.reddit.com/dev/api/#section_subreddits
type SubredditService service 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"` Names []string `json:"names,omitempty"`
} }
type subredditShortsRoot struct { type rootSubredditShorts struct {
Subreddits []SubredditShort `json:"subreddits,omitempty"` Subreddits []SubredditShort `json:"subreddits,omitempty"`
} }
@ -66,7 +71,7 @@ func (s *SubredditService) GetByName(ctx context.Context, subreddit string) (*Su
return nil, nil, err return nil, nil, err
} }
root := new(subredditRoot) root := new(rootSubreddit)
resp, err := s.client.Do(ctx, req, root) resp, err := s.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
@ -161,7 +166,7 @@ func (s *SubredditService) SearchSubredditNames(ctx context.Context, query strin
return nil, nil, err return nil, nil, err
} }
root := new(subredditNamesRoot) root := new(rootSubredditNames)
resp, err := s.client.Do(ctx, req, root) resp, err := s.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
@ -180,7 +185,7 @@ func (s *SubredditService) SearchSubredditInfo(ctx context.Context, query string
return nil, nil, err return nil, nil, err
} }
root := new(subredditShortsRoot) root := new(rootSubredditShorts)
resp, err := s.client.Do(ctx, req, root) resp, err := s.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err

51
testdata/moderation/actions.json vendored Normal file
View File

@ -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
}
}

View File

@ -18,7 +18,8 @@ const (
kindKarmaList = "KarmaList" kindKarmaList = "KarmaList"
kindTrophyList = "TrophyList" kindTrophyList = "TrophyList"
kindUserList = "UserList" kindUserList = "UserList"
kindMode = "more" kindMore = "more"
kindModAction = "modaction"
) )
// Sort is a sorting option. // Sort is a sorting option.
@ -123,29 +124,10 @@ type Things struct {
Users []User `json:"users,omitempty"` Users []User `json:"users,omitempty"`
Posts []Post `json:"posts,omitempty"` Posts []Post `json:"posts,omitempty"`
Subreddits []Subreddit `json:"subreddits,omitempty"` Subreddits []Subreddit `json:"subreddits,omitempty"`
ModActions []ModAction `json:"moderationActions,omitempty"`
// todo: add the other kinds of things // 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() { func (t *Things) init() {
if t.Comments == nil { if t.Comments == nil {
t.Comments = make([]Comment, 0) t.Comments = make([]Comment, 0)
@ -159,6 +141,9 @@ func (t *Things) init() {
if t.Subreddits == nil { if t.Subreddits == nil {
t.Subreddits = make([]Subreddit, 0) t.Subreddits = make([]Subreddit, 0)
} }
if t.ModActions == nil {
t.ModActions = make([]ModAction, 0)
}
} }
// UnmarshalJSON implements the json.Unmarshaler interface. // UnmarshalJSON implements the json.Unmarshaler interface.
@ -171,31 +156,38 @@ func (t *Things) UnmarshalJSON(b []byte) error {
} }
for _, child := range children { for _, child := range children {
byteValue, _ := json.Marshal(child) data := child["data"]
byteValue, _ := json.Marshal(data)
switch child["kind"] { switch child["kind"] {
// todo: kindMore // todo: kindMore
case kindComment: case kindComment:
root := new(commentRoot) v := new(Comment)
if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { if err := json.Unmarshal(byteValue, v); err == nil && v != nil {
t.Comments = append(t.Comments, *root.Data) t.Comments = append(t.Comments, *v)
} }
case kindAccount: case kindAccount:
root := new(userRoot) v := new(User)
if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { if err := json.Unmarshal(byteValue, v); err == nil && v != nil {
t.Users = append(t.Users, *root.Data) t.Users = append(t.Users, *v)
} }
case kindLink: case kindLink:
root := new(postRoot) v := new(Post)
if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { if err := json.Unmarshal(byteValue, v); err == nil && v != nil {
t.Posts = append(t.Posts, *root.Data) t.Posts = append(t.Posts, *v)
} }
case kindMessage: case kindMessage:
case kindSubreddit: case kindSubreddit:
root := new(subredditRoot) v := new(Subreddit)
if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil { if err := json.Unmarshal(byteValue, v); err == nil && v != nil {
t.Subreddits = append(t.Subreddits, *root.Data) t.Subreddits = append(t.Subreddits, *v)
} }
case kindAward: 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 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 // Comments is a list of comments
type Comments struct { type Comments struct {
Comments []Comment `json:"comments"` Comments []Comment `json:"comments"`
@ -422,6 +424,13 @@ type Posts struct {
Before string `json:"before"` 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 // PostAndComments is a post and its comments
type PostAndComments struct { type PostAndComments struct {
Post Post `json:"post"` Post Post `json:"post"`

15
user.go
View File

@ -13,6 +13,11 @@ import (
// Reddit API docs: https://www.reddit.com/dev/api/#section_users // Reddit API docs: https://www.reddit.com/dev/api/#section_users
type UserService service type UserService service
type rootUser struct {
Kind string `json:"kind,omitempty"`
Data *User `json:"data,omitempty"`
}
// User represents a Reddit user. // User represents a Reddit user.
type User struct { type User struct {
// this is not the full ID, watch out. // this is not the full ID, watch out.
@ -65,8 +70,8 @@ type rootTrophyListing struct {
} }
type rootTrophy struct { type rootTrophy struct {
Kind string `json:"kind,omitempty"` Kind string `json:"kind,omitempty"`
Data Trophy `json:"data"` Data *Trophy `json:"data,omitempty"`
} }
// Trophy is a Reddit award. // Trophy is a Reddit award.
@ -84,7 +89,7 @@ func (s *UserService) Get(ctx context.Context, username string) (*User, *Respons
return nil, nil, err return nil, nil, err
} }
root := new(userRoot) root := new(rootUser)
resp, err := s.client.Do(ctx, req, root) resp, err := s.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
@ -501,7 +506,9 @@ func (s *UserService) TrophiesOf(ctx context.Context, username string) ([]Trophy
var trophies []Trophy var trophies []Trophy
for _, trophy := range root.Data.Trophies { for _, trophy := range root.Data.Trophies {
trophies = append(trophies, trophy.Data) if trophy.Data != nil {
trophies = append(trophies, *trophy.Data)
}
} }
return trophies, resp, nil return trophies, resp, nil