From 3dbbe92603b0e4f59ab7a1634563d0ebe5d01d47 Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Wed, 14 Jul 2021 15:17:59 -0400 Subject: [PATCH 01/11] parse MeResponse --- go.mod | 1 + internal/reddit/types.go | 11 ++ internal/reddit/types_test.go | 187 ++++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 internal/reddit/types_test.go diff --git a/go.mod b/go.mod index de07c27..61e8014 100644 --- a/go.mod +++ b/go.mod @@ -16,5 +16,6 @@ require ( github.com/sideshow/apns2 v0.20.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.2.1 + github.com/stretchr/testify v1.7.0 github.com/valyala/fastjson v1.6.3 ) diff --git a/internal/reddit/types.go b/internal/reddit/types.go index e2754a7..6e27961 100644 --- a/internal/reddit/types.go +++ b/internal/reddit/types.go @@ -3,6 +3,8 @@ package reddit import ( "fmt" "strings" + + "github.com/valyala/fastjson" ) type Error struct { @@ -59,3 +61,12 @@ type MeResponse struct { func (mr *MeResponse) NormalizedUsername() string { return strings.ToLower(mr.Name) } + +func NewMeResponse(val *fastjson.Value) *MeResponse { + mr := &MeResponse{} + + mr.ID = string(val.GetStringBytes("id")) + mr.Name = string(val.GetStringBytes("name")) + + return mr +} diff --git a/internal/reddit/types_test.go b/internal/reddit/types_test.go new file mode 100644 index 0000000..1522f39 --- /dev/null +++ b/internal/reddit/types_test.go @@ -0,0 +1,187 @@ +package reddit + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/valyala/fastjson" +) + +func TestMeResponseParsing(t *testing.T) { + bb := []byte(` +{ + "is_employee": false, + "seen_layout_switch": true, + "has_visited_new_profile": true, + "pref_no_profanity": false, + "has_external_account": false, + "pref_geopopular": "GLOBAL", + "seen_redesign_modal": true, + "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": "#EA0027", + "user_is_muted": false, + "display_name": "u_changelog", + "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_19_EA0027.png", + "description": "", + "submit_link_label": "", + "header_size": null, + "restrict_commenting": false, + "subscribers": 3, + "submit_text_label": "", + "is_default_icon": true, + "link_flair_position": "", + "display_name_prefixed": "u/changelog", + "key_color": "", + "name": "t5_c30sw", + "is_default_banner": true, + "url": "/user/changelog/", + "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 + }, + "pref_show_presence": false, + "snoovatar_img": "", + "snoovatar_size": null, + "gold_expiration": null, + "has_gold_subscription": false, + "is_sponsor": false, + "num_friends": 7, + "features": { + "mod_service_mute_writes": true, + "promoted_trend_blanks": true, + "show_amp_link": true, + "chat": true, + "mweb_link_tab": { + "owner": "growth", + "variant": "control_1", + "experiment_id": 404 + }, + "is_email_permission_required": true, + "mod_awards": true, + "mweb_xpromo_revamp_v3": { + "owner": "growth", + "variant": "treatment_4", + "experiment_id": 480 + }, + "chat_subreddit": true, + "awards_on_streams": true, + "webhook_config": true, + "mweb_xpromo_modal_listing_click_daily_dismissible_ios": true, + "live_orangereds": true, + "cookie_consent_banner": true, + "modlog_copyright_removal": true, + "do_not_track": true, + "mod_service_mute_reads": true, + "chat_user_settings": true, + "use_pref_account_deployment": true, + "mweb_xpromo_interstitial_comments_ios": true, + "noreferrer_to_noopener": true, + "premium_subscriptions_table": true, + "mweb_xpromo_interstitial_comments_android": true, + "mweb_nsfw_xpromo": { + "owner": "growth", + "variant": "control_2", + "experiment_id": 361 + }, + "mweb_xpromo_modal_listing_click_daily_dismissible_android": true, + "mweb_sharing_web_share_api": { + "owner": "growth", + "variant": "control_1", + "experiment_id": 314 + }, + "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 + }, + "can_edit_name": false, + "verified": true, + "new_modmail_exists": null, + "pref_autoplay": false, + "coins": 0, + "has_paypal_subscription": false, + "has_subscribed_to_premium": false, + "id": "1ia22", + "has_stripe_subscription": false, + "oauth_client_id": "5JHxEu-4wnFfBA", + "can_create_subreddit": true, + "over_18": true, + "is_gold": false, + "is_mod": false, + "awarder_karma": 0, + "suspension_expiration_utc": null, + "has_verified_email": true, + "is_suspended": false, + "pref_video_autoplay": false, + "in_chat": true, + "has_android_subscription": false, + "in_redesign_beta": true, + "icon_img": "https://www.redditstatic.com/avatars/avatar_default_19_EA0027.png", + "has_mod_mail": false, + "pref_nightmode": true, + "awardee_karma": 10, + "hide_from_robots": true, + "password_set": true, + "link_karma": 2058, + "force_password_reset": false, + "total_karma": 3445, + "seen_give_award_tooltip": false, + "inbox_count": 0, + "seen_premium_adblock_modal": false, + "pref_top_karma_subreddits": false, + "has_mail": false, + "pref_show_snoovatar": false, + "name": "changelog", + "pref_clickgadget": 5, + "created": 1176750666.0, + "gold_creddits": 0, + "created_utc": 1176721866.0, + "has_ios_subscription": false, + "pref_show_twitter": false, + "in_beta": true, + "comment_karma": 1377, + "has_subscribed": true, + "linked_identities": [], + "seen_subreddit_chat_ftux": false +} + `) + + parser := &fastjson.Parser{} + val, err := parser.ParseBytes(bb) + assert.NoError(t, err) + + me := NewMeResponse(val) + assert.NotNil(t, me) + + assert.Equal(t, "1ia22", me.ID) + assert.Equal(t, "changelog", me.Name) +} From 1555c9bcf2de606153a9fc2e26a24b5441fbc466 Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Wed, 14 Jul 2021 15:22:49 -0400 Subject: [PATCH 02/11] add git action --- .github/workflows/test.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3efb7dc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,16 @@ +on: [push, pull_request] +name: Unit Tests +jobs: + test: + name: "go ${{ matrix.go-version }}" + matrix: + go-version: [1.16.5] + + steps: + - uses: actions/checkout@v2 + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Test + run: go test ./... From da230b3110278d1f88e23949abf420c956e0fca9 Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Wed, 14 Jul 2021 15:25:26 -0400 Subject: [PATCH 03/11] fix workflow --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3efb7dc..f7d6ead 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,10 +2,11 @@ on: [push, pull_request] name: Unit Tests jobs: test: - name: "go ${{ matrix.go-version }}" + name: "go ${{ matrix.go-version }} (${{ matrix.platform }})" matrix: go-version: [1.16.5] - + platform: [ubuntu-latest] + runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - name: Setup Go From b43f9ec8ed5d6d63e9ed560a75923d2cf86b8dc3 Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Wed, 14 Jul 2021 15:26:51 -0400 Subject: [PATCH 04/11] fix workflow for real this time --- .github/workflows/test.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f7d6ead..6ab6d05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,15 +3,16 @@ name: Unit Tests jobs: test: name: "go ${{ matrix.go-version }} (${{ matrix.platform }})" - matrix: - go-version: [1.16.5] - platform: [ubuntu-latest] + strategy: + matrix: + go-version: [1.16.5] + platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v2 - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: ${{ matrix.go-version }} - - name: Test - run: go test ./... + - uses: actions/checkout@v2 + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Test + run: go test ./... From 129c3f3df9f944443df3333d4b033ae622b095e4 Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Wed, 14 Jul 2021 15:28:56 -0400 Subject: [PATCH 05/11] run workflow with verbose tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ab6d05..d98683e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,4 +15,4 @@ jobs: with: go-version: ${{ matrix.go-version }} - name: Test - run: go test ./... + run: go test ./... -v From 3b29b2713582b77a8570e2192f38a5fb2826b438 Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Wed, 14 Jul 2021 15:31:25 -0400 Subject: [PATCH 06/11] add dependabot --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..eb4bfe6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" From 34b9159f468d8b7eef473893220bbf74ba99a51a Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Wed, 14 Jul 2021 15:49:54 -0400 Subject: [PATCH 07/11] use fixtures --- internal/reddit/testdata/me.json | 165 +++++++++++++++++++++++++++++ internal/reddit/types_test.go | 176 ++----------------------------- 2 files changed, 173 insertions(+), 168 deletions(-) create mode 100644 internal/reddit/testdata/me.json diff --git a/internal/reddit/testdata/me.json b/internal/reddit/testdata/me.json new file mode 100644 index 0000000..23450e5 --- /dev/null +++ b/internal/reddit/testdata/me.json @@ -0,0 +1,165 @@ +{ + "is_employee": false, + "seen_layout_switch": true, + "has_visited_new_profile": true, + "pref_no_profanity": false, + "has_external_account": false, + "pref_geopopular": "GLOBAL", + "seen_redesign_modal": true, + "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": "#EA0027", + "user_is_muted": false, + "display_name": "u_changelog", + "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_19_EA0027.png", + "description": "", + "submit_link_label": "", + "header_size": null, + "restrict_commenting": false, + "subscribers": 3, + "submit_text_label": "", + "is_default_icon": true, + "link_flair_position": "", + "display_name_prefixed": "u/changelog", + "key_color": "", + "name": "t5_c30sw", + "is_default_banner": true, + "url": "/user/changelog/", + "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 + }, + "pref_show_presence": false, + "snoovatar_img": "", + "snoovatar_size": null, + "gold_expiration": null, + "has_gold_subscription": false, + "is_sponsor": false, + "num_friends": 7, + "features": { + "mod_service_mute_writes": true, + "promoted_trend_blanks": true, + "show_amp_link": true, + "chat": true, + "mweb_link_tab": { + "owner": "growth", + "variant": "control_1", + "experiment_id": 404 + }, + "is_email_permission_required": true, + "mod_awards": true, + "mweb_xpromo_revamp_v3": { + "owner": "growth", + "variant": "treatment_4", + "experiment_id": 480 + }, + "chat_subreddit": true, + "awards_on_streams": true, + "webhook_config": true, + "mweb_xpromo_modal_listing_click_daily_dismissible_ios": true, + "live_orangereds": true, + "cookie_consent_banner": true, + "modlog_copyright_removal": true, + "do_not_track": true, + "mod_service_mute_reads": true, + "chat_user_settings": true, + "use_pref_account_deployment": true, + "mweb_xpromo_interstitial_comments_ios": true, + "noreferrer_to_noopener": true, + "premium_subscriptions_table": true, + "mweb_xpromo_interstitial_comments_android": true, + "mweb_nsfw_xpromo": { + "owner": "growth", + "variant": "control_2", + "experiment_id": 361 + }, + "mweb_xpromo_modal_listing_click_daily_dismissible_android": true, + "mweb_sharing_web_share_api": { + "owner": "growth", + "variant": "control_1", + "experiment_id": 314 + }, + "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 + }, + "can_edit_name": false, + "verified": true, + "new_modmail_exists": null, + "pref_autoplay": false, + "coins": 0, + "has_paypal_subscription": false, + "has_subscribed_to_premium": false, + "id": "1ia22", + "has_stripe_subscription": false, + "oauth_client_id": "5JHxEu-4wnFfBA", + "can_create_subreddit": true, + "over_18": true, + "is_gold": false, + "is_mod": false, + "awarder_karma": 0, + "suspension_expiration_utc": null, + "has_verified_email": true, + "is_suspended": false, + "pref_video_autoplay": false, + "in_chat": true, + "has_android_subscription": false, + "in_redesign_beta": true, + "icon_img": "https://www.redditstatic.com/avatars/avatar_default_19_EA0027.png", + "has_mod_mail": false, + "pref_nightmode": true, + "awardee_karma": 10, + "hide_from_robots": true, + "password_set": true, + "link_karma": 2058, + "force_password_reset": false, + "total_karma": 3445, + "seen_give_award_tooltip": false, + "inbox_count": 0, + "seen_premium_adblock_modal": false, + "pref_top_karma_subreddits": false, + "has_mail": false, + "pref_show_snoovatar": false, + "name": "changelog", + "pref_clickgadget": 5, + "created": 1176750666.0, + "gold_creddits": 0, + "created_utc": 1176721866.0, + "has_ios_subscription": false, + "pref_show_twitter": false, + "in_beta": true, + "comment_karma": 1377, + "has_subscribed": true, + "linked_identities": [], + "seen_subreddit_chat_ftux": false +} + diff --git a/internal/reddit/types_test.go b/internal/reddit/types_test.go index 1522f39..4a06359 100644 --- a/internal/reddit/types_test.go +++ b/internal/reddit/types_test.go @@ -1,181 +1,21 @@ package reddit import ( + "io/ioutil" "testing" "github.com/stretchr/testify/assert" "github.com/valyala/fastjson" ) -func TestMeResponseParsing(t *testing.T) { - bb := []byte(` -{ - "is_employee": false, - "seen_layout_switch": true, - "has_visited_new_profile": true, - "pref_no_profanity": false, - "has_external_account": false, - "pref_geopopular": "GLOBAL", - "seen_redesign_modal": true, - "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": "#EA0027", - "user_is_muted": false, - "display_name": "u_changelog", - "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_19_EA0027.png", - "description": "", - "submit_link_label": "", - "header_size": null, - "restrict_commenting": false, - "subscribers": 3, - "submit_text_label": "", - "is_default_icon": true, - "link_flair_position": "", - "display_name_prefixed": "u/changelog", - "key_color": "", - "name": "t5_c30sw", - "is_default_banner": true, - "url": "/user/changelog/", - "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 - }, - "pref_show_presence": false, - "snoovatar_img": "", - "snoovatar_size": null, - "gold_expiration": null, - "has_gold_subscription": false, - "is_sponsor": false, - "num_friends": 7, - "features": { - "mod_service_mute_writes": true, - "promoted_trend_blanks": true, - "show_amp_link": true, - "chat": true, - "mweb_link_tab": { - "owner": "growth", - "variant": "control_1", - "experiment_id": 404 - }, - "is_email_permission_required": true, - "mod_awards": true, - "mweb_xpromo_revamp_v3": { - "owner": "growth", - "variant": "treatment_4", - "experiment_id": 480 - }, - "chat_subreddit": true, - "awards_on_streams": true, - "webhook_config": true, - "mweb_xpromo_modal_listing_click_daily_dismissible_ios": true, - "live_orangereds": true, - "cookie_consent_banner": true, - "modlog_copyright_removal": true, - "do_not_track": true, - "mod_service_mute_reads": true, - "chat_user_settings": true, - "use_pref_account_deployment": true, - "mweb_xpromo_interstitial_comments_ios": true, - "noreferrer_to_noopener": true, - "premium_subscriptions_table": true, - "mweb_xpromo_interstitial_comments_android": true, - "mweb_nsfw_xpromo": { - "owner": "growth", - "variant": "control_2", - "experiment_id": 361 - }, - "mweb_xpromo_modal_listing_click_daily_dismissible_android": true, - "mweb_sharing_web_share_api": { - "owner": "growth", - "variant": "control_1", - "experiment_id": 314 - }, - "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 - }, - "can_edit_name": false, - "verified": true, - "new_modmail_exists": null, - "pref_autoplay": false, - "coins": 0, - "has_paypal_subscription": false, - "has_subscribed_to_premium": false, - "id": "1ia22", - "has_stripe_subscription": false, - "oauth_client_id": "5JHxEu-4wnFfBA", - "can_create_subreddit": true, - "over_18": true, - "is_gold": false, - "is_mod": false, - "awarder_karma": 0, - "suspension_expiration_utc": null, - "has_verified_email": true, - "is_suspended": false, - "pref_video_autoplay": false, - "in_chat": true, - "has_android_subscription": false, - "in_redesign_beta": true, - "icon_img": "https://www.redditstatic.com/avatars/avatar_default_19_EA0027.png", - "has_mod_mail": false, - "pref_nightmode": true, - "awardee_karma": 10, - "hide_from_robots": true, - "password_set": true, - "link_karma": 2058, - "force_password_reset": false, - "total_karma": 3445, - "seen_give_award_tooltip": false, - "inbox_count": 0, - "seen_premium_adblock_modal": false, - "pref_top_karma_subreddits": false, - "has_mail": false, - "pref_show_snoovatar": false, - "name": "changelog", - "pref_clickgadget": 5, - "created": 1176750666.0, - "gold_creddits": 0, - "created_utc": 1176721866.0, - "has_ios_subscription": false, - "pref_show_twitter": false, - "in_beta": true, - "comment_karma": 1377, - "has_subscribed": true, - "linked_identities": [], - "seen_subreddit_chat_ftux": false -} - `) +var ( + parser = &fastjson.Parser{} +) + +func TestMeResponseParsing(t *testing.T) { + bb, err := ioutil.ReadFile("testdata/me.json") + assert.NoError(t, err) - parser := &fastjson.Parser{} val, err := parser.ParseBytes(bb) assert.NoError(t, err) From ad929b98d9b2cc2cc07c4bbb01cf69ecf90dc1a3 Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Wed, 14 Jul 2021 20:52:51 -0400 Subject: [PATCH 08/11] more parsing --- internal/reddit/client.go | 11 +- internal/reddit/testdata/me.json | 98 +-- internal/reddit/testdata/message_inbox.json | 778 ++++++++++++++++++++ internal/reddit/types.go | 62 ++ internal/reddit/types_test.go | 36 +- 5 files changed, 921 insertions(+), 64 deletions(-) create mode 100644 internal/reddit/testdata/message_inbox.json diff --git a/internal/reddit/client.go b/internal/reddit/client.go index db6f917..8449973 100644 --- a/internal/reddit/client.go +++ b/internal/reddit/client.go @@ -155,7 +155,7 @@ func (rac *AuthenticatedClient) RefreshTokens() (*RefreshTokenResponse, error) { return rtr, nil } -func (rac *AuthenticatedClient) MessageInbox(from string) (*MessageListingResponse, error) { +func (rac *AuthenticatedClient) MessageInbox(from string) (*ListingResponse, error) { req := NewRequest( WithTags([]string{"url:/api/v1/message/inbox"}), WithMethod("GET"), @@ -170,9 +170,12 @@ func (rac *AuthenticatedClient) MessageInbox(from string) (*MessageListingRespon return nil, err } - mlr := &MessageListingResponse{} - json.Unmarshal([]byte(body), mlr) - return mlr, nil + val, err := rac.parser.ParseBytes(body) + if err != nil { + return nil, err + } + + return NewListingResponse(val), nil } func (rac *AuthenticatedClient) MessageUnread(from string) (*MessageListingResponse, error) { diff --git a/internal/reddit/testdata/me.json b/internal/reddit/testdata/me.json index 23450e5..e3358e4 100644 --- a/internal/reddit/testdata/me.json +++ b/internal/reddit/testdata/me.json @@ -1,8 +1,8 @@ { "is_employee": false, "seen_layout_switch": true, - "has_visited_new_profile": true, - "pref_no_profanity": false, + "has_visited_new_profile": false, + "pref_no_profanity": true, "has_external_account": false, "pref_geopopular": "GLOBAL", "seen_redesign_modal": true, @@ -16,9 +16,9 @@ "free_form_reports": true, "community_icon": null, "show_media": true, - "icon_color": "#EA0027", + "icon_color": "#545452", "user_is_muted": false, - "display_name": "u_changelog", + "display_name": "u_hugocat", "header_img": null, "title": "", "coins": 0, @@ -29,20 +29,20 @@ 256 ], "primary_color": "", - "icon_img": "https://www.redditstatic.com/avatars/avatar_default_19_EA0027.png", + "icon_img": "https://www.redditstatic.com/avatars/avatar_default_07_545452.png", "description": "", "submit_link_label": "", "header_size": null, "restrict_commenting": false, - "subscribers": 3, + "subscribers": 0, "submit_text_label": "", "is_default_icon": true, "link_flair_position": "", - "display_name_prefixed": "u/changelog", + "display_name_prefixed": "u/hugocat", "key_color": "", - "name": "t5_c30sw", + "name": "t5_1e6nc8", "is_default_banner": true, - "url": "/user/changelog/", + "url": "/user/hugocat/", "quarantine": false, "banner_size": null, "user_is_moderator": true, @@ -52,35 +52,34 @@ "subreddit_type": "user", "user_is_subscriber": false }, - "pref_show_presence": false, + "pref_show_presence": true, "snoovatar_img": "", "snoovatar_size": null, "gold_expiration": null, "has_gold_subscription": false, "is_sponsor": false, - "num_friends": 7, + "num_friends": 1, "features": { "mod_service_mute_writes": true, "promoted_trend_blanks": true, "show_amp_link": true, - "chat": true, - "mweb_link_tab": { + "top_content_email_digest_v2": { "owner": "growth", "variant": "control_1", - "experiment_id": 404 + "experiment_id": 363 }, + "chat": true, "is_email_permission_required": true, "mod_awards": true, - "mweb_xpromo_revamp_v3": { + "expensive_coins_package": true, + "mweb_xpromo_revamp_v2": { "owner": "growth", - "variant": "treatment_4", - "experiment_id": 480 + "variant": "treatment_1", + "experiment_id": 457 }, - "chat_subreddit": true, "awards_on_streams": true, - "webhook_config": true, "mweb_xpromo_modal_listing_click_daily_dismissible_ios": true, - "live_orangereds": true, + "chat_subreddit": true, "cookie_consent_banner": true, "modlog_copyright_removal": true, "do_not_track": true, @@ -91,75 +90,58 @@ "noreferrer_to_noopener": true, "premium_subscriptions_table": true, "mweb_xpromo_interstitial_comments_android": true, - "mweb_nsfw_xpromo": { - "owner": "growth", - "variant": "control_2", - "experiment_id": 361 - }, - "mweb_xpromo_modal_listing_click_daily_dismissible_android": true, - "mweb_sharing_web_share_api": { - "owner": "growth", - "variant": "control_1", - "experiment_id": 314 - }, "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 + "mweb_xpromo_modal_listing_click_daily_dismissible_android": true }, "can_edit_name": false, "verified": true, - "new_modmail_exists": null, - "pref_autoplay": false, - "coins": 0, + "new_modmail_exists": true, + "pref_autoplay": true, + "coins": 100, "has_paypal_subscription": false, "has_subscribed_to_premium": false, - "id": "1ia22", + "id": "xgeee", "has_stripe_subscription": false, "oauth_client_id": "5JHxEu-4wnFfBA", "can_create_subreddit": true, "over_18": true, "is_gold": false, - "is_mod": false, + "is_mod": true, "awarder_karma": 0, "suspension_expiration_utc": null, "has_verified_email": true, "is_suspended": false, - "pref_video_autoplay": false, + "pref_video_autoplay": true, "in_chat": true, "has_android_subscription": false, "in_redesign_beta": true, - "icon_img": "https://www.redditstatic.com/avatars/avatar_default_19_EA0027.png", - "has_mod_mail": false, - "pref_nightmode": true, - "awardee_karma": 10, - "hide_from_robots": true, + "icon_img": "https://www.redditstatic.com/avatars/avatar_default_07_545452.png", + "has_mod_mail": true, + "pref_nightmode": false, + "awardee_karma": 55, + "hide_from_robots": false, "password_set": true, - "link_karma": 2058, + "link_karma": 166, "force_password_reset": false, - "total_karma": 3445, + "total_karma": 324, "seen_give_award_tooltip": false, "inbox_count": 0, "seen_premium_adblock_modal": false, - "pref_top_karma_subreddits": false, + "pref_top_karma_subreddits": true, "has_mail": false, "pref_show_snoovatar": false, - "name": "changelog", + "name": "hugocat", "pref_clickgadget": 5, - "created": 1176750666.0, + "created": 1461652799.0, "gold_creddits": 0, - "created_utc": 1176721866.0, + "created_utc": 1461623999.0, "has_ios_subscription": false, "pref_show_twitter": false, - "in_beta": true, - "comment_karma": 1377, + "in_beta": false, + "comment_karma": 103, "has_subscribed": true, "linked_identities": [], - "seen_subreddit_chat_ftux": false + "seen_subreddit_chat_ftux": true } - diff --git a/internal/reddit/testdata/message_inbox.json b/internal/reddit/testdata/message_inbox.json new file mode 100644 index 0000000..3d325e6 --- /dev/null +++ b/internal/reddit/testdata/message_inbox.json @@ -0,0 +1,778 @@ +{ + "kind": "Listing", + "data": { + "after": "t1_h470gjv", + "dist": 25, + "modhash": null, + "geo_filter": "", + "children": [ + { + "kind": "t4", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": null, + "likes": null, + "replies": "", + "author_fullname": "t2_4mwol", + "id": "138z6ke", + "subject": "how goes it", + "associated_awarding_id": null, + "score": 0, + "author": "iamthatis", + "num_comments": null, + "parent_id": null, + "subreddit_name_prefixed": null, + "new": true, + "type": "unknown", + "body": "how are you today", + "dest": "hugocat", + "was_comment": false, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>how are you today</p>\n</div><!-- SC_ON -->", + "name": "t4_138z6ke", + "created": 1626314195.0, + "created_utc": 1626285395.0, + "context": "", + "distinguished": null + } + }, + { + "kind": "t4", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": null, + "likes": null, + "replies": "", + "author_fullname": null, + "id": "138wl51", + "subject": "Welcome to r/memes!", + "associated_awarding_id": null, + "score": 0, + "author": "welcomebot", + "num_comments": null, + "parent_id": null, + "subreddit_name_prefixed": null, + "new": true, + "type": "unknown", + "body": "Welcome to r/memes, we are really happy to have you onboard\n\nA meme is a way of describing cultural information being shared.\nAn element of a culture or system of behavior that may be considered to be passed from one individual to another by nongenetic means, especially imitation.\n\nMake sure to read the rules of the sub before posting\n\nEnjoy your stay!\n\n----\n\nThis message can not be replied to. If you have questions for the moderators of r/memes you can message them [here](https://reddit.com/message/compose?to=r/memes).", + "dest": "hugocat", + "was_comment": false, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Welcome to <a href=\"/r/memes\">r/memes</a>, we are really happy to have you onboard</p>\n\n<p>A meme is a way of describing cultural information being shared.\nAn element of a culture or system of behavior that may be considered to be passed from one individual to another by nongenetic means, especially imitation.</p>\n\n<p>Make sure to read the rules of the sub before posting</p>\n\n<p>Enjoy your stay!</p>\n\n<hr/>\n\n<p>This message can not be replied to. If you have questions for the moderators of <a href=\"/r/memes\">r/memes</a> you can message them <a href=\"https://reddit.com/message/compose?to=r/memes\">here</a>.</p>\n</div><!-- SC_ON -->", + "name": "t4_138wl51", + "created": 1626309041.0, + "created_utc": 1626280241.0, + "context": "", + "distinguished": "admin" + } + }, + { + "kind": "t4", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": null, + "likes": null, + "replies": "", + "author_fullname": null, + "id": "138wl47", + "subject": "Welcome to r/Genshin_Impact!", + "associated_awarding_id": null, + "score": 0, + "author": "welcomebot", + "num_comments": null, + "parent_id": null, + "subreddit_name_prefixed": null, + "new": true, + "type": "unknown", + "body": "Hello Traveler!\n\nWelcome to /r/Genshin_Impact \n\nIf you are new to the game, we recommend our [FAQ page](https://www.reddit.com/r/Genshin_Impact/wiki/faq) to get started.\n\nIf you have questions, please write them in our pinned Daily Questions thread instead of opening a new one. The community will be happy to help you :)\n\nThank you for joining and enjoy your stay!\n\n----\n\nThis message can not be replied to. If you have questions for the moderators of r/Genshin_Impact you can message them [here](https://reddit.com/message/compose?to=r/Genshin_Impact).", + "dest": "hugocat", + "was_comment": false, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Hello Traveler!</p>\n\n<p>Welcome to <a href=\"/r/Genshin_Impact\">/r/Genshin_Impact</a> </p>\n\n<p>If you are new to the game, we recommend our <a href=\"https://www.reddit.com/r/Genshin_Impact/wiki/faq\">FAQ page</a> to get started.</p>\n\n<p>If you have questions, please write them in our pinned Daily Questions thread instead of opening a new one. The community will be happy to help you :)</p>\n\n<p>Thank you for joining and enjoy your stay!</p>\n\n<hr/>\n\n<p>This message can not be replied to. If you have questions for the moderators of <a href=\"/r/Genshin_Impact\">r/Genshin_Impact</a> you can message them <a href=\"https://reddit.com/message/compose?to=r/Genshin_Impact\">here</a>.</p>\n</div><!-- SC_ON -->", + "name": "t4_138wl47", + "created": 1626309041.0, + "created_utc": 1626280241.0, + "context": "", + "distinguished": "admin" + } + }, + { + "kind": "t4", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": null, + "likes": null, + "replies": "", + "author_fullname": null, + "id": "138wl41", + "subject": "Welcome to r/soccer!", + "associated_awarding_id": null, + "score": 0, + "author": "welcomebot", + "num_comments": null, + "parent_id": null, + "subreddit_name_prefixed": null, + "new": true, + "type": "unknown", + "body": "Welcome to /r/soccer: the back page of the internet!\n\nWe are a subreddit for news, results and discussion about the beautiful game.\n\n* When participating on /r/soccer, please follow the [rules of our community](https://www.reddit.com/r/soccer/wiki/rules#wiki_community_rules).\n* When creating a post, please follow our [submission guidelines](https://www.reddit.com/r/soccer/wiki/rules#wiki_submission_guidelines).\n* Represent your club and [choose a flair](https://www.reddit.com/r/soccer/wiki/flair).\n* Find other supporters like you and [visit our related subreddits](https://www.reddit.com/r/soccer/wiki/relatedsubreddits).\n* In need of help? [Consult our FAQ](https://www.reddit.com/r/soccer/wiki/faq), [read our wiki](https://www.reddit.com/r/soccer/wiki/index) or [message the mod team](https://www.reddit.com/message/compose?to=%2Fr%2Fsoccer).\n\nWe sticky regular threads to promote discussion.\n\n* Every day: [Daily Discussion](https://www.reddit.com/r/soccer/search?sort=new&restrict_sr=on&q=flair%3ADaily%2BDiscussion).\n* Monday: [Monday Moan](https://www.reddit.com/r/soccer/search?q=flair%3AMonday%2BMoan&restrict_sr=on&sort=new&t=all).\n* Tuesday: [Tactics Tuesday](https://www.reddit.com/r/soccer/search?sort=new&restrict_sr=on&q=flair%3ATactics).\n* Wednesday: [Change my View/Player vs Player/Would You Rather](https://www.reddit.com/r/soccer/search?sort=new&restrict_sr=on&q=flair%3AChange%2BMy%2BView).\n* Thursday: [Trivia Thursday](https://www.reddit.com/r/soccer/search?q=flair%3ATrivia&restrict_sr=on&sort=new&t=all).\n* Friday: [Free Talk Friday](https://www.reddit.com/r/soccer/search?q=flair%3AFree%2BTalk&restrict_sr=on&sort=new&t=all).\n* Saturday: [World Football Weekend](https://www.reddit.com/r/soccer/search?sort=new&restrict_sr=on&q=flair%3AWorld%2BFootball).\n* Sunday: [Sunday Support](https://www.reddit.com/r/soccer/search?q=flair%3ASunday%2BSupport&restrict_sr=on&sort=new&t=all).\n\n----\n\nThis message can not be replied to. If you have questions for the moderators of r/soccer you can message them [here](https://reddit.com/message/compose?to=r/soccer).", + "dest": "hugocat", + "was_comment": false, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Welcome to <a href=\"/r/soccer\">/r/soccer</a>: the back page of the internet!</p>\n\n<p>We are a subreddit for news, results and discussion about the beautiful game.</p>\n\n<ul>\n<li>When participating on <a href=\"/r/soccer\">/r/soccer</a>, please follow the <a href=\"https://www.reddit.com/r/soccer/wiki/rules#wiki_community_rules\">rules of our community</a>.</li>\n<li>When creating a post, please follow our <a href=\"https://www.reddit.com/r/soccer/wiki/rules#wiki_submission_guidelines\">submission guidelines</a>.</li>\n<li>Represent your club and <a href=\"https://www.reddit.com/r/soccer/wiki/flair\">choose a flair</a>.</li>\n<li>Find other supporters like you and <a href=\"https://www.reddit.com/r/soccer/wiki/relatedsubreddits\">visit our related subreddits</a>.</li>\n<li>In need of help? <a href=\"https://www.reddit.com/r/soccer/wiki/faq\">Consult our FAQ</a>, <a href=\"https://www.reddit.com/r/soccer/wiki/index\">read our wiki</a> or <a href=\"https://www.reddit.com/message/compose?to=%2Fr%2Fsoccer\">message the mod team</a>.</li>\n</ul>\n\n<p>We sticky regular threads to promote discussion.</p>\n\n<ul>\n<li>Every day: <a href=\"https://www.reddit.com/r/soccer/search?sort=new&amp;restrict_sr=on&amp;q=flair%3ADaily%2BDiscussion\">Daily Discussion</a>.</li>\n<li>Monday: <a href=\"https://www.reddit.com/r/soccer/search?q=flair%3AMonday%2BMoan&amp;restrict_sr=on&amp;sort=new&amp;t=all\">Monday Moan</a>.</li>\n<li>Tuesday: <a href=\"https://www.reddit.com/r/soccer/search?sort=new&amp;restrict_sr=on&amp;q=flair%3ATactics\">Tactics Tuesday</a>.</li>\n<li>Wednesday: <a href=\"https://www.reddit.com/r/soccer/search?sort=new&amp;restrict_sr=on&amp;q=flair%3AChange%2BMy%2BView\">Change my View/Player vs Player/Would You Rather</a>.</li>\n<li>Thursday: <a href=\"https://www.reddit.com/r/soccer/search?q=flair%3ATrivia&amp;restrict_sr=on&amp;sort=new&amp;t=all\">Trivia Thursday</a>.</li>\n<li>Friday: <a href=\"https://www.reddit.com/r/soccer/search?q=flair%3AFree%2BTalk&amp;restrict_sr=on&amp;sort=new&amp;t=all\">Free Talk Friday</a>.</li>\n<li>Saturday: <a href=\"https://www.reddit.com/r/soccer/search?sort=new&amp;restrict_sr=on&amp;q=flair%3AWorld%2BFootball\">World Football Weekend</a>.</li>\n<li>Sunday: <a href=\"https://www.reddit.com/r/soccer/search?q=flair%3ASunday%2BSupport&amp;restrict_sr=on&amp;sort=new&amp;t=all\">Sunday Support</a>.</li>\n</ul>\n\n<hr/>\n\n<p>This message can not be replied to. If you have questions for the moderators of <a href=\"/r/soccer\">r/soccer</a> you can message them <a href=\"https://reddit.com/message/compose?to=r/soccer\">here</a>.</p>\n</div><!-- SC_ON -->", + "name": "t4_138wl41", + "created": 1626309041.0, + "created_utc": 1626280241.0, + "context": "", + "distinguished": "admin" + } + }, + { + "kind": "t4", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": null, + "likes": null, + "replies": "", + "author_fullname": null, + "id": "138wl2f", + "subject": "Welcome to r/ffxiv!", + "associated_awarding_id": null, + "score": 0, + "author": "welcomebot", + "num_comments": null, + "parent_id": null, + "subreddit_name_prefixed": null, + "new": true, + "type": "unknown", + "body": "### **Thank you for subscribing!**\n\nWelcome to the largest fan-made community dedicated to FFXIV. This message is intended to inform you about the subreddit and how it functions.\n\nThe subreddit is open to everyone, all fans and players of FFXIV, including prospective players; to talk about the game, share fan creations, discuss the lore, theorycraft endgame content, to seek help with playing the game, to ask questions, and much much more!\n\nFirst up, take a quick look at the [subreddit rules](https://www.reddit.com/r/ffxiv/about/rules). If the moderators have to take action on something, such as removing content or changing the subreddit, they'll always do their best to explain why. If you ever need to contact the moderators [send a message to the subreddit](https://reddit.com/message/compose?to=r/ffxiv). This is commonly called modmail.\n\nNext, [assign yourself a swanky user flair](https://www.reddit.com/r/ffxiv/wiki/flairs_spoilers_filters#wiki_giving_yourself_a_flair)! Show off your favourite classes and jobs with emoji, include your characters name or something else!\n\n### Stickied posts\n\nEvery day, two posts are 'stickied' to the very top of the subreddit, filling both available sticky slots:\n\n* [A Daily Questions & FAQ Thread](https://www.reddit.com/r/ffxiv/about/sticky?num=1)\n * Ask any FFXIV-related questions here. Many users make use of the post and answer questions very frequently. Ask a question and you'll get an answer within minutes, if not seconds! Also called the DQT.\n * Always in the first sticky slot and only replaced by new DQTs.\n* [A weekly themed post](https://www.reddit.com/r/ffxiv/about/sticky?num=2)\n * A post focused on something specific such as in-game raids, lore discussion, celebrating personal achievements. See the sidebar of the subreddit for the full list!\n * Always in the second sticky slot. Frequently the weekly themed posts in the second sticky slot are replaced with megathreads for important topics. such discussion of patch notes or news.\n\n### Your posts\n\nWhen creating a post, there are a number of post flairs to choose from. Select the one that is most appropriate for your post. If you're unsure about which post flair to use, [check out this wiki page](https://www.reddit.com/r/ffxiv/wiki/flairs_spoilers_filters#wiki_giving_your_post_a_flair).\n\nBe sure to exclude any potential spoilers from your posts' title. If the content contains spoilers or it's likely that the comments will contain spoilers, or if you're unsure if it counts as a spoiler, mark it as containing spoilers. Better safe than sorry!\n\n### Others' posts\n\nIf you want to customise your viewing & reading experience, you can filter out types of posts based on their post flairs. The filters function if you are on the desktop version of Reddit. For example, if you want to view only discussion posts [click this link](https://www.reddit.com/r/ffxiv/search?q=flair:discussion%20OR%20flair:guide&restrict_sr=1&t=year&sort=new&feature=legacy_search#res-hide-options) and only discussion posts will show up.\n\n### Wiki\n\nThe [subreddit wiki](https://www.reddit.com/r/ffxiv/w/) contains a wealth of information and can be edited by anyone. It's also essentially a directory for finding things related to FFXIV, such as fan websites.\n\n* [Resources](https://www.reddit.com/r/ffxiv/w/resources/)\n * [New & returning player resources](https://www.reddit.com/r/ffxiv/w/resources#wiki_new_player_resources/)\n * [Endgame guides & tools](https://www.reddit.com/r/ffxiv/wiki/resources/endgame/)\n* [Communities & Content creators](https://www.reddit.com/r/ffxiv/wiki/resources/communities/) - Streamers, magazines, podcasts\n * [Communities on Reddit](https://www.reddit.com/r/ffxiv/w/communities/reddit/)\n * [Communities on Discord](https://www.reddit.com/r/ffxiv/w/communities/discord/)\"\n\n### Enjoy your stay!\n\n----\n\nThis message can not be replied to. If you have questions for the moderators of r/ffxiv you can message them [here](https://reddit.com/message/compose?to=r/ffxiv).", + "dest": "hugocat", + "was_comment": false, + "body_html": "<!-- SC_OFF --><div class=\"md\"><h3><strong>Thank you for subscribing!</strong></h3>\n\n<p>Welcome to the largest fan-made community dedicated to FFXIV. This message is intended to inform you about the subreddit and how it functions.</p>\n\n<p>The subreddit is open to everyone, all fans and players of FFXIV, including prospective players; to talk about the game, share fan creations, discuss the lore, theorycraft endgame content, to seek help with playing the game, to ask questions, and much much more!</p>\n\n<p>First up, take a quick look at the <a href=\"https://www.reddit.com/r/ffxiv/about/rules\">subreddit rules</a>. If the moderators have to take action on something, such as removing content or changing the subreddit, they&#39;ll always do their best to explain why. If you ever need to contact the moderators <a href=\"https://reddit.com/message/compose?to=r/ffxiv\">send a message to the subreddit</a>. This is commonly called modmail.</p>\n\n<p>Next, <a href=\"https://www.reddit.com/r/ffxiv/wiki/flairs_spoilers_filters#wiki_giving_yourself_a_flair\">assign yourself a swanky user flair</a>! Show off your favourite classes and jobs with emoji, include your characters name or something else!</p>\n\n<h3>Stickied posts</h3>\n\n<p>Every day, two posts are &#39;stickied&#39; to the very top of the subreddit, filling both available sticky slots:</p>\n\n<ul>\n<li><a href=\"https://www.reddit.com/r/ffxiv/about/sticky?num=1\">A Daily Questions &amp; FAQ Thread</a>\n\n<ul>\n<li>Ask any FFXIV-related questions here. Many users make use of the post and answer questions very frequently. Ask a question and you&#39;ll get an answer within minutes, if not seconds! Also called the DQT.</li>\n<li>Always in the first sticky slot and only replaced by new DQTs.</li>\n</ul></li>\n<li><a href=\"https://www.reddit.com/r/ffxiv/about/sticky?num=2\">A weekly themed post</a>\n\n<ul>\n<li>A post focused on something specific such as in-game raids, lore discussion, celebrating personal achievements. See the sidebar of the subreddit for the full list!</li>\n<li>Always in the second sticky slot. Frequently the weekly themed posts in the second sticky slot are replaced with megathreads for important topics. such discussion of patch notes or news.</li>\n</ul></li>\n</ul>\n\n<h3>Your posts</h3>\n\n<p>When creating a post, there are a number of post flairs to choose from. Select the one that is most appropriate for your post. If you&#39;re unsure about which post flair to use, <a href=\"https://www.reddit.com/r/ffxiv/wiki/flairs_spoilers_filters#wiki_giving_your_post_a_flair\">check out this wiki page</a>.</p>\n\n<p>Be sure to exclude any potential spoilers from your posts&#39; title. If the content contains spoilers or it&#39;s likely that the comments will contain spoilers, or if you&#39;re unsure if it counts as a spoiler, mark it as containing spoilers. Better safe than sorry!</p>\n\n<h3>Others&#39; posts</h3>\n\n<p>If you want to customise your viewing &amp; reading experience, you can filter out types of posts based on their post flairs. The filters function if you are on the desktop version of Reddit. For example, if you want to view only discussion posts <a href=\"https://www.reddit.com/r/ffxiv/search?q=flair:discussion%20OR%20flair:guide&amp;restrict_sr=1&amp;t=year&amp;sort=new&amp;feature=legacy_search#res-hide-options\">click this link</a> and only discussion posts will show up.</p>\n\n<h3>Wiki</h3>\n\n<p>The <a href=\"https://www.reddit.com/r/ffxiv/w/\">subreddit wiki</a> contains a wealth of information and can be edited by anyone. It&#39;s also essentially a directory for finding things related to FFXIV, such as fan websites.</p>\n\n<ul>\n<li><a href=\"https://www.reddit.com/r/ffxiv/w/resources/\">Resources</a>\n\n<ul>\n<li><a href=\"https://www.reddit.com/r/ffxiv/w/resources#wiki_new_player_resources/\">New &amp; returning player resources</a></li>\n<li><a href=\"https://www.reddit.com/r/ffxiv/wiki/resources/endgame/\">Endgame guides &amp; tools</a></li>\n</ul></li>\n<li><a href=\"https://www.reddit.com/r/ffxiv/wiki/resources/communities/\">Communities &amp; Content creators</a> - Streamers, magazines, podcasts\n\n<ul>\n<li><a href=\"https://www.reddit.com/r/ffxiv/w/communities/reddit/\">Communities on Reddit</a></li>\n<li><a href=\"https://www.reddit.com/r/ffxiv/w/communities/discord/\">Communities on Discord</a>&quot;</li>\n</ul></li>\n</ul>\n\n<h3>Enjoy your stay!</h3>\n\n<hr/>\n\n<p>This message can not be replied to. If you have questions for the moderators of <a href=\"/r/ffxiv\">r/ffxiv</a> you can message them <a href=\"https://reddit.com/message/compose?to=r/ffxiv\">here</a>.</p>\n</div><!-- SC_ON -->", + "name": "t4_138wl2f", + "created": 1626309040.0, + "created_utc": 1626280240.0, + "context": "", + "distinguished": "admin" + } + }, + { + "kind": "t4", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": null, + "likes": null, + "replies": "", + "author_fullname": null, + "id": "138wl17", + "subject": "Welcome to r/anime!", + "associated_awarding_id": null, + "score": 0, + "author": "welcomebot", + "num_comments": null, + "parent_id": null, + "subreddit_name_prefixed": null, + "new": true, + "type": "unknown", + "body": "Welcome to /r/anime! Here are a few quick pieces of information to help you make the most of the subreddit.\n\n* **[A guide to the subreddit we encourage you to read!](https://old.reddit.com/id248i)**\n* [List of Anime Watch order guides](https://www.reddit.com/r/anime/w/watch_order)\n* [Anime screenshot/source finder](https://trace.moe/)\n* [List of streaming sites](https://www.reddit.com/r/anime/wiki/legal_streams)\n\n####Things to note:\n\nMerch posts go in the [merch thread](https://www.reddit.com/r/anime/search?q=author%3AAnimeMod+title%3A%22Merch+Mondays%22&restrict_sr=on&sort=new&t=week) (all weekly threads are open throughout the week, just posted on specific days).\n\nIn an effort to keep our content focused, we don't allow memes. Try /r/animememes or /r/anime_irl\n\n####Spoiler rules \n\n**We consider any and all media applicable to our spoiler rule, whether it is an anime, a book, a movie, whether it was released yesterday or 100 years ago**. And please don't make obnoxious hints such as \"Haha, don't get too attached to that character, what happened to him had me *dying* of laughter\".\n\nWe also don't use the `>!spoiler!<` code as it shows as plaintext on some apps. To learn how to use the ones on our subreddit, please read the subreddit guide mentioned before.\n\nIf you have any other questions don't hesitate to contact us or your fellow subreddit members in the [Casual Discussion thread](https://www.reddit.com/r/anime/search?q=title%3A%22Casual+Discussion+Fridays%22+author%3AAnimeMod&restrict_sr=on&sort=new&t=week). We look forward to seeing you around!\n\n----\n\nThis message can not be replied to. If you have questions for the moderators of r/anime you can message them [here](https://reddit.com/message/compose?to=r/anime).", + "dest": "hugocat", + "was_comment": false, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Welcome to <a href=\"/r/anime\">/r/anime</a>! Here are a few quick pieces of information to help you make the most of the subreddit.</p>\n\n<ul>\n<li><strong><a href=\"https://old.reddit.com/id248i\">A guide to the subreddit we encourage you to read!</a></strong></li>\n<li><a href=\"https://www.reddit.com/r/anime/w/watch_order\">List of Anime Watch order guides</a></li>\n<li><a href=\"https://trace.moe/\">Anime screenshot/source finder</a></li>\n<li><a href=\"https://www.reddit.com/r/anime/wiki/legal_streams\">List of streaming sites</a></li>\n</ul>\n\n<h4>Things to note:</h4>\n\n<p>Merch posts go in the <a href=\"https://www.reddit.com/r/anime/search?q=author%3AAnimeMod+title%3A%22Merch+Mondays%22&amp;restrict_sr=on&amp;sort=new&amp;t=week\">merch thread</a> (all weekly threads are open throughout the week, just posted on specific days).</p>\n\n<p>In an effort to keep our content focused, we don&#39;t allow memes. Try <a href=\"/r/animememes\">/r/animememes</a> or <a href=\"/r/anime_irl\">/r/anime_irl</a></p>\n\n<h4>Spoiler rules</h4>\n\n<p><strong>We consider any and all media applicable to our spoiler rule, whether it is an anime, a book, a movie, whether it was released yesterday or 100 years ago</strong>. And please don&#39;t make obnoxious hints such as &quot;Haha, don&#39;t get too attached to that character, what happened to him had me <em>dying</em> of laughter&quot;.</p>\n\n<p>We also don&#39;t use the <code>&gt;!spoiler!&lt;</code> code as it shows as plaintext on some apps. To learn how to use the ones on our subreddit, please read the subreddit guide mentioned before.</p>\n\n<p>If you have any other questions don&#39;t hesitate to contact us or your fellow subreddit members in the <a href=\"https://www.reddit.com/r/anime/search?q=title%3A%22Casual+Discussion+Fridays%22+author%3AAnimeMod&amp;restrict_sr=on&amp;sort=new&amp;t=week\">Casual Discussion thread</a>. We look forward to seeing you around!</p>\n\n<hr/>\n\n<p>This message can not be replied to. If you have questions for the moderators of <a href=\"/r/anime\">r/anime</a> you can message them <a href=\"https://reddit.com/message/compose?to=r/anime\">here</a>.</p>\n</div><!-- SC_ON -->", + "name": "t4_138wl17", + "created": 1626309039.0, + "created_utc": 1626280239.0, + "context": "", + "distinguished": "admin" + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h4q5j98", + "subject": "comment reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t1_h46tec3", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "comment_reply", + "body": "This is a comment reply", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>This is a comment reply</p>\n</div><!-- SC_ON -->", + "name": "t1_h4q5j98", + "created": 1625969948.0, + "created_utc": 1625941148.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h4q5j98/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h4q5ilx", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "This is a post comment", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>This is a post comment</p>\n</div><!-- SC_ON -->", + "name": "t1_h4q5ilx", + "created": 1625969939.0, + "created_utc": 1625941139.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h4q5ilx/?context=3", + "distinguished": null + } + }, + { + "kind": "t4", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": null, + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "1354krc", + "subject": "mlem", + "associated_awarding_id": null, + "score": 0, + "author": "changelog", + "num_comments": null, + "parent_id": null, + "subreddit_name_prefixed": null, + "new": true, + "type": "unknown", + "body": "nomnom", + "dest": "hugocat", + "was_comment": false, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>nomnom</p>\n</div><!-- SC_ON -->", + "name": "t4_1354krc", + "created": 1625969551.0, + "created_utc": 1625940751.0, + "context": "", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h4q48sp", + "subject": "username mention", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 4, + "parent_id": "t3_m73kag", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "username_mention", + "body": "Hey /u/hugocat you'll like this one", + "link_title": "Here are my favorite photos of my cats I've taken lately!", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>Hey <a href=\"/u/hugocat\">/u/hugocat</a> you&#39;ll like this one</p>\n</div><!-- SC_ON -->", + "name": "t1_h4q48sp", + "created": 1625969276.0, + "created_utc": 1625940476.0, + "context": "/r/calicosummer/comments/m73kag/here_are_my_favorite_photos_of_my_cats_ive_taken/h4q48sp/?context=3", + "distinguished": null + } + }, + { + "kind": "t4", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": null, + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "132h8rp", + "subject": "I am waiting", + "associated_awarding_id": null, + "score": 0, + "author": "changelog", + "num_comments": null, + "parent_id": null, + "subreddit_name_prefixed": null, + "new": true, + "type": "unknown", + "body": "for those pets", + "dest": "hugocat", + "was_comment": false, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>for those pets</p>\n</div><!-- SC_ON -->", + "name": "t4_132h8rp", + "created": 1625721903.0, + "created_utc": 1625693103.0, + "context": "", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "apolloapp", + "likes": null, + "replies": "", + "author_fullname": "t2_5hk0rgrc", + "id": "h4duiop", + "subject": "comment reply", + "associated_awarding_id": null, + "score": 1, + "author": "Qeweyou", + "num_comments": 11, + "parent_id": "t1_h4dt2td", + "subreddit_name_prefixed": "r/apolloapp", + "new": true, + "type": "comment_reply", + "body": "sup", + "link_title": "Crossposts end up taking up a huge amount of my scrolling. Would there ever be a way to coordinate with something like karma decay to group/hide the same post from showing up a million times?", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>sup</p>\n</div><!-- SC_ON -->", + "name": "t1_h4duiop", + "created": 1625712530.0, + "created_utc": 1625683730.0, + "context": "/r/apolloapp/comments/oemfzc/crossposts_end_up_taking_up_a_huge_amount_of_my/h4duiop/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_4mwol", + "id": "h4au97f", + "subject": "comment reply", + "associated_awarding_id": null, + "score": 1, + "author": "iamthatis", + "num_comments": 24, + "parent_id": "t1_h46tec3", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "comment_reply", + "body": "test123 hello", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>test123 hello</p>\n</div><!-- SC_ON -->", + "name": "t1_h4au97f", + "created": 1625647019.0, + "created_utc": 1625618219.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h4au97f/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_4mwol", + "id": "h4au826", + "subject": "comment reply", + "associated_awarding_id": null, + "score": 1, + "author": "iamthatis", + "num_comments": 24, + "parent_id": "t1_h46tec3", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "comment_reply", + "body": "testtest", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>testtest</p>\n</div><!-- SC_ON -->", + "name": "t1_h4au826", + "created": 1625647002.0, + "created_utc": 1625618202.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h4au826/?context=3", + "distinguished": null + } + }, + { + "kind": "t4", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": null, + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "130rizy", + "subject": "Send nudes", + "associated_awarding_id": null, + "score": 0, + "author": "changelog", + "num_comments": null, + "parent_id": null, + "subreddit_name_prefixed": null, + "new": true, + "type": "unknown", + "body": "now plz", + "dest": "hugocat", + "was_comment": false, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>now plz</p>\n</div><!-- SC_ON -->", + "name": "t4_130rizy", + "created": 1625567538.0, + "created_utc": 1625538738.0, + "context": "", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h4716tx", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "asdasdasd", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>asdasdasd</p>\n</div><!-- SC_ON -->", + "name": "t1_h4716tx", + "created": 1625567384.0, + "created_utc": 1625538584.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h4716tx/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h4711ci", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "asdasdasd", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>asdasdasd</p>\n</div><!-- SC_ON -->", + "name": "t1_h4711ci", + "created": 1625567295.0, + "created_utc": 1625538495.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h4711ci/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h47115i", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "asdasdasd", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>asdasdasd</p>\n</div><!-- SC_ON -->", + "name": "t1_h47115i", + "created": 1625567292.0, + "created_utc": 1625538492.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h47115i/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h470zhl", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "asdasdasdasda", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>asdasdasdasda</p>\n</div><!-- SC_ON -->", + "name": "t1_h470zhl", + "created": 1625567268.0, + "created_utc": 1625538468.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h470zhl/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h470zcp", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "asd", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>asd</p>\n</div><!-- SC_ON -->", + "name": "t1_h470zcp", + "created": 1625567266.0, + "created_utc": 1625538466.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h470zcp/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h470z9f", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "asdasdasd", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>asdasdasd</p>\n</div><!-- SC_ON -->", + "name": "t1_h470z9f", + "created": 1625567265.0, + "created_utc": 1625538465.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h470z9f/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h470z5d", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "asdasdasdasd", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>asdasdasdasd</p>\n</div><!-- SC_ON -->", + "name": "t1_h470z5d", + "created": 1625567263.0, + "created_utc": 1625538463.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h470z5d/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h470gt8", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "sadfasdfsadgfasdgasdfgsdfg", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>sadfasdfsadgfasdgasdfgsdfg</p>\n</div><!-- SC_ON -->", + "name": "t1_h470gt8", + "created": 1625566969.0, + "created_utc": 1625538169.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h470gt8/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h470goc", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "sdfgasdgsdfgsdfgsdfgsdf", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>sdfgasdgsdfgsdfgsdfgsdf</p>\n</div><!-- SC_ON -->", + "name": "t1_h470goc", + "created": 1625566967.0, + "created_utc": 1625538167.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h470goc/?context=3", + "distinguished": null + } + }, + { + "kind": "t1", + "data": { + "first_message": null, + "first_message_name": null, + "subreddit": "calicosummer", + "likes": null, + "replies": "", + "author_fullname": "t2_1ia22", + "id": "h470gjv", + "subject": "post reply", + "associated_awarding_id": null, + "score": 1, + "author": "changelog", + "num_comments": 24, + "parent_id": "t3_ngcapc", + "subreddit_name_prefixed": "r/calicosummer", + "new": true, + "type": "post_reply", + "body": "asdasd123123123", + "link_title": "hello i am a cat", + "dest": "hugocat", + "was_comment": true, + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>asdasd123123123</p>\n</div><!-- SC_ON -->", + "name": "t1_h470gjv", + "created": 1625566965.0, + "created_utc": 1625538165.0, + "context": "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h470gjv/?context=3", + "distinguished": null + } + } + ], + "before": null + } +} diff --git a/internal/reddit/types.go b/internal/reddit/types.go index 6e27961..05e0c46 100644 --- a/internal/reddit/types.go +++ b/internal/reddit/types.go @@ -70,3 +70,65 @@ func NewMeResponse(val *fastjson.Value) *MeResponse { return mr } + +type Thing struct { + Kind string `json:"kind"` + ID string `json:"id"` + Type string `json:"type"` + Author string `json:"author"` + Subject string `json:"subject"` + Body string `json:"body"` + CreatedAt float64 `json:"created_utc"` + Context string `json:"context"` + ParentID string `json:"parent_id"` + LinkTitle string `json:"link_title"` + Destination string `json:"dest"` + Subreddit string `json:"subreddit"` +} + +func NewThing(val *fastjson.Value) *Thing { + t := &Thing{} + + t.Kind = string(val.GetStringBytes("kind")) + + data := val.Get("data") + + t.ID = string(data.GetStringBytes("id")) + t.Type = string(data.GetStringBytes("type")) + t.Author = string(data.GetStringBytes("author")) + t.Subject = string(data.GetStringBytes("subject")) + t.Body = string(data.GetStringBytes("body")) + t.CreatedAt = data.GetFloat64("created_utc") + t.Context = string(data.GetStringBytes("context")) + t.ParentID = string(data.GetStringBytes("parent_id")) + t.LinkTitle = string(data.GetStringBytes("link_title")) + t.Destination = string(data.GetStringBytes("dest")) + t.Subreddit = string(data.GetStringBytes("subreddit")) + + return t +} + +type ListingResponse struct { + Count int + Children []*Thing + After string + Before string +} + +func NewListingResponse(val *fastjson.Value) *ListingResponse { + lr := &ListingResponse{} + + data := val.Get("data") + lr.After = string(data.GetStringBytes("after")) + lr.Before = string(data.GetStringBytes("before")) + lr.Count = data.GetInt("dist") + lr.Children = make([]*Thing, lr.Count) + + children := data.GetArray("children") + for i := 0; i < lr.Count; i++ { + t := NewThing(children[i]) + lr.Children[i] = t + } + + return lr +} diff --git a/internal/reddit/types_test.go b/internal/reddit/types_test.go index 4a06359..61642e9 100644 --- a/internal/reddit/types_test.go +++ b/internal/reddit/types_test.go @@ -22,6 +22,38 @@ func TestMeResponseParsing(t *testing.T) { me := NewMeResponse(val) assert.NotNil(t, me) - assert.Equal(t, "1ia22", me.ID) - assert.Equal(t, "changelog", me.Name) + assert.Equal(t, "xgeee", me.ID) + assert.Equal(t, "hugocat", me.Name) +} + +func TestListingResponseParsing(t *testing.T) { + bb, err := ioutil.ReadFile("testdata/message_inbox.json") + assert.NoError(t, err) + + val, err := parser.ParseBytes(bb) + assert.NoError(t, err) + + l := NewListingResponse(val) + assert.NotNil(t, l) + + assert.Equal(t, 25, l.Count) + assert.Equal(t, 25, len(l.Children)) + assert.Equal(t, "t1_h470gjv", l.After) + assert.Equal(t, "", l.Before) + + thing := l.Children[0] + assert.Equal(t, "t4", thing.Kind) + assert.Equal(t, "138z6ke", thing.ID) + assert.Equal(t, "unknown", thing.Type) + assert.Equal(t, "iamthatis", thing.Author) + assert.Equal(t, "how goes it", thing.Subject) + assert.Equal(t, "how are you today", thing.Body) + assert.Equal(t, 1626285395.0, thing.CreatedAt) + assert.Equal(t, "hugocat", thing.Destination) + + thing = l.Children[6] + assert.Equal(t, "/r/calicosummer/comments/ngcapc/hello_i_am_a_cat/h4q5j98/?context=3", thing.Context) + assert.Equal(t, "t1_h46tec3", thing.ParentID) + assert.Equal(t, "hello i am a cat", thing.LinkTitle) + assert.Equal(t, "calicosummer", thing.Subreddit) } From ec783632528735b71e6ae3898bbb962dc34215a6 Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Thu, 15 Jul 2021 10:51:34 -0400 Subject: [PATCH 09/11] update notifier logic --- internal/cmd/worker.go | 1 + internal/reddit/client.go | 67 +++++++++------------ internal/reddit/testdata/error.json | 4 ++ internal/reddit/testdata/refresh_token.json | 7 +++ internal/reddit/types.go | 52 +++++++--------- internal/reddit/types_test.go | 14 +++++ internal/worker/notifications.go | 36 ++++++++--- 7 files changed, 106 insertions(+), 75 deletions(-) create mode 100644 internal/reddit/testdata/error.json create mode 100644 internal/reddit/testdata/refresh_token.json diff --git a/internal/cmd/worker.go b/internal/cmd/worker.go index fc13d5a..9323009 100644 --- a/internal/cmd/worker.go +++ b/internal/cmd/worker.go @@ -61,6 +61,7 @@ func WorkerCmd(ctx context.Context) *cobra.Command { } consumers := runtime.NumCPU() * multiplier + //consumers = 1 worker := workerFn(logger, statsd, db, redis, queue, consumers) worker.Start() diff --git a/internal/reddit/client.go b/internal/reddit/client.go index 8449973..959e533 100644 --- a/internal/reddit/client.go +++ b/internal/reddit/client.go @@ -1,7 +1,6 @@ package reddit import ( - "encoding/json" "fmt" "io/ioutil" "net/http" @@ -23,7 +22,7 @@ type Client struct { secret string client *http.Client tracer *httptrace.ClientTrace - parser *fastjson.Parser + pool *fastjson.ParserPool statsd *statsd.Client } @@ -74,14 +73,14 @@ func NewClient(id, secret string, statsd *statsd.Client, connLimit int) *Client client := &http.Client{Transport: t} - parser := &fastjson.Parser{} + pool := &fastjson.ParserPool{} return &Client{ id, secret, client, tracer, - parser, + pool, statsd, } } @@ -98,7 +97,7 @@ func (rc *Client) NewAuthenticatedClient(refreshToken, accessToken string) *Auth return &AuthenticatedClient{rc, refreshToken, accessToken, nil} } -func (rac *AuthenticatedClient) request(r *Request) ([]byte, error) { +func (rac *AuthenticatedClient) request(r *Request) (*fastjson.Value, error) { req, err := r.HTTPRequest() if err != nil { return nil, err @@ -122,16 +121,21 @@ func (rac *AuthenticatedClient) request(r *Request) ([]byte, error) { rac.statsd.Incr("reddit.api.errors", r.tags, 0.1) return nil, err } + + parser := rac.pool.Get() + defer rac.pool.Put(parser) + if resp.StatusCode != 200 { rac.statsd.Incr("reddit.api.errors", r.tags, 0.1) + // Try to parse a json error. Otherwise we generate a generic one - rerr := &Error{} - if jerr := json.Unmarshal(bb, rerr); jerr != nil { + val, jerr := parser.ParseBytes(bb) + if jerr != nil { return nil, fmt.Errorf("error from reddit: %d", resp.StatusCode) } - return nil, rerr + return nil, NewError(val) } - return bb, nil + return parser.ParseBytes(bb) } func (rac *AuthenticatedClient) RefreshTokens() (*RefreshTokenResponse, error) { @@ -144,33 +148,24 @@ func (rac *AuthenticatedClient) RefreshTokens() (*RefreshTokenResponse, error) { WithBasicAuth(rac.id, rac.secret), ) - body, err := rac.request(req) - + val, err := rac.request(req) if err != nil { return nil, err } - rtr := &RefreshTokenResponse{} - json.Unmarshal([]byte(body), rtr) - return rtr, nil + return NewRefreshTokenResponse(val), nil } -func (rac *AuthenticatedClient) MessageInbox(from string) (*ListingResponse, error) { - req := NewRequest( +func (rac *AuthenticatedClient) MessageInbox(opts ...RequestOption) (*ListingResponse, error) { + opts = append([]RequestOption{ WithTags([]string{"url:/api/v1/message/inbox"}), WithMethod("GET"), WithToken(rac.accessToken), WithURL("https://oauth.reddit.com/message/inbox.json"), - WithQuery("before", from), - ) + }, opts...) + req := NewRequest(opts...) - body, err := rac.request(req) - - if err != nil { - return nil, err - } - - val, err := rac.parser.ParseBytes(body) + val, err := rac.request(req) if err != nil { return nil, err } @@ -178,24 +173,22 @@ func (rac *AuthenticatedClient) MessageInbox(from string) (*ListingResponse, err return NewListingResponse(val), nil } -func (rac *AuthenticatedClient) MessageUnread(from string) (*MessageListingResponse, error) { - req := NewRequest( +func (rac *AuthenticatedClient) MessageUnread(opts ...RequestOption) (*ListingResponse, error) { + opts = append([]RequestOption{ WithTags([]string{"url:/api/v1/message/unread"}), WithMethod("GET"), WithToken(rac.accessToken), WithURL("https://oauth.reddit.com/message/unread.json"), - WithQuery("before", from), - ) + }, opts...) - body, err := rac.request(req) + req := NewRequest(opts...) + val, err := rac.request(req) if err != nil { return nil, err } - mlr := &MessageListingResponse{} - json.Unmarshal([]byte(body), mlr) - return mlr, nil + return NewListingResponse(val), nil } func (rac *AuthenticatedClient) Me() (*MeResponse, error) { @@ -206,14 +199,10 @@ func (rac *AuthenticatedClient) Me() (*MeResponse, error) { WithURL("https://oauth.reddit.com/api/v1/me"), ) - body, err := rac.request(req) - + val, err := rac.request(req) if err != nil { return nil, err } - mr := &MeResponse{} - err = json.Unmarshal(body, mr) - - return mr, err + return NewMeResponse(val), nil } diff --git a/internal/reddit/testdata/error.json b/internal/reddit/testdata/error.json new file mode 100644 index 0000000..f8f1305 --- /dev/null +++ b/internal/reddit/testdata/error.json @@ -0,0 +1,4 @@ +{ + "message": "Unauthorized", + "error": 401 +} diff --git a/internal/reddit/testdata/refresh_token.json b/internal/reddit/testdata/refresh_token.json new file mode 100644 index 0000000..d1c7ad3 --- /dev/null +++ b/internal/reddit/testdata/refresh_token.json @@ -0,0 +1,7 @@ +{ + "access_token": "***REMOVED***", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "***REMOVED***", + "scope": "account creddits edit flair history identity livemanage modconfig modcontributors modflair modlog modmail modothers modposts modself modtraffic modwiki mysubreddits privatemessages read report save structuredstyles submit subscribe vote wikiedit wikiread" +} diff --git a/internal/reddit/types.go b/internal/reddit/types.go index 05e0c46..0c10480 100644 --- a/internal/reddit/types.go +++ b/internal/reddit/types.go @@ -16,36 +16,13 @@ func (err *Error) Error() string { return fmt.Sprintf("%s (%d)", err.Message, err.Code) } -type Message struct { - ID string `json:"id"` - Kind string `json:"kind"` - Type string `json:"type"` - Author string `json:"author"` - Subject string `json:"subject"` - Body string `json:"body"` - CreatedAt float64 `json:"created_utc"` - Context string `json:"context"` - ParentID string `json:"parent_id"` - LinkTitle string `json:"link_title"` - Destination string `json:"dest"` - Subreddit string `json:"subreddit"` -} +func NewError(val *fastjson.Value) *Error { + err := &Error{} -type MessageData struct { - Message `json:"data"` - Kind string `json:"kind"` -} + err.Message = string(val.GetStringBytes("message")) + err.Code = val.GetInt("error") -func (md MessageData) FullName() string { - return fmt.Sprintf("%s_%s", md.Kind, md.ID) -} - -type MessageListing struct { - Messages []MessageData `json:"children"` -} - -type MessageListingResponse struct { - MessageListing MessageListing `json:"data"` + return err } type RefreshTokenResponse struct { @@ -53,6 +30,15 @@ type RefreshTokenResponse struct { RefreshToken string `json:"refresh_token"` } +func NewRefreshTokenResponse(val *fastjson.Value) *RefreshTokenResponse { + rtr := &RefreshTokenResponse{} + + rtr.AccessToken = string(val.GetStringBytes("access_token")) + rtr.RefreshToken = string(val.GetStringBytes("refresh_token")) + + return rtr +} + type MeResponse struct { ID string `json:"id"` Name string @@ -86,6 +72,10 @@ type Thing struct { Subreddit string `json:"subreddit"` } +func (t *Thing) FullName() string { + return fmt.Sprintf("%s_%s", t.Kind, t.ID) +} + func NewThing(val *fastjson.Value) *Thing { t := &Thing{} @@ -122,8 +112,12 @@ func NewListingResponse(val *fastjson.Value) *ListingResponse { lr.After = string(data.GetStringBytes("after")) lr.Before = string(data.GetStringBytes("before")) lr.Count = data.GetInt("dist") - lr.Children = make([]*Thing, lr.Count) + if lr.Count == 0 { + return lr + } + + lr.Children = make([]*Thing, lr.Count) children := data.GetArray("children") for i := 0; i < lr.Count; i++ { t := NewThing(children[i]) diff --git a/internal/reddit/types_test.go b/internal/reddit/types_test.go index 61642e9..10d5aa9 100644 --- a/internal/reddit/types_test.go +++ b/internal/reddit/types_test.go @@ -26,6 +26,20 @@ func TestMeResponseParsing(t *testing.T) { assert.Equal(t, "hugocat", me.Name) } +func TestRefreshTokenResponseParsing(t *testing.T) { + bb, err := ioutil.ReadFile("testdata/refresh_token.json") + assert.NoError(t, err) + + val, err := parser.ParseBytes(bb) + assert.NoError(t, err) + + rtr := NewRefreshTokenResponse(val) + assert.NotNil(t, rtr) + + assert.Equal(t, "***REMOVED***", rtr.AccessToken) + assert.Equal(t, "***REMOVED***", rtr.RefreshToken) +} + func TestListingResponseParsing(t *testing.T) { bb, err := ioutil.ReadFile("testdata/message_inbox.json") assert.NoError(t, err) diff --git a/internal/worker/notifications.go b/internal/worker/notifications.go index b3617ba..df0ab98 100644 --- a/internal/worker/notifications.go +++ b/internal/worker/notifications.go @@ -248,7 +248,7 @@ func (nc *notificationsConsumer) Consume(delivery rmq.Delivery) { nc.logger.WithFields(logrus.Fields{ "accountID": id, }).Debug("fetching message inbox") - msgs, err := rac.MessageInbox(account.LastMessageID) + msgs, err := rac.MessageInbox(reddit.WithQuery("limit", "10")) if err != nil { nc.logger.WithFields(logrus.Fields{ @@ -258,12 +258,32 @@ func (nc *notificationsConsumer) Consume(delivery rmq.Delivery) { return } + // Figure out where we stand + if msgs.Count == 0 || msgs.Children[0].FullName() == account.LastMessageID { + nc.logger.WithFields(logrus.Fields{ + "accountID": id, + }).Debug("no new messages, bailing early") + return + } + + // Find which one is the oldest we haven't notified on + oldest := 0 + for i, t := range msgs.Children { + if t.FullName() == account.LastMessageID { + break + } + + oldest = i + } + + tt := msgs.Children[:oldest] + nc.logger.WithFields(logrus.Fields{ "accountID": id, - "count": len(msgs.MessageListing.Messages), + "count": len(tt), }).Debug("fetched messages") - if len(msgs.MessageListing.Messages) == 0 { + if len(tt) == 0 { nc.logger.WithFields(logrus.Fields{ "accountID": id, }).Debug("no new messages, bailing early") @@ -271,7 +291,7 @@ func (nc *notificationsConsumer) Consume(delivery rmq.Delivery) { } // Set latest message we alerted on - latestMsg := msgs.MessageListing.Messages[0] + latestMsg := tt[0] if err = nc.db.BeginFunc(ctx, func(tx pgx.Tx) error { stmt := ` UPDATE accounts @@ -316,10 +336,12 @@ func (nc *notificationsConsumer) Consume(delivery rmq.Delivery) { devices = append(devices, device) } - for _, msg := range msgs.MessageListing.Messages { + // Iterate backwards so we notify from older to newer + for i := len(tt) - 1; i >= 0; i-- { + msg := tt[i] notification := &apns2.Notification{} notification.Topic = "com.christianselig.Apollo" - notification.Payload = payloadFromMessage(account, &msg, len(msgs.MessageListing.Messages)) + notification.Payload = payloadFromMessage(account, msg, len(tt)) for _, device := range devices { notification.DeviceToken = device.APNSToken @@ -353,7 +375,7 @@ func (nc *notificationsConsumer) Consume(delivery rmq.Delivery) { }).Debug("finishing job") } -func payloadFromMessage(acct *data.Account, msg *reddit.MessageData, badgeCount int) *payload.Payload { +func payloadFromMessage(acct *data.Account, msg *reddit.Thing, badgeCount int) *payload.Payload { postBody := msg.Body if len(postBody) > 2000 { postBody = msg.Body[:2000] From 5ebac84014e024c16bd958e23d8d91d25388b56b Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Thu, 15 Jul 2021 10:55:24 -0400 Subject: [PATCH 10/11] remove dead code --- internal/cmd/worker.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/cmd/worker.go b/internal/cmd/worker.go index 9323009..fc13d5a 100644 --- a/internal/cmd/worker.go +++ b/internal/cmd/worker.go @@ -61,7 +61,6 @@ func WorkerCmd(ctx context.Context) *cobra.Command { } consumers := runtime.NumCPU() * multiplier - //consumers = 1 worker := workerFn(logger, statsd, db, redis, queue, consumers) worker.Start() From a9a329967abd7516c49c55eb0696b8a107c9e274 Mon Sep 17 00:00:00 2001 From: Andre Medeiros Date: Thu, 15 Jul 2021 11:03:52 -0400 Subject: [PATCH 11/11] remove dead logic --- internal/worker/notifications.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/internal/worker/notifications.go b/internal/worker/notifications.go index df0ab98..055fec3 100644 --- a/internal/worker/notifications.go +++ b/internal/worker/notifications.go @@ -283,13 +283,6 @@ func (nc *notificationsConsumer) Consume(delivery rmq.Delivery) { "count": len(tt), }).Debug("fetched messages") - if len(tt) == 0 { - nc.logger.WithFields(logrus.Fields{ - "accountID": id, - }).Debug("no new messages, bailing early") - return - } - // Set latest message we alerted on latestMsg := tt[0] if err = nc.db.BeginFunc(ctx, func(tx pgx.Tx) error {