From 28b59f02c748ebced28b5335c688ef4104abed47 Mon Sep 17 00:00:00 2001 From: Vartan Benohanian Date: Sun, 27 Sep 2020 23:11:15 -0400 Subject: [PATCH] WIP: Create widgets Signed-off-by: Vartan Benohanian --- reddit/widget.go | 198 +++++++++++++++++++++++++++--------------- reddit/widget_test.go | 61 +++++++++++++ 2 files changed, 189 insertions(+), 70 deletions(-) diff --git a/reddit/widget.go b/reddit/widget.go index bbc075e..b4488a3 100644 --- a/reddit/widget.go +++ b/reddit/widget.go @@ -3,6 +3,7 @@ package reddit import ( "context" "encoding/json" + "errors" "fmt" "net/http" ) @@ -22,6 +23,8 @@ type Widget interface { // kind returns the widget kind. // having un unexported method on an exported interface means it cannot be implemented by a client. kind() string + // GetID returns the widget's id. + GetID() string } const ( @@ -36,6 +39,47 @@ const ( widgetKindCustom = "custom" ) +type rootWidget struct { + Data Widget +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (w *rootWidget) UnmarshalJSON(data []byte) error { + root := new(struct { + Kind string `json:"kind"` + }) + + err := json.Unmarshal(data, root) + if err != nil { + return err + } + + switch root.Kind { + case widgetKindTextArea: + w.Data = new(TextAreaWidget) + case widgetKindButton: + w.Data = new(ButtonWidget) + case widgetKindImage: + w.Data = new(ImageWidget) + case widgetKindCommunityList: + w.Data = new(CommunityListWidget) + case widgetKindMenu: + w.Data = new(MenuWidget) + case widgetKindCommunityDetails: + w.Data = new(CommunityDetailsWidget) + case widgetKindModerators: + w.Data = new(ModeratorsWidget) + case widgetKindSubredditRules: + w.Data = new(SubredditRulesWidget) + case widgetKindCustom: + w.Data = new(CustomWidget) + default: + return fmt.Errorf("unrecognized widget kind: %q", root.Kind) + } + + return json.Unmarshal(data, w.Data) +} + // WidgetList is a list of widgets. type WidgetList []Widget @@ -47,46 +91,14 @@ func (l *WidgetList) UnmarshalJSON(data []byte) error { return err } - type widgetKind struct { - Kind string `json:"kind"` - } for _, w := range widgetMap { - root := new(widgetKind) + root := new(rootWidget) err = json.Unmarshal(w, root) if err != nil { return err } - var widget Widget - switch root.Kind { - case widgetKindTextArea: - widget = new(TextAreaWidget) - case widgetKindButton: - widget = new(ButtonWidget) - case widgetKindImage: - widget = new(ImageWidget) - case widgetKindCommunityList: - widget = new(CommunityListWidget) - case widgetKindMenu: - widget = new(MenuWidget) - case widgetKindCommunityDetails: - widget = new(CommunityDetailsWidget) - case widgetKindModerators: - widget = new(ModeratorsWidget) - case widgetKindSubredditRules: - widget = new(SubredditRulesWidget) - case widgetKindCustom: - widget = new(CustomWidget) - default: - continue - } - - err = json.Unmarshal(w, widget) - if err != nil { - return err - } - - *l = append(*l, widget) + *l = append(*l, root.Data) } return nil @@ -99,6 +111,9 @@ type widget struct { Style *WidgetStyle `json:"styles,omitempty"` } +func (w *widget) kind() string { return w.Kind } +func (w *widget) GetID() string { return w.ID } + // TextAreaWidget displays a box of text in the subreddit. type TextAreaWidget struct { widget @@ -107,10 +122,6 @@ type TextAreaWidget struct { Text string `json:"text,omitempty"` } -func (w *TextAreaWidget) kind() string { - return widgetKindTextArea -} - // ButtonWidget displays up to 10 button style links with customizable font colors for each button. type ButtonWidget struct { widget @@ -120,10 +131,6 @@ type ButtonWidget struct { Buttons []*WidgetButton `json:"buttons,omitempty"` } -func (w *ButtonWidget) kind() string { - return widgetKindButton -} - // ImageWidget display a random image from up to 10 selected images. // The image can be clickable links. type ImageWidget struct { @@ -133,10 +140,6 @@ type ImageWidget struct { Images []*WidgetImageLink `json:"data,omitempty"` } -func (w *ImageWidget) kind() string { - return widgetKindImage -} - // CommunityListWidget display a list of up to 10 other communities (subreddits). type CommunityListWidget struct { widget @@ -145,10 +148,6 @@ type CommunityListWidget struct { Communities []*WidgetCommunity `json:"data,omitempty"` } -func (w *CommunityListWidget) kind() string { - return widgetKindCommunityList -} - // MenuWidget displays tabs for your community's menu. These can be direct links or submenus that // create a drop-down menu to multiple links. type MenuWidget struct { @@ -158,10 +157,6 @@ type MenuWidget struct { Links WidgetLinkList `json:"data,omitempty"` } -func (w *MenuWidget) kind() string { - return widgetKindMenu -} - // CommunityDetailsWidget displays your subscriber count, users online, and community description, // as defined in your subreddit settings. You can customize the displayed text for subscribers and // users currently viewing the community. @@ -178,10 +173,6 @@ type CommunityDetailsWidget struct { CurrentlyViewingText string `json:"currentlyViewingText,omitempty"` } -func (*CommunityDetailsWidget) kind() string { - return widgetKindCommunityDetails -} - // ModeratorsWidget displays the list of moderators of the subreddit. type ModeratorsWidget struct { widget @@ -190,10 +181,6 @@ type ModeratorsWidget struct { Total int `json:"totalMods"` } -func (*ModeratorsWidget) kind() string { - return widgetKindModerators -} - // UnmarshalJSON implements the json.Unmarshaler interface. func (w *ModeratorsWidget) UnmarshalJSON(data []byte) error { root := new(struct { @@ -229,10 +216,6 @@ type SubredditRulesWidget struct { Rules []string `json:"rules,omitempty"` } -func (*SubredditRulesWidget) kind() string { - return widgetKindSubredditRules -} - // UnmarshalJSON implements the json.Unmarshaler interface. func (w *SubredditRulesWidget) UnmarshalJSON(data []byte) error { root := new(struct { @@ -272,10 +255,6 @@ type CustomWidget struct { Images []*WidgetImage `json:"imageData,omitempty"` } -func (*CustomWidget) kind() string { - return widgetKindCustom -} - // WidgetStyle contains style information for the widget. type WidgetStyle struct { HeaderColor string `json:"headerColor,omitempty"` @@ -381,6 +360,52 @@ type WidgetButtonHoverState struct { StrokeColor string `json:"color,omitempty"` } +// WidgetCreateRequest represents a request to create a widget. +type WidgetCreateRequest interface { + requestKind() string +} + +// TextAreaWidgetCreateRequest represents a requets to create a text area widget. +type TextAreaWidgetCreateRequest struct { + Style *WidgetStyle `json:"styles,omitempty"` + // No longer than 30 characters. + Name string `json:"shortName,omitempty"` + // Raw markdown text. + Text string `json:"text,omitempty"` +} + +func (*TextAreaWidgetCreateRequest) requestKind() string { return widgetKindTextArea } + +// MarshalJSON implements the json.Marshaler interface. +func (r *TextAreaWidgetCreateRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Kind string `json:"kind"` + Style *WidgetStyle `json:"styles,omitempty"` + Name string `json:"shortName,omitempty"` + Text string `json:"text,omitempty"` + }{r.requestKind(), r.Style, r.Name, r.Text}) +} + +// CommunityListWidgetCreateRequest represents a requets to create a community list widget. +type CommunityListWidgetCreateRequest struct { + Style *WidgetStyle `json:"styles,omitempty"` + // No longer than 30 characters. + Name string `json:"shortName,omitempty"` + Communities []string `json:"data,omitempty"` +} + +func (*CommunityListWidgetCreateRequest) requestKind() string { return widgetKindCommunityList } + +// MarshalJSON implements the json.Marshaler interface. +func (r *CommunityListWidgetCreateRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Kind string `json:"kind"` + Style *WidgetStyle `json:"styles,omitempty"` + Name string `json:"shortName,omitempty"` + Communities []string `json:"data,omitempty"` + }{r.requestKind(), r.Style, r.Name, r.Communities}) +} + // Get the subreddit's widgets. func (s *WidgetService) Get(ctx context.Context, subreddit string) ([]Widget, *Response, error) { path := fmt.Sprintf("r/%s/api/widgets?progressive_images=true", subreddit) @@ -400,6 +425,27 @@ func (s *WidgetService) Get(ctx context.Context, subreddit string) ([]Widget, *R return root.Widgets, resp, nil } +// Create a widget for the subreddit. +func (s *WidgetService) Create(ctx context.Context, subreddit string, request WidgetCreateRequest) (Widget, *Response, error) { + if request == nil { + return nil, nil, errors.New("WidgetCreateRequest: cannot be nil") + } + + path := fmt.Sprintf("r/%s/api/widget", subreddit) + req, err := s.client.NewJSONRequest(http.MethodPost, path, request) + if err != nil { + return nil, nil, err + } + + root := new(rootWidget) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Data, resp, nil +} + // Delete a widget via its id. func (s *WidgetService) Delete(ctx context.Context, subreddit, id string) (*Response, error) { path := fmt.Sprintf("r/%s/api/widget/%s", subreddit, id) @@ -409,3 +455,15 @@ func (s *WidgetService) Delete(ctx context.Context, subreddit, id string) (*Resp } return s.client.Do(ctx, req, nil) } + +// Reorder the widgets in the subreddit. +// The order should contain every single widget id in the subreddit; omitting any id will result in an error. +// The id list should only contain sidebar widgets. It should exclude the community details and moderators widgets. +func (s *WidgetService) Reorder(ctx context.Context, subreddit string, ids []string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/widget_order/sidebar", subreddit) + req, err := s.client.NewJSONRequest(http.MethodPatch, path, ids) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} diff --git a/reddit/widget_test.go b/reddit/widget_test.go index 4159664..7a42e59 100644 --- a/reddit/widget_test.go +++ b/reddit/widget_test.go @@ -1,6 +1,7 @@ package reddit import ( + "encoding/json" "fmt" "net/http" "net/url" @@ -195,6 +196,49 @@ func TestWidgetService_Get(t *testing.T) { require.ElementsMatch(t, expectedWidgets, widgets) } +func TestWidgetService_Create(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/widget", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + body := new(struct { + Name string `json:"shortName"` + Text string `json:"text"` + }) + + err := json.NewDecoder(r.Body).Decode(body) + require.NoError(t, err) + require.Equal(t, "test name", body.Name) + require.Equal(t, "test text", body.Text) + + fmt.Fprint(w, `{ + "text": "test text", + "kind": "textarea", + "shortName": "test name", + "id": "id123" + }`) + }) + + _, _, err := client.Widget.Create(ctx, "testsubreddit", nil) + require.EqualError(t, err, "WidgetCreateRequest: cannot be nil") + + createdWidget, _, err := client.Widget.Create(ctx, "testsubreddit", &TextAreaWidgetCreateRequest{ + Name: "test name", + Text: "test text", + }) + require.NoError(t, err) + require.Equal(t, &TextAreaWidget{ + widget: widget{ + ID: "id123", + Kind: "textarea", + }, + Name: "test name", + Text: "test text", + }, createdWidget) +} + func TestWidgetService_Delete(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -206,3 +250,20 @@ func TestWidgetService_Delete(t *testing.T) { _, err := client.Widget.Delete(ctx, "testsubreddit", "abc123") require.NoError(t, err) } + +func TestWidgetService_Reorder(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/widget_order/sidebar", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPatch, r.Method) + + var ids []string + err := json.NewDecoder(r.Body).Decode(&ids) + require.NoError(t, err) + require.Equal(t, []string{"test1", "test2", "test3", "test4"}, ids) + }) + + _, err := client.Widget.Reorder(ctx, "testsubreddit", []string{"test1", "test2", "test3", "test4"}) + require.NoError(t, err) +}