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

View File

@ -98,6 +98,7 @@ type Client struct {
Comment *CommentService
Flair *FlairService
Listings *ListingsService
Moderation *ModerationService
Multi *MultiService
Post *PostService
Search *SearchService
@ -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)

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

View File

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

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"
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"`

13
user.go
View File

@ -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.
@ -66,7 +71,7 @@ type rootTrophyListing struct {
type rootTrophy struct {
Kind string `json:"kind,omitempty"`
Data Trophy `json:"data"`
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