Create and edit a subreddit, get its settings

Signed-off-by: Vartan Benohanian <vartanbeno@gmail.com>
This commit is contained in:
Vartan Benohanian 2020-09-21 23:28:05 -04:00
parent a76dfa0a00
commit e14a54f64e
5 changed files with 323 additions and 24 deletions

View file

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strings"
) )
// APIError is an error coming from Reddit. // APIError is an error coming from Reddit.
@ -44,14 +45,14 @@ type JSONErrorResponse struct {
} }
func (r *JSONErrorResponse) Error() string { func (r *JSONErrorResponse) Error() string {
var message string errorMessages := make([]string, len(r.JSON.Errors))
if len(r.JSON.Errors) > 0 { for i, err := range r.JSON.Errors {
message = r.JSON.Errors[0].Error() errorMessages[i] = err.Error()
} }
return fmt.Sprintf( return fmt.Sprintf(
"%s %s: %d %s", "%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, ";"),
) )
} }

View file

@ -159,6 +159,103 @@ func (s *SubredditTrafficStats) UnmarshalJSON(b []byte) error {
return nil 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) { func (s *SubredditService) getPosts(ctx context.Context, sort string, subreddit string, opts interface{}) ([]*Post, *Response, error) {
path := sort path := sort
if subreddit != "" { 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) { func (s *SubredditService) UploadMobileIcon(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) {
return s.uploadImage(ctx, subreddit, imagePath, "icon", imageName) 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
}

View file

@ -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) { func TestSubredditService_HotPosts(t *testing.T) {
client, mux, teardown := setup() client, mux, teardown := setup()
defer teardown() defer teardown()
@ -1523,3 +1585,20 @@ func TestSubredditService_UploadImage_Error(t *testing.T) {
_, _, err = client.Subreddit.UploadImage(ctx, "testsubreddit", imageFile.Name(), "testname") _, _, err = client.Subreddit.UploadImage(ctx, "testsubreddit", imageFile.Name(), "testname")
require.EqualError(t, err, "could not upload image: error one; error two") 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)
}

View file

@ -6,26 +6,27 @@ import (
) )
const ( const (
kindComment = "t1" kindComment = "t1"
kindUser = "t2" kindUser = "t2"
kindPost = "t3" kindPost = "t3"
kindMessage = "t4" kindMessage = "t4"
kindSubreddit = "t5" kindSubreddit = "t5"
kindTrophy = "t6" kindTrophy = "t6"
kindListing = "Listing" kindListing = "Listing"
kindKarmaList = "KarmaList" kindSubredditSettings = "subreddit_settings"
kindTrophyList = "TrophyList" kindKarmaList = "KarmaList"
kindUserList = "UserList" kindTrophyList = "TrophyList"
kindMore = "more" kindUserList = "UserList"
kindLiveThread = "LiveUpdateEvent" kindMore = "more"
kindLiveThreadUpdate = "LiveUpdate" kindLiveThread = "LiveUpdateEvent"
kindModAction = "modaction" kindLiveThreadUpdate = "LiveUpdate"
kindMulti = "LabeledMulti" kindModAction = "modaction"
kindMultiDescription = "LabeledMultiDescription" kindMulti = "LabeledMulti"
kindWikiPage = "wikipage" kindMultiDescription = "LabeledMultiDescription"
kindWikiPageListing = "wikipagelisting" kindWikiPage = "wikipage"
kindWikiPageSettings = "wikipagesettings" kindWikiPageListing = "wikipagelisting"
kindStyleSheet = "stylesheet" kindWikiPageSettings = "wikipagesettings"
kindStyleSheet = "stylesheet"
) )
type anchor interface { type anchor interface {
@ -91,6 +92,8 @@ func (t *thing) UnmarshalJSON(b []byte) error {
v = new(Post) v = new(Post)
case kindSubreddit: case kindSubreddit:
v = new(Subreddit) v = new(Subreddit)
case kindSubredditSettings:
v = new(SubredditSettings)
case kindLiveThread: case kindLiveThread:
v = new(LiveThread) v = new(LiveThread)
case kindLiveThreadUpdate: case kindLiveThreadUpdate:
@ -158,6 +161,11 @@ func (t *thing) Subreddit() (v *Subreddit, ok bool) {
return return
} }
func (t *thing) SubredditSettings() (v *SubredditSettings, ok bool) {
v, ok = t.Data.(*SubredditSettings)
return
}
func (t *thing) LiveThread() (v *LiveThread, ok bool) { func (t *thing) LiveThread() (v *LiveThread, ok bool) {
v, ok = t.Data.(*LiveThread) v, ok = t.Data.(*LiveThread)
return return

57
testdata/subreddit/settings.json vendored Normal file
View file

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