diff --git a/reddit/things.go b/reddit/things.go index 213d695..ad1e13d 100644 --- a/reddit/things.go +++ b/reddit/things.go @@ -19,6 +19,7 @@ const ( kindMore = "more" kindModAction = "modaction" kindMulti = "LabeledMulti" + kindWikiPage = "wikipage" kindWikiPageListing = "wikipagelisting" kindWikiPageSettings = "wikipagesettings" ) @@ -96,6 +97,8 @@ func (t *thing) UnmarshalJSON(b []byte) error { v = new(trophyList) case kindKarmaList: v = new([]*SubredditKarma) + case kindWikiPage: + v = new(WikiPage) case kindWikiPageListing: v = new([]string) case kindWikiPageSettings: @@ -174,6 +177,11 @@ func (t *thing) Karma() ([]*SubredditKarma, bool) { return *v, ok } +func (t *thing) WikiPage() (v *WikiPage, ok bool) { + v, ok = t.Data.(*WikiPage) + return +} + func (t *thing) WikiPages() ([]string, bool) { v, ok := t.Data.(*[]string) if !ok { diff --git a/reddit/wiki.go b/reddit/wiki.go index 68fe76a..11bbfb9 100644 --- a/reddit/wiki.go +++ b/reddit/wiki.go @@ -19,6 +19,48 @@ type WikiService struct { client *Client } +// WikiPage is a wiki page in a subreddit. +type WikiPage struct { + Content string `json:"content_md,omitempty"` + Reason string `json:"reason,omitempty"` + MayRevise bool `json:"may_revise"` + + RevisionID string `json:"revision_id,omitempty"` + RevisionDate *Timestamp `json:"revision_date,omitempty"` + RevisionBy *User `json:"revision_by,omitempty"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (p *WikiPage) UnmarshalJSON(b []byte) error { + root := new(struct { + Content string `json:"content_md,omitempty"` + Reason string `json:"reason,omitempty"` + MayRevise bool `json:"may_revise"` + + RevisionID string `json:"revision_id,omitempty"` + RevisionDate *Timestamp `json:"revision_date,omitempty"` + RevisionBy thing `json:"revision_by,omitempty"` + }) + + err := json.Unmarshal(b, root) + if err != nil { + return err + } + + p.Content = root.Content + p.Reason = root.Reason + p.MayRevise = root.MayRevise + + p.RevisionID = root.RevisionID + p.RevisionDate = root.RevisionDate + + if user, ok := root.RevisionBy.User(); ok { + p.RevisionBy = user + } + + return nil +} + // WikiPagePermissionLevel defines who can edit a specific wiki page in a subreddit. type WikiPagePermissionLevel int @@ -71,6 +113,25 @@ func (s *WikiPageSettings) UnmarshalJSON(b []byte) error { return nil } +// Page gets a wiki page. +func (s *WikiService) Page(ctx context.Context, subreddit, page string) (*WikiPage, *Response, error) { + path := fmt.Sprintf("r/%s/wiki/%s", subreddit, page) + + 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 + } + + wikiPage, _ := root.WikiPage() + return wikiPage, resp, nil +} + // Pages retrieves a list of wiki pages in the subreddit. // Returns 403 Forbidden if the wiki is disabled. func (s *WikiService) Pages(ctx context.Context, subreddit string) ([]string, *Response, error) { @@ -87,8 +148,8 @@ func (s *WikiService) Pages(ctx context.Context, subreddit string) ([]string, *R return nil, resp, err } - pages, _ := root.WikiPages() - return pages, resp, nil + wikiPages, _ := root.WikiPages() + return wikiPages, resp, nil } // Settings gets the subreddit's wiki page's settings. @@ -168,3 +229,26 @@ func (s *WikiService) Deny(ctx context.Context, subreddit, page, username string return s.client.Do(ctx, req, nil) } + +// Discussions gets a list of discussions (posts) about the wiki page. +func (s *WikiService) Discussions(ctx context.Context, subreddit, page string, opts *ListOptions) ([]*Post, *Response, error) { + path := fmt.Sprintf("r/%s/wiki/discussions/%s", subreddit, page) + path, err := addOptions(path, opts) + if err != nil { + return nil, nil, err + } + + 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 + } + + listing, _ := root.Listing() + return listing.Posts(), resp, nil +} diff --git a/reddit/wiki_test.go b/reddit/wiki_test.go index 0edfb00..35282de 100644 --- a/reddit/wiki_test.go +++ b/reddit/wiki_test.go @@ -10,6 +10,26 @@ import ( "github.com/stretchr/testify/require" ) +var expectedWikiPage = &WikiPage{ + Content: "test reason", + Reason: "this is a reason!", + MayRevise: true, + + RevisionID: "3c4e9fab-ef2c-11ea-90b6-0e9189256887", + RevisionDate: &Timestamp{time.Date(2020, 9, 5, 3, 59, 45, 0, time.UTC)}, + RevisionBy: &User{ + ID: "164ab8", + Name: "v_95", + Created: &Timestamp{time.Date(2017, 3, 12, 4, 56, 47, 0, time.UTC)}, + + PostKarma: 691, + CommentKarma: 22235, + + HasVerifiedEmail: true, + NSFW: true, + }, +} + var expectedWikiPageSettings = &WikiPageSettings{ PermissionLevel: PermissionSubredditWikiPermissions, Listed: true, @@ -28,6 +48,50 @@ var expectedWikiPageSettings = &WikiPageSettings{ }, } +var expectedWikiPageDiscussions = []*Post{ + { + ID: "imj8g5", + FullID: "t3_imj8g5", + Created: &Timestamp{time.Date(2020, 9, 4, 16, 33, 33, 0, time.UTC)}, + Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, + + Permalink: "/r/helloworldtestt/comments/imj8g5/test/", + URL: "https://www.reddit.com/r/helloworldtestt/wiki/index", + + Title: "test", + + Likes: Bool(true), + + Score: 1, + UpvoteRatio: 1, + NumberOfComments: 0, + + SubredditName: "helloworldtestt", + SubredditNamePrefixed: "r/helloworldtestt", + SubredditID: "t5_2uquw1", + + Author: "v_95", + AuthorID: "t2_164ab8", + }, +} + +func TestWikiService_Page(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + blob, err := readFileContents("../testdata/wiki/page.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/wiki/testpage", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + wikiPage, _, err := client.Wiki.Page(ctx, "testsubreddit", "testpage") + require.NoError(t, err) + require.Equal(t, expectedWikiPage, wikiPage) +} + func TestWikiService_Pages(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -43,9 +107,9 @@ func TestWikiService_Pages(t *testing.T) { }`) }) - pages, _, err := client.Wiki.Pages(ctx, "testsubreddit") + wikiPages, _, err := client.Wiki.Pages(ctx, "testsubreddit") require.NoError(t, err) - require.Equal(t, []string{"faq", "index"}, pages) + require.Equal(t, []string{"faq", "index"}, wikiPages) } func TestWikiService_Settings(t *testing.T) { @@ -60,9 +124,9 @@ func TestWikiService_Settings(t *testing.T) { fmt.Fprint(w, blob) }) - settings, _, err := client.Wiki.Settings(ctx, "testsubreddit", "testpage") + wikiPageSettings, _, err := client.Wiki.Settings(ctx, "testsubreddit", "testpage") require.NoError(t, err) - require.Equal(t, expectedWikiPageSettings, settings) + require.Equal(t, expectedWikiPageSettings, wikiPageSettings) } func TestWikiService_UpdateSettings(t *testing.T) { @@ -89,12 +153,12 @@ func TestWikiService_UpdateSettings(t *testing.T) { _, _, err = client.Wiki.UpdateSettings(ctx, "testsubreddit", "testpage", nil) require.EqualError(t, err, "updateRequest: cannot be nil") - settings, _, err := client.Wiki.UpdateSettings(ctx, "testsubreddit", "testpage", &WikiPageSettingsUpdateRequest{ + wikiPageSettings, _, err := client.Wiki.UpdateSettings(ctx, "testsubreddit", "testpage", &WikiPageSettingsUpdateRequest{ Listed: Bool(false), PermissionLevel: PermissionApprovedContributorsOnly, }) require.NoError(t, err) - require.Equal(t, expectedWikiPageSettings, settings) + require.Equal(t, expectedWikiPageSettings, wikiPageSettings) } func TestWikiService_Allow(t *testing.T) { @@ -136,3 +200,20 @@ func TestWikiService_Deny(t *testing.T) { _, err := client.Wiki.Deny(ctx, "testsubreddit", "testpage", "testusername") require.NoError(t, err) } + +func TestWikiService_Discussions(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + blob, err := readFileContents("../testdata/wiki/discussions.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/wiki/discussions/testpage", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + wikiPageDiscussions, _, err := client.Wiki.Discussions(ctx, "testsubreddit", "testpage", nil) + require.NoError(t, err) + require.Equal(t, expectedWikiPageDiscussions, wikiPageDiscussions) +} diff --git a/testdata/wiki/discussions.json b/testdata/wiki/discussions.json new file mode 100644 index 0000000..9997c9f --- /dev/null +++ b/testdata/wiki/discussions.json @@ -0,0 +1,136 @@ +{ + "kind": "Listing", + "data": { + "modhash": null, + "dist": 1, + "children": [ + { + "kind": "t3", + "data": { + "author_flair_background_color": "transparent", + "approved_at_utc": null, + "subreddit": "helloworldtestt", + "selftext": "", + "author_fullname": "t2_164ab8", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "test", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/helloworldtestt", + "hidden": false, + "pwls": null, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_imj8g5", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 1.0, + "ignore_reports": false, + "subreddit_type": "private", + "ups": 1, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": "3ec5dbec-d986-11ea-ac0f-0eaf0635e607", + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": true, + "score": 1, + "approved_by": null, + "author_premium": false, + "thumbnail": "default", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [ + { + "e": "text", + "t": "test " + }, + { + "a": ":karma:", + "e": "emoji", + "u": "https://emoji.redditmedia.com/dgnf69ls1guz_t5_3nqvj/karma" + } + ], + "gildings": {}, + "content_categories": null, + "is_self": false, + "mod_note": null, + "created": 1599266013.0, + "link_flair_type": "text", + "wls": null, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "richtext", + "domain": "reddit.com", + "allow_live_comments": false, + "selftext_html": null, + "likes": true, + "suggested_sort": null, + "banned_at_utc": null, + "url_overridden_by_dest": "https://www.reddit.com/r/helloworldtestt/wiki/index", + "view_count": null, + "archived": false, + "no_follow": false, + "spam": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "removed": false, + "spoiler": false, + "locked": false, + "author_flair_text": "test :karma:", + "treatment_tags": [], + "rte_mode": "markdown", + "visited": false, + "removed_by": null, + "num_reports": 0, + "distinguished": null, + "subreddit_id": "t5_2uquw1", + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "imj8g5", + "is_robot_indexable": true, + "report_reasons": [], + "author": "v_95", + "discussion_type": null, + "num_comments": 0, + "send_replies": true, + "whitelist_status": null, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "approved": false, + "author_flair_text_color": "dark", + "permalink": "/r/helloworldtestt/comments/imj8g5/test/", + "parent_whitelist_status": null, + "stickied": false, + "url": "https://www.reddit.com/r/helloworldtestt/wiki/index", + "subreddit_subscribers": 2, + "created_utc": 1599237213.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + } + ], + "after": null, + "before": null + } +} diff --git a/testdata/wiki/page.json b/testdata/wiki/page.json new file mode 100644 index 0000000..32b6690 --- /dev/null +++ b/testdata/wiki/page.json @@ -0,0 +1,182 @@ +{ + "kind": "wikipage", + "data": { + "content_md": "test reason", + "may_revise": true, + "reason": "this is a reason!", + "revision_date": 1599278385, + "revision_by": { + "kind": "t2", + "data": { + "is_employee": false, + "has_visited_new_profile": false, + "is_friend": false, + "pref_no_profanity": false, + "has_external_account": false, + "pref_geopopular": "GLOBAL", + "pref_show_trending": true, + "subreddit": { + "default_set": true, + "user_is_contributor": false, + "banner_img": "", + "restrict_posting": true, + "user_is_banned": false, + "free_form_reports": true, + "community_icon": null, + "show_media": true, + "icon_color": "#94E044", + "user_is_muted": false, + "display_name": "u_v_95", + "header_img": null, + "title": "", + "coins": 0, + "previous_names": [], + "over_18": false, + "icon_size": [256, 256], + "primary_color": "", + "icon_img": "https://www.redditstatic.com/avatars/avatar_default_01_94E044.png", + "description": "", + "submit_link_label": "", + "header_size": null, + "restrict_commenting": false, + "subscribers": 1, + "submit_text_label": "", + "is_default_icon": true, + "link_flair_position": "", + "display_name_prefixed": "u/v_95", + "key_color": "", + "name": "t5_17a8op", + "is_default_banner": true, + "url": "/user/v_95/", + "quarantine": false, + "banner_size": null, + "user_is_moderator": true, + "public_description": "", + "link_flair_enabled": false, + "disable_contributor_requests": false, + "subreddit_type": "user", + "user_is_subscriber": false + }, + "is_sponsor": false, + "gold_expiration": null, + "has_gold_subscription": false, + "num_friends": 29, + "features": { + "mod_service_mute_writes": true, + "promoted_trend_blanks": true, + "show_amp_link": true, + "report_service_handles_report_writes_to_db_for_helpdesk_reports": true, + "report_service_handles_self_harm_reports": true, + "report_service_handles_report_writes_to_db_for_modmail_reports": true, + "chat": true, + "mweb_link_tab": { + "owner": "growth", + "variant": "treatment_1", + "experiment_id": 404 + }, + "reports_double_write_to_report_service_for_spam": true, + "is_email_permission_required": true, + "reports_double_write_to_report_service_for_modmail_reports": true, + "mod_awards": true, + "econ_wallet_service": true, + "mweb_xpromo_revamp_v2": { + "owner": "growth", + "variant": "treatment_6", + "experiment_id": 457 + }, + "awards_on_streams": true, + "report_service_handles_accept_report": true, + "mweb_xpromo_modal_listing_click_daily_dismissible_ios": true, + "reports_double_write_to_report_service_for_som": true, + "chat_subreddit": true, + "reports_double_write_to_report_service_for_users": true, + "modlog_copyright_removal": true, + "report_service_handles_report_writes_to_db_for_users": true, + "do_not_track": true, + "reports_double_write_to_report_service_for_helpdesk_reports": true, + "report_service_handles_report_writes_to_db_for_spam": true, + "reports_double_write_to_report_service_for_sendbird_chats": true, + "mod_service_mute_reads": true, + "mweb_xpromo_interstitial_comments_ios": true, + "mweb_xpromo_modal_listing_click_daily_dismissible_android": true, + "chat_user_settings": true, + "premium_subscriptions_table": true, + "reports_double_write_to_report_service": true, + "mweb_xpromo_interstitial_comments_android": true, + "report_service_handles_report_writes_to_db_for_awards": true, + "mweb_nsfw_xpromo": { + "owner": "growth", + "variant": "control_2", + "experiment_id": 361 + }, + "noreferrer_to_noopener": true, + "reports_double_write_to_report_service_for_awards": true, + "mweb_sharing_web_share_api": { + "owner": "growth", + "variant": "control_1", + "experiment_id": 314 + }, + "mweb_xpromo_revamp_v3": { + "owner": "growth", + "variant": "treatment_2", + "experiment_id": 480 + }, + "chat_group_rollout": true, + "resized_styles_images": true, + "spez_modal": true, + "mweb_sharing_clipboard": { + "owner": "growth", + "variant": "control_2", + "experiment_id": 315 + }, + "expensive_coins_package": true, + "report_service_handles_report_writes_to_db_for_som": true + }, + "can_edit_name": false, + "verified": true, + "new_modmail_exists": true, + "pref_autoplay": true, + "coins": 0, + "has_paypal_subscription": false, + "has_subscribed_to_premium": false, + "id": "164ab8", + "has_stripe_subscription": false, + "can_create_subreddit": true, + "over_18": true, + "is_gold": false, + "is_mod": true, + "suspension_expiration_utc": null, + "has_verified_email": true, + "is_suspended": false, + "pref_video_autoplay": true, + "in_chat": true, + "has_android_subscription": false, + "in_redesign_beta": false, + "icon_img": "https://www.redditstatic.com/avatars/avatar_default_01_94E044.png", + "has_mod_mail": false, + "pref_nightmode": true, + "hide_from_robots": false, + "password_set": true, + "modhash": null, + "link_karma": 691, + "force_password_reset": false, + "inbox_count": 0, + "pref_top_karma_subreddits": false, + "has_mail": false, + "pref_show_snoovatar": false, + "name": "v_95", + "pref_clickgadget": 5, + "created": 1489323407.0, + "gold_creddits": 0, + "created_utc": 1489294607.0, + "has_ios_subscription": false, + "pref_show_twitter": false, + "in_beta": false, + "comment_karma": 22235, + "has_subscribed": true + } + }, + "revision_id": "3c4e9fab-ef2c-11ea-90b6-0e9189256887", + "content_html": "<!-- SC_OFF --><div class=\"md wiki\"><p>test reason</p>\n</div><!-- SC_ON -->" + } +}