From e14a54f64e3a99b45d0df2bfc187c7ae02079fd0 Mon Sep 17 00:00:00 2001 From: Vartan Benohanian Date: Mon, 21 Sep 2020 23:28:05 -0400 Subject: [PATCH] Create and edit a subreddit, get its settings Signed-off-by: Vartan Benohanian --- reddit/errors.go | 9 +- reddit/subreddit.go | 154 +++++++++++++++++++++++++++++++ reddit/subreddit_test.go | 79 ++++++++++++++++ reddit/things.go | 48 ++++++---- testdata/subreddit/settings.json | 57 ++++++++++++ 5 files changed, 323 insertions(+), 24 deletions(-) create mode 100644 testdata/subreddit/settings.json diff --git a/reddit/errors.go b/reddit/errors.go index a7d6cd4..549a3c2 100644 --- a/reddit/errors.go +++ b/reddit/errors.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" ) // APIError is an error coming from Reddit. @@ -44,14 +45,14 @@ type JSONErrorResponse struct { } func (r *JSONErrorResponse) Error() string { - var message string - if len(r.JSON.Errors) > 0 { - message = r.JSON.Errors[0].Error() + errorMessages := make([]string, len(r.JSON.Errors)) + for i, err := range r.JSON.Errors { + errorMessages[i] = err.Error() } return fmt.Sprintf( "%s %s: %d %s", - r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, message, + r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, strings.Join(errorMessages, ";"), ) } diff --git a/reddit/subreddit.go b/reddit/subreddit.go index 0aeb363..316a9d6 100644 --- a/reddit/subreddit.go +++ b/reddit/subreddit.go @@ -159,6 +159,103 @@ func (s *SubredditTrafficStats) UnmarshalJSON(b []byte) error { return nil } +// SubredditSettings are a subreddit's settings. +type SubredditSettings struct { + // The id of the subreddit. + ID string `url:"-" json:"subreddit_id,omitempty"` + + // One of: public, restricted, private, gold_restricted, archived, employees_only, gold_only, user. + Type *string `url:"type,omitempty" json:"subreddit_type,omitempty"` + + // A valid IETF language tag (underscore separated). + Language *string `url:"lang,omitempty" json:"language,omitempty"` + + // No longer than 100 characters. + Title *string `url:"title,omitempty" json:"title,omitempty"` + // Raw markdown text. No longer than 500 characters. + Description *string `url:"public_description,omitempty" json:"public_description,omitempty"` + // Raw markdown text. No longer than 10240 characters. + Sidebar *string `url:"description,omitempty" json:"description,omitempty"` + // Raw markdown text. No longer than 1024 characters. + SubmissionText *string `url:"submit_text,omitempty" json:"submit_text,omitempty"` + // Raw markdown text. No longer than 5000 characters. + WelcomeMessage *string `url:"welcome_message_text,omitempty" json:"welcome_message_text,omitempty"` + WelcomeMessageEnabled *bool `url:"welcome_message_enabled,omitempty" json:"welcome_message_enabled,omitempty"` + + AllowCrossposts *bool `url:"allow_post_crossposts,omitempty" json:"allow_post_crossposts,omitempty"` + AllowChatPosts *bool `url:"allow_chat_post_creation,omitempty" json:"allow_chat_post_creation,omitempty"` + AllowPollPosts *bool `url:"allow_polls,omitempty" json:"allow_polls,omitempty"` + AllowFreeFormReports *bool `url:"free_form_reports,omitempty" json:"free_form_reports,omitempty"` + AllowOriginalContent *bool `url:"original_content_tag_enabled,omitempty" json:"original_content_tag_enabled,omitempty"` + // Allow image uploads and links to image hosting sites. + AllowImages *bool `url:"allow_images,omitempty" json:"allow_images,omitempty"` + AllowMultipleImagesPerPost *bool `url:"allow_galleries,omitempty" json:"allow_galleries,omitempty"` + + ExcludeSitewideBannedUsersContent *bool `url:"exclude_banned_modqueue,omitempty" json:"exclude_banned_modqueue,omitempty"` + + // An integer from 0 to 3. + CrowdControlChalLevel *int `url:"crowd_control_chat_level,omitempty" json:"crowd_control_chat_level,omitempty"` + + // Mark all posts in this subreddit as Original Content (OC) on the desktop redesign. + AllOriginalContent *bool `url:"all_original_content,omitempty" json:"all_original_content,omitempty"` + + // One of: none (recommended), confidence, top, new, controversial, old, random, qa, live. + SuggestedCommentSort *string `url:"suggested_comment_sort,omitempty" json:"suggested_comment_sort,omitempty"` + + // No longer than 60 characters. + SubmitLinkPostLabel *string `url:"submit_link_label,omitempty" json:"submit_link_label,omitempty"` + // No longer than 60 characters. + SubmitTextPostLabel *string `url:"submit_text_label,omitempty" json:"submit_text_label,omitempty"` + + // One of: any, link, self. + PostType *string `url:"link_type,omitempty" json:"content_options,omitempty"` + + // One of: low (disable most filtering), high (standard), all (filter everything, requiring mod approval). + SpamFilterStrengthLinkPosts *string `url:"spam_links,omitempty" json:"spam_links,omitempty"` + // One of: low (disable most filtering), high (standard), all (filter everything, requiring mod approval). + SpamFilterStrengthTextPosts *string `url:"spam_selfposts,omitempty" json:"spam_selfposts,omitempty"` + // One of: low (disable most filtering), high (standard), all (filter everything, requiring mod approval). + SpamFilterStrengthComments *string `url:"spam_comments,omitempty" json:"spam_comments,omitempty"` + + ShowContentThumbnails *bool `url:"show_media,omitempty" json:"show_media,omitempty"` + ExpandMediaPreviewsOnCommentsPages *bool `url:"show_media_preview,omitempty" json:"show_media_preview,omitempty"` + + CollapseDeletedComments *bool `url:"collapse_deleted_comments,omitempty" json:"collapse_deleted_comments,omitempty"` + // An integer between 0 and 1440. + MinutesToHideCommentScores *int `url:"comment_score_hide_mins,omitempty" json:"comment_score_hide_mins,omitempty"` + + // Enable marking posts as containing spoilers. + SpoilersEnabled *bool `url:"spoilers_enabled,omitempty" json:"spoilers_enabled,omitempty"` + + // If there's an image header set, hovering the mouse over it will display this text. + HeaderMouseoverText *string `url:"header-title,omitempty" json:"header_hover_text,omitempty"` + + // 6-digit rgb hex colour, e.g. #AABBCC. + // Thematic colour for the subreddit on mobile. + MobileColour *string `url:"key_color,omitempty" json:"key_color,omitempty"` + + // Can only be set to true if subreddit type is gold_only. + HideAds *bool `url:"hide_ads,omitempty" json:"hide_ads,omitempty"` + // Require viewers to be over 18 years old. + NSFW *bool `url:"over_18,omitempty" json:"over_18,omitempty"` + + // Show up in high-traffic feeds: Allow your community to be in r/all, r/popular, and trending lists where it can be + // seen by the general Reddit population. + AllowDiscoveryInHighTrafficFeeds *bool `url:"allow_top,omitempty" json:"default_set,omitempty"` + // Get recommended to individual redditors. Let Reddit recommend your community to people who have similar interests. + AllowDiscoveryByIndividualUsers *bool `url:"allow_discovery,omitempty" json:"allow_discovery,omitempty"` + + // One of: + // - disabled: wiki is disabled for everyone except mods. + // - modonly: only mods, approved wiki contributors, or those on a page's edit list may edit. + // - anyone: anyone who can submit to the subreddit may edit. + WikiMode *string `url:"wikimode,omitempty" json:"wikimode,omitempty"` + // Account age (in days) required to create and edit wiki pages. + WikiMinimumAccountAge *int `url:"wiki_edit_age,omitempty" json:"wiki_edit_age,omitempty"` + // Subreddit karma required to create and edit wiki pages. + WikiMinimumKarma *int `url:"wiki_edit_karma,omitempty" json:"wiki_edit_karma,omitempty"` +} + func (s *SubredditService) getPosts(ctx context.Context, sort string, subreddit string, opts interface{}) ([]*Post, *Response, error) { path := sort if subreddit != "" { @@ -977,3 +1074,60 @@ func (s *SubredditService) UploadMobileHeader(ctx context.Context, subreddit, im func (s *SubredditService) UploadMobileIcon(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) { return s.uploadImage(ctx, subreddit, imagePath, "icon", imageName) } + +// Create a subreddit. +func (s *SubredditService) Create(ctx context.Context, name string, request *SubredditSettings) (*Response, error) { + if request == nil { + return nil, errors.New("*SubredditSettings: cannot be nil") + } + + form, err := query.Values(request) + if err != nil { + return nil, err + } + form.Set("name", name) + form.Set("api_type", "json") + + path := "api/site_admin" + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// Edit a subreddit. +// This endpoint expects all values of the request to be provided. +// To make this easier, it might be useful to get the subreddit's current settings via GetSettings(), and use that as a starting point. +func (s *SubredditService) Edit(ctx context.Context, subredditID string, request *SubredditSettings) (*Response, error) { + if request == nil { + return nil, errors.New("*SubredditSettings: cannot be nil") + } + + form, err := query.Values(request) + if err != nil { + return nil, err + } + form.Set("sr", subredditID) + form.Set("api_type", "json") + + path := "api/site_admin" + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// GetSettings gets the settings of a subreddit. +func (s *SubredditService) GetSettings(ctx context.Context, subreddit string) (*SubredditSettings, *Response, error) { + path := fmt.Sprintf("r/%s/about/edit", subreddit) + t, resp, err := s.client.getThing(ctx, path, nil) + if err != nil { + return nil, resp, err + } + settings, _ := t.SubredditSettings() + return settings, resp, nil +} diff --git a/reddit/subreddit_test.go b/reddit/subreddit_test.go index 375723f..d405680 100644 --- a/reddit/subreddit_test.go +++ b/reddit/subreddit_test.go @@ -338,6 +338,68 @@ var expectedStyleSheet = &SubredditStyleSheet{ }`, } +var expectedSubredditSettings = &SubredditSettings{ + ID: "t5_test", + + Type: String("private"), + + Language: String("en"), + + Title: String("hello!"), + Description: String("description"), + Sidebar: String("sidebar"), + SubmissionText: String(""), + WelcomeMessage: String(""), + WelcomeMessageEnabled: Bool(false), + + AllowCrossposts: Bool(false), + AllowChatPosts: Bool(true), + AllowPollPosts: Bool(false), + AllowFreeFormReports: Bool(true), + AllowOriginalContent: Bool(false), + AllowImages: Bool(true), + AllowMultipleImagesPerPost: Bool(true), + + ExcludeSitewideBannedUsersContent: Bool(false), + + CrowdControlChalLevel: Int(2), + + AllOriginalContent: Bool(false), + + SuggestedCommentSort: nil, + + SubmitLinkPostLabel: String("submit a link!"), + SubmitTextPostLabel: String("submit a post!"), + + PostType: String("any"), + + SpamFilterStrengthLinkPosts: String("low"), + SpamFilterStrengthTextPosts: String("low"), + SpamFilterStrengthComments: String("low"), + + ShowContentThumbnails: Bool(false), + ExpandMediaPreviewsOnCommentsPages: Bool(true), + + CollapseDeletedComments: Bool(false), + MinutesToHideCommentScores: Int(0), + + SpoilersEnabled: Bool(true), + + HeaderMouseoverText: String("hello!"), + + MobileColour: String(""), + + HideAds: Bool(false), + NSFW: Bool(false), + + AllowDiscoveryInHighTrafficFeeds: Bool(true), + AllowDiscoveryByIndividualUsers: Bool(true), + + WikiMode: String("modonly"), + WikiMinimumAccountAge: Int(0), + WikiMinimumKarma: Int(0), +} + func TestSubredditService_HotPosts(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -1523,3 +1585,20 @@ func TestSubredditService_UploadImage_Error(t *testing.T) { _, _, err = client.Subreddit.UploadImage(ctx, "testsubreddit", imageFile.Name(), "testname") require.EqualError(t, err, "could not upload image: error one; error two") } + +func TestSubredditService_GetSettings(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + blob, err := readFileContents("../testdata/subreddit/settings.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/about/edit", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + subredditSettings, _, err := client.Subreddit.GetSettings(ctx, "testsubreddit") + require.NoError(t, err) + require.Equal(t, expectedSubredditSettings, subredditSettings) +} diff --git a/reddit/things.go b/reddit/things.go index 4e6ae65..c9ed724 100644 --- a/reddit/things.go +++ b/reddit/things.go @@ -6,26 +6,27 @@ import ( ) const ( - kindComment = "t1" - kindUser = "t2" - kindPost = "t3" - kindMessage = "t4" - kindSubreddit = "t5" - kindTrophy = "t6" - kindListing = "Listing" - kindKarmaList = "KarmaList" - kindTrophyList = "TrophyList" - kindUserList = "UserList" - kindMore = "more" - kindLiveThread = "LiveUpdateEvent" - kindLiveThreadUpdate = "LiveUpdate" - kindModAction = "modaction" - kindMulti = "LabeledMulti" - kindMultiDescription = "LabeledMultiDescription" - kindWikiPage = "wikipage" - kindWikiPageListing = "wikipagelisting" - kindWikiPageSettings = "wikipagesettings" - kindStyleSheet = "stylesheet" + kindComment = "t1" + kindUser = "t2" + kindPost = "t3" + kindMessage = "t4" + kindSubreddit = "t5" + kindTrophy = "t6" + kindListing = "Listing" + kindSubredditSettings = "subreddit_settings" + kindKarmaList = "KarmaList" + kindTrophyList = "TrophyList" + kindUserList = "UserList" + kindMore = "more" + kindLiveThread = "LiveUpdateEvent" + kindLiveThreadUpdate = "LiveUpdate" + kindModAction = "modaction" + kindMulti = "LabeledMulti" + kindMultiDescription = "LabeledMultiDescription" + kindWikiPage = "wikipage" + kindWikiPageListing = "wikipagelisting" + kindWikiPageSettings = "wikipagesettings" + kindStyleSheet = "stylesheet" ) type anchor interface { @@ -91,6 +92,8 @@ func (t *thing) UnmarshalJSON(b []byte) error { v = new(Post) case kindSubreddit: v = new(Subreddit) + case kindSubredditSettings: + v = new(SubredditSettings) case kindLiveThread: v = new(LiveThread) case kindLiveThreadUpdate: @@ -158,6 +161,11 @@ func (t *thing) Subreddit() (v *Subreddit, ok bool) { return } +func (t *thing) SubredditSettings() (v *SubredditSettings, ok bool) { + v, ok = t.Data.(*SubredditSettings) + return +} + func (t *thing) LiveThread() (v *LiveThread, ok bool) { v, ok = t.Data.(*LiveThread) return diff --git a/testdata/subreddit/settings.json b/testdata/subreddit/settings.json new file mode 100644 index 0000000..0d271d8 --- /dev/null +++ b/testdata/subreddit/settings.json @@ -0,0 +1,57 @@ +{ + "kind": "subreddit_settings", + "data": { + "default_set": true, + "toxicity_threshold_chat_level": 1, + "crowd_control_chat_level": 2, + "disable_contributor_requests": false, + "subreddit_id": "t5_test", + "allow_images": true, + "free_form_reports": true, + "domain": null, + "show_media": false, + "wiki_edit_age": 0, + "submit_text": "", + "allow_polls": false, + "title": "hello!", + "collapse_deleted_comments": false, + "wikimode": "modonly", + "over_18": false, + "allow_videos": true, + "spoilers_enabled": true, + "new_pinned_post_pns_enabled": true, + "crowd_control_mode": false, + "welcome_message_enabled": false, + "welcome_message_text": "", + "suggested_comment_sort": null, + "restrict_posting": true, + "original_content_tag_enabled": false, + "description": "sidebar", + "submit_link_label": "submit a link!", + "allow_galleries": true, + "allow_post_crossposts": false, + "spam_comments": "low", + "public_traffic": false, + "restrict_commenting": false, + "crowd_control_level": 0, + "submit_text_label": "submit a post!", + "all_original_content": false, + "spam_selfposts": "low", + "key_color": "", + "language": "en", + "wiki_edit_karma": 0, + "hide_ads": false, + "header_hover_text": "hello!", + "allow_chat_post_creation": true, + "allow_discovery": true, + "exclude_banned_modqueue": false, + "public_description": "description", + "show_media_preview": true, + "comment_score_hide_mins": 0, + "subreddit_type": "private", + "spam_links": "low", + "allow_predictions": false, + "user_flair_pns_enabled": true, + "content_options": "any" + } +}