diff --git a/reddit/subreddit.go b/reddit/subreddit.go index 6e7d3f1..eff1379 100644 --- a/reddit/subreddit.go +++ b/reddit/subreddit.go @@ -1,6 +1,7 @@ package reddit import ( + "bytes" "context" "encoding/json" "errors" @@ -113,6 +114,20 @@ type SubredditTrafficStats struct { Subscribers int `json:"subscribers"` } +// SubredditImage is an image part of the image set of a subreddit. +type SubredditImage struct { + Name string `json:"name"` + Link string `json:"link"` + URL string `json:"url"` +} + +// SubredditStyleSheet contains the subreddit's styling information. +type SubredditStyleSheet struct { + SubredditID string `json:"subreddit_id"` + Images []*SubredditImage `json:"images"` + StyleSheet string `json:"stylesheet"` +} + // UnmarshalJSON implements the json.Unmarshaler interface. func (s *SubredditTrafficStats) UnmarshalJSON(b []byte) error { var data [4]int @@ -816,3 +831,126 @@ func (s *SubredditService) Traffic(ctx context.Context, subreddit string) ([]*Su return root.Day, root.Hour, root.Month, resp, nil } + +// StyleSheet returns the subreddit's style sheet, as well as some information about images. +func (s *SubredditService) StyleSheet(ctx context.Context, subreddit string) (*SubredditStyleSheet, *Response, error) { + path := fmt.Sprintf("r/%s/about/stylesheet", subreddit) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(thing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + styleSheet, _ := root.StyleSheet() + return styleSheet, resp, nil +} + +// StyleSheetRaw returns the subreddit's style sheet with all comments and newlines stripped. +func (s *SubredditService) StyleSheetRaw(ctx context.Context, subreddit string) (string, *Response, error) { + path := fmt.Sprintf("r/%s/stylesheet", subreddit) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return "", nil, err + } + + buf := new(bytes.Buffer) + resp, err := s.client.Do(ctx, req, buf) + if err != nil { + return "", resp, err + } + + return buf.String(), resp, nil +} + +// UpdateStyleSheet updates the style sheet of the subreddit. +// Providing a reason is optional. +func (s *SubredditService) UpdateStyleSheet(ctx context.Context, subreddit, styleSheet, reason string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/subreddit_stylesheet", subreddit) + + form := url.Values{} + form.Set("api_type", "json") + form.Set("op", "save") + form.Set("stylesheet_contents", styleSheet) + if reason != "" { + form.Set("reason", reason) + } + + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// RemoveHeaderImage removes the subreddit's custom header image. +// The call succeeds even if there's no header image. +func (s *SubredditService) RemoveHeaderImage(ctx context.Context, subreddit string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/delete_sr_header", subreddit) + + form := url.Values{} + form.Set("api_type", "json") + + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// RemoveMobileIcon removes the subreddit's custom mobile icon. +// The call succeeds even if there's no mobile icon. +func (s *SubredditService) RemoveMobileIcon(ctx context.Context, subreddit string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/delete_sr_icon", subreddit) + + form := url.Values{} + form.Set("api_type", "json") + + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// RemoveMobileBanner removes the subreddit's custom mobile banner. +// The call succeeds even if there's no mobile banner. +func (s *SubredditService) RemoveMobileBanner(ctx context.Context, subreddit string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/delete_sr_banner", subreddit) + + form := url.Values{} + form.Set("api_type", "json") + + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// RemoveImage removes an image from the subreddit's custom image set. +// The call succeeds even if the named image does not exist. +func (s *SubredditService) RemoveImage(ctx context.Context, subreddit, imageName string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/delete_sr_img", subreddit) + + form := url.Values{} + form.Set("api_type", "json") + form.Set("img_name", imageName) + + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/reddit/subreddit_test.go b/reddit/subreddit_test.go index 9e024b1..3c86156 100644 --- a/reddit/subreddit_test.go +++ b/reddit/subreddit_test.go @@ -316,6 +316,24 @@ var expectedMonthTraffic = []*SubredditTrafficStats{ {&Timestamp{time.Date(2020, 7, 1, 0, 0, 0, 0, time.UTC)}, 4, 264, 0}, } +var expectedStyleSheet = &SubredditStyleSheet{ + SubredditID: "t5_2rc7j", + Images: []*SubredditImage{ + { + Name: "gopher", + Link: "url(%%gopher%%)", + URL: "http://b.thumbs.redditmedia.com/q5Wb6hTPm2Bd6Of9_xMrTu4n5qgAljJNqtnbE3Tging.png", + }, + }, + StyleSheet: `.flair-gopher { + background: url(%%gopher%%) no-repeat; + border: 0; + padding: 0; + width: 16px; + height: 16px; +}`, +} + func TestSubredditService_HotPosts(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -1155,3 +1173,133 @@ func TestSubredditService_Traffic(t *testing.T) { require.Equal(t, expectedHourTraffic, hourTraffic) require.Equal(t, expectedMonthTraffic, monthTraffic) } + +func TestSubredditService_StyleSheet(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + blob, err := readFileContents("../testdata/subreddit/stylesheet.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/about/stylesheet", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + styleSheet, _, err := client.Subreddit.StyleSheet(ctx, "testsubreddit") + require.NoError(t, err) + require.Equal(t, expectedStyleSheet, styleSheet) +} + +func TestSubredditService_StyleSheetRaw(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/stylesheet", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, "* { box-sizing: border-box; }") + }) + + styleSheet, _, err := client.Subreddit.StyleSheetRaw(ctx, "testsubreddit") + require.NoError(t, err) + require.Equal(t, "* { box-sizing: border-box; }", styleSheet) +} + +func TestSubredditService_UpdateStyleSheet(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/subreddit_stylesheet", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + form.Set("op", "save") + form.Set("stylesheet_contents", "* { box-sizing: border-box; }") + form.Set("reason", "testreason") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.Subreddit.UpdateStyleSheet(ctx, "testsubreddit", "* { box-sizing: border-box; }", "testreason") + require.NoError(t, err) +} + +func TestSubredditService_RemoveHeaderImage(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/delete_sr_header", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.Subreddit.RemoveHeaderImage(ctx, "testsubreddit") + require.NoError(t, err) +} + +func TestSubredditService_RemoveMobileIcon(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/delete_sr_icon", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.Subreddit.RemoveMobileIcon(ctx, "testsubreddit") + require.NoError(t, err) +} + +func TestSubredditService_RemoveMobileBanner(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/delete_sr_banner", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.Subreddit.RemoveMobileBanner(ctx, "testsubreddit") + require.NoError(t, err) +} + +func TestSubredditService_RemoveImage(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/delete_sr_img", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + form.Set("img_name", "testimage") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.Subreddit.RemoveImage(ctx, "testsubreddit", "testimage") + require.NoError(t, err) +} diff --git a/reddit/things.go b/reddit/things.go index 0f6a83c..b5b6977 100644 --- a/reddit/things.go +++ b/reddit/things.go @@ -23,6 +23,7 @@ const ( kindWikiPage = "wikipage" kindWikiPageListing = "wikipagelisting" kindWikiPageSettings = "wikipagesettings" + kindStyleSheet = "stylesheet" ) type anchor interface { @@ -106,6 +107,8 @@ func (t *thing) UnmarshalJSON(b []byte) error { v = new([]string) case kindWikiPageSettings: v = new(WikiPageSettings) + case kindStyleSheet: + v = new(SubredditStyleSheet) default: return fmt.Errorf("unrecognized kind: %q", t.Kind) } @@ -206,6 +209,11 @@ func (t *thing) WikiPageSettings() (v *WikiPageSettings, ok bool) { return } +func (t *thing) StyleSheet() (v *SubredditStyleSheet, ok bool) { + v, ok = t.Data.(*SubredditStyleSheet) + return +} + // listing is a list of things coming from the Reddit API. // It also contains the after/before anchors useful for subsequent requests. type listing struct { diff --git a/testdata/subreddit/stylesheet.json b/testdata/subreddit/stylesheet.json new file mode 100644 index 0000000..19dc702 --- /dev/null +++ b/testdata/subreddit/stylesheet.json @@ -0,0 +1,14 @@ +{ + "kind": "stylesheet", + "data": { + "images": [ + { + "url": "http://b.thumbs.redditmedia.com/q5Wb6hTPm2Bd6Of9_xMrTu4n5qgAljJNqtnbE3Tging.png", + "link": "url(%%gopher%%)", + "name": "gopher" + } + ], + "subreddit_id": "t5_2rc7j", + "stylesheet": ".flair-gopher {\n background: url(%%gopher%%) no-repeat;\n border: 0;\n padding: 0;\n width: 16px;\n height: 16px;\n}" + } +}