diff --git a/reddit/wiki.go b/reddit/wiki.go index 11bbfb9..6980f35 100644 --- a/reddit/wiki.go +++ b/reddit/wiki.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/google/go-querystring/query" ) @@ -61,6 +62,15 @@ func (p *WikiPage) UnmarshalJSON(b []byte) error { return nil } +// WikiPageEditRequest represents a request to edit a wiki page in a subreddit. +type WikiPageEditRequest struct { + Subreddit string `url:"-"` + Page string `url:"page"` + Content string `url:"content"` + // Optional, up to 256 characters long. + Reason string `url:"reason,omitempty"` +} + // WikiPagePermissionLevel defines who can edit a specific wiki page in a subreddit. type WikiPagePermissionLevel int @@ -113,10 +123,80 @@ func (s *WikiPageSettings) UnmarshalJSON(b []byte) error { return nil } +type wikiPageRevisionListing struct { + Data struct { + Revisions []*WikiPageRevision `json:"children"` + After string `json:"after"` + Before string `json:"before"` + } `json:"data"` +} + +func (l *wikiPageRevisionListing) After() string { + return l.Data.After +} + +func (l *wikiPageRevisionListing) Before() string { + return l.Data.Before +} + +// WikiPageRevision is a revision of a wiki page. +type WikiPageRevision struct { + ID string `json:"id,omitempty"` + Page string `json:"page,omitempty"` + Created *Timestamp `json:"timestamp,omitempty"` + Reason string `json:"reason,omitempty"` + Hidden bool `json:"revision_hidden"` + Author *User `json:"author,omitempty"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (r *WikiPageRevision) UnmarshalJSON(b []byte) error { + root := new(struct { + ID string `json:"id,omitempty"` + Page string `json:"page,omitempty"` + Created *Timestamp `json:"timestamp,omitempty"` + Reason string `json:"reason,omitempty"` + Hidden bool `json:"revision_hidden"` + Author thing `json:"author,omitempty"` + }) + + err := json.Unmarshal(b, root) + if err != nil { + return err + } + + r.ID = root.ID + r.Page = root.Page + r.Created = root.Created + r.Reason = root.Reason + r.Hidden = root.Hidden + + if user, ok := root.Author.User(); ok { + r.Author = user + } + + return nil +} + // Page gets a wiki page. func (s *WikiService) Page(ctx context.Context, subreddit, page string) (*WikiPage, *Response, error) { + return s.PageRevision(ctx, subreddit, page, "") +} + +// PageRevision gets a wiki page at the version it was at the revisionID provided. +// If revisionID is an empty string, it will get the most recent version. +func (s *WikiService) PageRevision(ctx context.Context, subreddit, page, revisionID string) (*WikiPage, *Response, error) { path := fmt.Sprintf("r/%s/wiki/%s", subreddit, page) + params := struct { + RevisionID string `url:"v,omitempty"` + }{revisionID} + + path, err := addOptions(path, params) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(http.MethodGet, path, nil) if err != nil { return nil, nil, err @@ -132,7 +212,7 @@ func (s *WikiService) Page(ctx context.Context, subreddit, page string) (*WikiPa return wikiPage, resp, nil } -// Pages retrieves a list of wiki pages in the subreddit. +// Pages gets 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) { path := fmt.Sprintf("r/%s/wiki/pages", subreddit) @@ -152,6 +232,42 @@ func (s *WikiService) Pages(ctx context.Context, subreddit string) ([]string, *R return wikiPages, resp, nil } +// Edit a wiki page. +func (s *WikiService) Edit(ctx context.Context, editRequest *WikiPageEditRequest) (*Response, error) { + if editRequest == nil { + return nil, errors.New("editRequest: cannot be nil") + } + + form, err := query.Values(editRequest) + if err != nil { + return nil, err + } + + path := fmt.Sprintf("r/%s/api/wiki/edit", editRequest.Subreddit) + req, err := s.client.NewRequestWithForm(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// Revert a wiki page to a specific revision. +func (s *WikiService) Revert(ctx context.Context, subreddit, page, revisionID string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/wiki/revert", subreddit) + + form := url.Values{} + form.Set("page", page) + form.Set("revision", revisionID) + + req, err := s.client.NewRequestWithForm(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + // Settings gets the subreddit's wiki page's settings. func (s *WikiService) Settings(ctx context.Context, subreddit, page string) (*WikiPageSettings, *Response, error) { path := fmt.Sprintf("r/%s/wiki/settings/%s", subreddit, page) @@ -198,6 +314,100 @@ func (s *WikiService) UpdateSettings(ctx context.Context, subreddit, page string return settings, resp, 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 +} + +// ToggleVisibility toggles the public visibility of a wiki page revision. +// The returned bool is whether the page was set to hidden or not. +func (s *WikiService) ToggleVisibility(ctx context.Context, subreddit, page, revisionID string) (bool, *Response, error) { + path := fmt.Sprintf("r/%s/api/wiki/hide", subreddit) + + form := url.Values{} + form.Set("page", page) + form.Set("revision", revisionID) + + req, err := s.client.NewRequestWithForm(http.MethodPost, path, form) + if err != nil { + return false, nil, err + } + + root := new(struct { + Status bool `json:"status"` + }) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return false, resp, err + } + + return root.Status, resp, nil +} + +func (s *WikiService) revisions(ctx context.Context, subreddit, page string, opts *ListOptions) ([]*WikiPageRevision, *Response, error) { + path := fmt.Sprintf("r/%s/wiki/revisions", subreddit) + if page != "" { + path += "/" + page + } + + if opts != nil { + idPrefix := "WikiRevision_" + if opts.After != "" && !strings.HasPrefix(opts.After, idPrefix) { + opts.After = idPrefix + opts.After + } + if opts.Before != "" && !strings.HasPrefix(opts.Before, idPrefix) { + opts.Before = idPrefix + opts.Before + } + } + + 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(wikiPageRevisionListing) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Data.Revisions, resp, nil +} + +// Revisions gets revisions of all pages in the wiki. +func (s *WikiService) Revisions(ctx context.Context, subreddit string, opts *ListOptions) ([]*WikiPageRevision, *Response, error) { + return s.revisions(ctx, subreddit, "", opts) +} + +// RevisionsPage gets revisions of the specific wiki page. +// If page is an empty string, it gets revisions of all pages in the wiki. +func (s *WikiService) RevisionsPage(ctx context.Context, subreddit, page string, opts *ListOptions) ([]*WikiPageRevision, *Response, error) { + return s.revisions(ctx, subreddit, page, opts) +} + // Allow the user to edit the specified wiki page in the subreddit. func (s *WikiService) Allow(ctx context.Context, subreddit, page, username string) (*Response, error) { path := fmt.Sprintf("r/%s/api/wiki/alloweditor/add", subreddit) @@ -229,26 +439,3 @@ 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 35282de..57b5a30 100644 --- a/reddit/wiki_test.go +++ b/reddit/wiki_test.go @@ -75,6 +75,45 @@ var expectedWikiPageDiscussions = []*Post{ }, } +var expectedWikiPageRevisions = []*WikiPageRevision{ + { + ID: "3b28c343-effb-11ea-859e-0efe313b2cd3", + Page: "index", + Created: &Timestamp{time.Date(2020, 9, 6, 4, 41, 29, 0, time.UTC)}, + Reason: "reverted back 1 day", + Hidden: false, + Author: &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, + }, + }, + { + ID: "d217592d-effa-11ea-90af-0e913d9ded0b", + Page: "index", + Created: &Timestamp{time.Date(2020, 9, 6, 4, 38, 33, 0, time.UTC)}, + Reason: "reverted back 10 minutes", + Hidden: false, + Author: &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, + }, + }, +} + func TestWikiService_Page(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -92,6 +131,31 @@ func TestWikiService_Page(t *testing.T) { require.Equal(t, expectedWikiPage, wikiPage) } +func TestWikiService_PageRevision(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) + + form := url.Values{} + form.Set("v", "testrevision") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.Form) + + fmt.Fprint(w, blob) + }) + + wikiPage, _, err := client.Wiki.PageRevision(ctx, "testsubreddit", "testpage", "testrevision") + require.NoError(t, err) + require.Equal(t, expectedWikiPage, wikiPage) +} + func TestWikiService_Pages(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -112,6 +176,55 @@ func TestWikiService_Pages(t *testing.T) { require.Equal(t, []string{"faq", "index"}, wikiPages) } +func TestWikiService_Edit(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/wiki/edit", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("page", "testpage") + form.Set("content", "testcontent") + form.Set("reason", "testreason") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.Wiki.Edit(ctx, nil) + require.EqualError(t, err, "editRequest: cannot be nil") + + _, err = client.Wiki.Edit(ctx, &WikiPageEditRequest{ + Subreddit: "testsubreddit", + Page: "testpage", + Content: "testcontent", + Reason: "testreason", + }) + require.NoError(t, err) +} + +func TestWikiService_Revert(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/wiki/revert", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("page", "testpage") + form.Set("revision", "testrevision") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.Wiki.Revert(ctx, "testsubreddit", "testpage", "testrevision") + require.NoError(t, err) +} + func TestWikiService_Settings(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -161,6 +274,94 @@ func TestWikiService_UpdateSettings(t *testing.T) { require.Equal(t, expectedWikiPageSettings, wikiPageSettings) } +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) +} + +func TestWikiService_ToggleVisibility(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/wiki/hide", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("page", "testpage") + form.Set("revision", "testrevision") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + + fmt.Fprint(w, `{"status": true}`) + }) + + hidden, _, err := client.Wiki.ToggleVisibility(ctx, "testsubreddit", "testpage", "testrevision") + require.NoError(t, err) + require.True(t, hidden) +} + +func TestWikiService_Revisions(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + blob, err := readFileContents("../testdata/wiki/revisions.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/wiki/revisions", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + wikiPageRevisions, _, err := client.Wiki.Revisions(ctx, "testsubreddit", nil) + require.NoError(t, err) + require.Equal(t, expectedWikiPageRevisions, wikiPageRevisions) +} + +func TestWikiService_RevisionsPage(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + blob, err := readFileContents("../testdata/wiki/revisions.json") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/wiki/revisions/testpage", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + + form := url.Values{} + form.Set("limit", "10") + form.Set("after", "WikiRevision_wikiId1") + form.Set("before", "WikiRevision_wikiId2") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.Form) + + fmt.Fprint(w, blob) + }) + + wikiPageRevisions, _, err := client.Wiki.RevisionsPage(ctx, "testsubreddit", "testpage", &ListOptions{ + Limit: 10, + After: "wikiId1", + Before: "wikiId2", + }) + require.NoError(t, err) + require.Equal(t, expectedWikiPageRevisions, wikiPageRevisions) +} + func TestWikiService_Allow(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -200,20 +401,3 @@ 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/revisions.json b/testdata/wiki/revisions.json new file mode 100644 index 0000000..b15832c --- /dev/null +++ b/testdata/wiki/revisions.json @@ -0,0 +1,367 @@ +{ + "kind": "Listing", + "data": { + "modhash": null, + "dist": null, + "children": [ + { + "author": { + "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 + } + }, + "timestamp": 1599367289, + "page": "index", + "revision_hidden": false, + "reason": "reverted back 1 day", + "id": "3b28c343-effb-11ea-859e-0efe313b2cd3" + }, + { + "author": { + "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 + } + }, + "timestamp": 1599367113, + "page": "index", + "revision_hidden": false, + "reason": "reverted back 10 minutes", + "id": "d217592d-effa-11ea-90af-0e913d9ded0b" + } + ], + "after": null, + "before": null + } +}