ghost 6.54.1 → 6.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
- package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
- package/components/tryghost-nql-string-0.0.0.tgz +0 -0
- package/core/boot.js +3 -6
- package/core/built/admin/assets/{Combination-CJG7Flp-.js → Combination-BCm9FL4o.js} +1 -1
- package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
- package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
- package/core/built/admin/assets/{account-migration-BklF2I_I.js → account-migration-D3O_yVck.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-CNovr6W-.js → account-migration-D8FbP-Z-.js} +7 -7
- package/core/built/admin/assets/activitypub/activitypub.js +6 -6
- package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-11EA1VSs.js} +2 -2
- package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-jfbLG-xe.js} +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-Ceslb1_M.js → bluesky-sharing-f1jTQe2P.js} +16 -16
- package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-nmTOPY9M.js} +3 -3
- package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-BCwo9ep3.js} +2 -2
- package/core/built/admin/assets/activitypub/{dist-DHAjBcV3.js → dist-BEfX-0aW.js} +12 -12
- package/core/built/admin/assets/activitypub/{domain-C1qQvJ8C.js → domain-AaDlZlrA.js} +9 -9
- package/core/built/admin/assets/activitypub/{edit-profile-BcH-jEfM.js → edit-profile-DPi8Tl5M.js} +945 -945
- package/core/built/admin/assets/activitypub/{explore-AxwBh9dI.js → explore-CxTzjILQ.js} +8 -8
- package/core/built/admin/assets/activitypub/{feed-BaOlbh7a.js → feed-DPv5x2MX.js} +33 -33
- package/core/built/admin/assets/activitypub/{field-B7mYYQpN.js → field-DbgA8lat.js} +3 -3
- package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-BWuZoYkA.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-35VDQ2K2.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-vXez-5uJ.js → inbox-DSrF_i1O.js} +85 -85
- package/core/built/admin/assets/activitypub/{label-Chv5T8bw.js → label-CPjFBbo7.js} +3 -3
- package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-DtSM2i1I.js → moderation-BkDC1XVZ.js} +11 -11
- package/core/built/admin/assets/activitypub/{note-Dckp4S-2.js → note-xEyH4xT-.js} +7 -7
- package/core/built/admin/assets/activitypub/{notifications-CG5T22vg.js → notifications-DClaL3ui.js} +9 -9
- package/core/built/admin/assets/activitypub/{preferences-D_799jdl.js → preferences-DpnOa-4Z.js} +20 -20
- package/core/built/admin/assets/activitypub/{profile-D2JfXBQl.js → profile-Dvi9mz-6.js} +237 -237
- package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cs9ySd6Z.js} +2 -2
- package/core/built/admin/assets/activitypub/{routes-CCHe2dQp.js → routes-DFBeTnfn.js} +1139 -1150
- package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-C5u_VjzY.js} +3 -3
- package/core/built/admin/assets/activitypub/{settings-kx-9Enkz.js → settings-CV2fFu6D.js} +13 -13
- package/core/built/admin/assets/activitypub/{site-x4_IjVJ0.js → site-_TgDd_EM.js} +81 -81
- package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-BfnNL2oF.js} +5 -5
- package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DLndZkO-.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-A-PZTtRE.js} +10 -10
- package/core/built/admin/assets/activitypub/{tabs-CyPRQQ1N.js → tabs-BUbz7tea.js} +321 -319
- package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-BdX_xYYT.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-EwuRemHh.js} +2 -2
- package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-cy4ACBc4.js} +92 -90
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-BAB6fNGt.js → use-reply-chain-data-CyFcGfan.js} +5 -5
- package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-7zk8mV2p.js} +2 -2
- package/core/built/admin/assets/audience-CkBb2LlO.js +1 -0
- package/core/built/admin/assets/{automated-emails-of1MvVsv.js → automated-emails-6t2bIRWf.js} +2 -2
- package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
- package/core/built/admin/assets/{automations-DRFjW5fw.js → automations-cMllU545.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-BbbjWdzs.js → bluesky-sharing-DPGpyD-G.js} +1 -1
- package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-B-M9Ip_2.js → breadcrumb-DIfcF5ow.js} +1 -1
- package/core/built/admin/assets/build-CsiH26uS.js +1 -0
- package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
- package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-xiSsW8r0.js → chart-helpers-DV0FkdcD.js} +1 -1
- package/core/built/admin/assets/{checkbox-EhJKHFdZ.js → checkbox-BIl077a0.js} +1 -1
- package/core/built/admin/assets/{chunk.361.3cdb584b973959e52ccc.js → chunk.332.05991a101d67f732dec3.js} +16 -12
- package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +25 -0
- package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +27 -0
- package/core/built/admin/assets/{code-editor-view-CZtJFGhR.js → code-editor-view-C0aWNwdl.js} +1 -1
- package/core/built/admin/assets/comments-qwhIjH3c.js +1 -0
- package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
- package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
- package/core/built/admin/assets/{date-range-select-CTlyo8zW.js → date-range-select-dXc-GsVe.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-qs2GfLvl.js → disabled-sources-indicator-DYAXW-6V.js} +1 -1
- package/core/built/admin/assets/dist-BUKmD51u.js +1 -0
- package/core/built/admin/assets/{dist-2Tnz2Yfq.js → dist-CNlMj7ML.js} +1 -1
- package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
- package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS0.js} +1 -1
- package/core/built/admin/assets/{domain-CPNXKVOV.js → domain-B5ogXUBa.js} +1 -1
- package/core/built/admin/assets/{dropzone-BmCXnw6J.js → dropzone-BnFG7dDP.js} +1 -1
- package/core/built/admin/assets/{edit-profile-SGrJve5M.js → edit-profile-B07jCZy9.js} +1 -1
- package/core/built/admin/assets/editor-DDYSOXGN.js +8 -0
- package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
- package/core/built/admin/assets/{explore-CUYLv9G9.js → explore-Ca1y8HW_.js} +1 -1
- package/core/built/admin/assets/{feed-CPdJMttL.js → feed-x4iFMBP4.js} +1 -1
- package/core/built/admin/assets/field-ChCAabZg.js +1 -0
- package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
- package/core/built/admin/assets/{filters-bXkkWro8.js → filters-CLehPo_D.js} +1 -1
- package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
- package/core/built/admin/assets/{ghost-05f9a7182ed56903dba05878a5b8422b.js → ghost-7f01131115186d1af84ead76e79646dd.js} +275 -255
- package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
- package/core/built/admin/assets/gift-link-modal-Buqh2vXh.js +1 -0
- package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
- package/core/built/admin/assets/growth-DajOBdN0.js +1 -0
- package/core/built/admin/assets/growth-Dy2UOSay.js +3 -0
- package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
- package/core/built/admin/assets/{inbox-Xx1CdnfO.js → inbox-BUOLDoji.js} +13 -13
- package/core/built/admin/assets/index-08n05-rc.js +67 -0
- package/core/built/admin/assets/index-DgUzgFvS.css +1 -0
- package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
- package/core/built/admin/assets/input-group-uSK3XT7b.js +1 -0
- package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9132 -9087
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +121 -121
- package/core/built/admin/assets/koenig-lexical/style.css +1 -1
- package/core/built/admin/assets/{koenig-lexical-B61IALf5.js → koenig-lexical-BFc4yFed.js} +82 -82
- package/core/built/admin/assets/{kpi-card-DCdlyaSS.js → kpi-card-BN-E6WYX.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BlZaG1Lo.js → kpi-card-CCt-qeWD.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-DpAuS1Nb.js → kpi-helpers-DlxTbTs4.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-Dme44bQS.js → kpi-tabs-BlcUttpn.js} +1 -1
- package/core/built/admin/assets/{label-dSs5aepE.js → label-AVrxwmsz.js} +1 -1
- package/core/built/admin/assets/label-picker-DVnpu_IF.js +1 -0
- package/core/built/admin/assets/labels-DAIqKed7.js +1 -0
- package/core/built/admin/assets/layout-B0YLh7R9.js +1 -0
- package/core/built/admin/assets/link-helpers-D9LsDOkG.js +1 -0
- package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
- package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +1 -0
- package/core/built/admin/assets/member-detail-CIVAdKXf.js +1 -0
- package/core/built/admin/assets/members-oLPiSkXD.js +10 -0
- package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
- package/core/built/admin/assets/moderation-CQ3urTFE.js +1 -0
- package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
- package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
- package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
- package/core/built/admin/assets/note-Dk877BTv.js +1 -0
- package/core/built/admin/assets/notifications-CPlLjPxO.js +1 -0
- package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
- package/core/built/admin/assets/onboarding-route-DdStOtjy.js +1 -0
- package/core/built/admin/assets/overview--4V-kx9c.js +1 -0
- package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
- package/core/built/admin/assets/{page-header-op0eN36I.js → page-header-pp4dWmOS.js} +1 -1
- package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
- package/core/built/admin/assets/{post-analytics-WwRDEnrV.js → post-analytics-BMJrI81R.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-Btml5vJI.js +1 -0
- package/core/built/admin/assets/{post-share-modal-Dx-MEjhI.js → post-share-modal-AcUZkbfL.js} +1 -1
- package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
- package/core/built/admin/assets/preferences-Ru4CRPHC.js +5 -0
- package/core/built/admin/assets/profile-BNb1oj7D.js +1 -0
- package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
- package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-BmEZf6c6.js} +1 -1
- package/core/built/admin/assets/select-B0HteHrp.js +1 -0
- package/core/built/admin/assets/settings-B1C9Q3wo.js +4 -0
- package/core/built/admin/assets/{settings-H6HxFgfI.js → settings-BGgk7oCI.js} +1 -1
- package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
- package/core/built/admin/assets/{share-modal-BJCadJyp.js → share-modal-BeuydaBf.js} +1 -1
- package/core/built/admin/assets/social-urls-DA2lXrhW.js +8 -0
- package/core/built/admin/assets/{sort-button-DyYlfYp7.js → sort-button-Df4t2AL8.js} +1 -1
- package/core/built/admin/assets/{source-icon-_ubRDbg_.js → source-icon-ajoaKb41.js} +1 -1
- package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
- package/core/built/admin/assets/{stats-BQZMGMvn.js → stats-C_lfxZAw.js} +1 -1
- package/core/built/admin/assets/{stats-view-Doei-qCX.js → stats-view-Cxot1iD8.js} +1 -1
- package/core/built/admin/assets/{step-1-IKiLBPoC.js → step-1-ogAA4lDr.js} +1 -1
- package/core/built/admin/assets/{step-2-jJ41PsDN.js → step-2-C5Xini-7.js} +1 -1
- package/core/built/admin/assets/{step-3-CaqFAEqf.js → step-3-CZv1FV81.js} +2 -2
- package/core/built/admin/assets/{switch-t-WyENU2.js → switch-CyQcwMAv.js} +1 -1
- package/core/built/admin/assets/{table-DxUL78jU.js → table-DFj1wiYv.js} +1 -1
- package/core/built/admin/assets/tabs-D4y1gqUd.js +1 -0
- package/core/built/admin/assets/{tags-CNujGwkT.js → tags-Dmft-Y4O.js} +1 -1
- package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
- package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
- package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
- package/core/built/admin/assets/{toggle-group-BeEpQ69p.js → toggle-group-R2QMFvXW.js} +1 -1
- package/core/built/admin/assets/{topic-filter-Bb5MOD-1.js → topic-filter-DGDxDalo.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-Cds6XDjg.js → use-analytics-data-C76v4LIp.js} +1 -1
- package/core/built/admin/assets/use-filter-params-WrdhAISv.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-D8uf98-G.js → use-growth-stats-aNXJOmQr.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-CRtgyuXB.js → use-post-referrers-CG0SZioj.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DbKYX50v.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-DC01kpo0.js → use-simple-pagination-DgcF69KI.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-C7bQRJWw.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-Az4tam7_.js → use-tinybird-token-C9fPjVM_.js} +1 -1
- package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
- package/core/built/admin/assets/{virtual-list-Dhuh9fGx.js → virtual-list-BmTUzeoF.js} +1 -1
- package/core/built/admin/assets/web-BqW0b0_C.js +1 -0
- package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
- package/core/built/admin/index.html +18 -18
- package/core/frontend/services/data/fetch-data.js +10 -5
- package/core/frontend/services/proxy.js +7 -8
- package/core/frontend/services/routing/api-adapter.js +115 -0
- package/core/frontend/services/routing/api-adapter.ts +157 -0
- package/core/frontend/services/routing/collection-router.js +5 -3
- package/core/frontend/services/routing/controllers/index.js +1 -1
- package/core/frontend/services/routing/controllers/rss.js +26 -31
- package/core/frontend/services/routing/controllers/rss.ts +66 -0
- package/core/frontend/services/routing/controllers/static.js +8 -7
- package/core/frontend/services/routing/parent-router.js +17 -4
- package/core/frontend/services/routing/permalink-adapter.js +26 -0
- package/core/frontend/services/routing/permalink-adapter.ts +23 -0
- package/core/frontend/services/routing/registry.js +59 -33
- package/core/frontend/services/routing/router-manager.js +14 -18
- package/core/frontend/services/routing/static-routes-router.js +11 -13
- package/core/frontend/services/routing/taxonomy-router.js +6 -2
- package/core/server/api/endpoints/automation-action-links.js +68 -0
- package/core/server/api/endpoints/automation-action-links.ts +46 -0
- package/core/server/api/endpoints/index.js +4 -0
- package/core/server/api/endpoints/settings.js +0 -2
- package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
- package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
- package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
- package/core/server/data/migrations/versions/6.56/2026-08-03-19-27-51-add-automation-run-step-id-to-automated-email-recipients.js +8 -0
- package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
- package/core/server/data/schema/commands.js +5 -0
- package/core/server/data/schema/default-settings/default-settings.json +0 -4
- package/core/server/data/schema/fixtures/fixtures.json +2 -2
- package/core/server/data/schema/schema.js +6 -1
- package/core/server/models/post.js +10 -1
- package/core/server/services/automations/automations-api.js +15 -0
- package/core/server/services/automations/automations-api.ts +19 -0
- package/core/server/services/automations/automations-repository.ts +20 -0
- package/core/server/services/automations/database-automations-repository.js +132 -30
- package/core/server/services/automations/database-automations-repository.ts +141 -12
- package/core/server/services/automations/database-date.js +10 -0
- package/core/server/services/automations/database-date.ts +5 -0
- package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
- package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
- package/core/server/services/automations/poll.js +7 -2
- package/core/server/services/automations/poll.ts +10 -2
- package/core/server/services/automations/scheduler-poll-time.js +40 -0
- package/core/server/services/automations/scheduler-poll-time.ts +38 -0
- package/core/server/services/automations/service.js +19 -3
- package/core/server/services/automations/service.ts +20 -3
- package/core/server/services/comments/comments-service-emails.js +10 -7
- package/core/server/services/comments/comments-service.js +9 -4
- package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
- package/core/server/services/email-service/email-service-wrapper.js +6 -1
- package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
- package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
- package/core/server/services/gift-links/CONTEXT.md +9 -0
- package/core/server/services/gifts/CONTEXT.md +55 -0
- package/core/server/services/gifts/README.md +28 -0
- package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
- package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
- package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
- package/core/server/services/gifts/gift-checkout-offer.js +74 -0
- package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
- package/core/server/services/gifts/gift-codec.js +63 -0
- package/core/server/services/gifts/gift-codec.ts +62 -0
- package/core/server/services/gifts/gift-controller.js +6 -31
- package/core/server/services/gifts/gift-controller.ts +6 -69
- package/core/server/services/gifts/gift-email-service.ts +3 -2
- package/core/server/services/gifts/gift-schema.js +34 -0
- package/core/server/services/gifts/gift-schema.ts +39 -0
- package/core/server/services/gifts/gift-service-wrapper.js +13 -11
- package/core/server/services/gifts/gift-service.js +265 -52
- package/core/server/services/gifts/gift-service.ts +431 -97
- package/core/server/services/gifts/gift.ts +16 -39
- package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
- package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
- package/core/server/services/link-redirection/link-redirect.js +2 -0
- package/core/server/services/link-redirection/link-redirect.ts +3 -1
- package/core/server/services/link-redirection/link-redirects-service.js +38 -2
- package/core/server/services/link-tracking/click-event.js +3 -0
- package/core/server/services/link-tracking/index.js +38 -6
- package/core/server/services/link-tracking/link-click-repository.js +35 -9
- package/core/server/services/link-tracking/link-click-tracking-service.js +54 -4
- package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +20 -4
- package/core/server/services/member-welcome-emails/service.js +15 -3
- package/core/server/services/members/api.js +1 -2
- package/core/server/services/members/import-export/csv/formula.js +16 -0
- package/core/server/services/members/import-export/csv/formula.ts +14 -0
- package/core/server/services/members/import-export/csv/index.js +3 -1
- package/core/server/services/members/import-export/csv/index.ts +1 -0
- package/core/server/services/members/import-export/import/completion-email.js +22 -3
- package/core/server/services/members/import-export/import/completion-email.ts +29 -7
- package/core/server/services/members/import-export/import/importer.js +25 -4
- package/core/server/services/members/import-export/import/importer.ts +44 -5
- package/core/server/services/members/import-export/index.js +18 -1
- package/core/server/services/members/import-export/index.ts +32 -3
- package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
- package/core/server/services/members/members-api/members-api.js +13 -7
- package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
- package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
- package/core/server/services/members/members-api/services/payments-service.js +6 -68
- package/core/server/services/members/service.js +8 -1
- package/core/server/services/members-custom-fields/schema.js +8 -1
- package/core/server/services/members-custom-fields/schema.ts +8 -0
- package/core/server/services/members-custom-fields/values-service.js +136 -8
- package/core/server/services/members-custom-fields/values-service.ts +152 -10
- package/core/server/services/mentions/service.js +5 -1
- package/core/server/services/oembed/oembed-service.js +44 -3
- package/core/server/services/posts/posts-service.js +11 -0
- package/core/server/services/route-settings/dynamic-routing-service.js +21 -52
- package/core/server/services/route-settings/index.js +1 -16
- package/core/server/services/route-settings/route-settings-parser.js +25 -25
- package/core/server/services/route-settings/route-settings-parser.ts +27 -27
- package/core/server/services/route-settings/validation-errors.js +68 -4
- package/core/server/services/route-settings/validation-errors.ts +79 -4
- package/core/server/services/settings/settings-service.js +0 -18
- package/core/server/services/stats/posts-stats-service.js +27 -23
- package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
- package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
- package/core/server/services/stripe/stripe-api.js +10 -0
- package/core/server/services/url/url-service-facade.js +1 -1
- package/core/server/services/url/url-service-facade.ts +8 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/gift-preview/controller.js +10 -25
- package/core/shared/labs.js +2 -2
- package/package.json +8 -7
- package/pnpm-lock.yaml +28 -18
- package/pnpm-workspace.yaml +6 -4
- package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
- package/core/built/admin/assets/Growth-BTP1LthE.js +0 -3
- package/core/built/admin/assets/Newsletters-CSEX0myc.js +0 -1
- package/core/built/admin/assets/Overview-BjACEsJF.js +0 -1
- package/core/built/admin/assets/Web-CxxxjCcv.js +0 -2
- package/core/built/admin/assets/activitypub/layout-CziktJXo.js +0 -1
- package/core/built/admin/assets/audience-X_XsZivI.js +0 -1
- package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +0 -25
- package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +0 -27
- package/core/built/admin/assets/comments-CvyXxOyN.js +0 -1
- package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
- package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
- package/core/built/admin/assets/editor-BOG2YPUm.js +0 -8
- package/core/built/admin/assets/field-D0XISCK2.js +0 -1
- package/core/built/admin/assets/filter-sources-Bieps75g.js +0 -1
- package/core/built/admin/assets/filters-CuB-A7ma.js +0 -1
- package/core/built/admin/assets/gift-link-modal-su8MsRdg.js +0 -1
- package/core/built/admin/assets/growth-Ba6S5FQX.js +0 -1
- package/core/built/admin/assets/index-D8PbzuT9.css +0 -1
- package/core/built/admin/assets/index-ukW_0Uie.js +0 -67
- package/core/built/admin/assets/input-group-CGOXcy4l.js +0 -1
- package/core/built/admin/assets/label-picker-Uq91atex.js +0 -1
- package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
- package/core/built/admin/assets/layout-WUcz6tu1.js +0 -1
- package/core/built/admin/assets/link-helpers-tOlxmuH5.js +0 -1
- package/core/built/admin/assets/member-custom-fields-NCzrMuba.js +0 -1
- package/core/built/admin/assets/member-detail-DPWDtYnN.js +0 -1
- package/core/built/admin/assets/members-B9rO7qr4.js +0 -10
- package/core/built/admin/assets/modals-CxRaan1J.js +0 -31
- package/core/built/admin/assets/moderation-D8zon60E.js +0 -1
- package/core/built/admin/assets/multi-select-combobox-1wYm3nle.js +0 -1
- package/core/built/admin/assets/newsletter-DbYMRxRS.js +0 -1
- package/core/built/admin/assets/note-PiWPnnrh.js +0 -1
- package/core/built/admin/assets/notifications-DEgqqB9G.js +0 -1
- package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
- package/core/built/admin/assets/onboarding-route-BzF6HeVn.js +0 -1
- package/core/built/admin/assets/overview-DixLeIqY.js +0 -1
- package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-BJCPLMIb.js +0 -1
- package/core/built/admin/assets/preferences-D7-j6A4C.js +0 -5
- package/core/built/admin/assets/profile-Dl69QPp0.js +0 -1
- package/core/built/admin/assets/select-C1qDZnW4.js +0 -1
- package/core/built/admin/assets/settings-BbmC_Iqq.js +0 -4
- package/core/built/admin/assets/social-urls-BPL1nADp.js +0 -8
- package/core/built/admin/assets/tabs-BQcJQ1ke.js +0 -1
- package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
- package/core/built/admin/assets/use-filter-params-DjXkhO54.js +0 -1
- package/core/built/admin/assets/web-LtHNiNBV.js +0 -1
- package/core/server/services/gifts/gift-repository.js +0 -2
- package/core/server/services/gifts/gift-repository.ts +0 -29
- package/core/server/services/route-settings/activation-bridge.js +0 -164
- package/core/server/services/route-settings/activation-bridge.ts +0 -210
- package/core/server/services/route-settings/default-settings-manager.js +0 -63
- package/core/server/services/route-settings/route-settings.js +0 -170
- package/core/server/services/route-settings/settings-loader.js +0 -60
- package/core/server/services/route-settings/settings-path-manager.js +0 -47
- package/core/server/services/route-settings/validate.js +0 -440
|
@@ -4,7 +4,9 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import ObjectId from 'bson-objectid';
|
|
5
5
|
import {dequal} from 'dequal';
|
|
6
6
|
import {type Knex} from 'knex';
|
|
7
|
-
|
|
7
|
+
// @ts-expect-error This module currently lacks type definitions.
|
|
8
|
+
import lexicalLib from '../../lib/lexical';
|
|
9
|
+
import urlUtils from '../../../shared/url-utils';
|
|
8
10
|
import {DEFAULT_EMAIL_DESIGN_SETTING_SLUG, MEMBER_WELCOME_EMAIL_SLUGS} from '../member-welcome-emails/constants';
|
|
9
11
|
import type {
|
|
10
12
|
AutomatedEmailEvents,
|
|
@@ -19,11 +21,11 @@ import type {
|
|
|
19
21
|
EditAutomationData,
|
|
20
22
|
Page
|
|
21
23
|
} from './automations-repository';
|
|
24
|
+
import {toDatabaseDate} from './database-date';
|
|
22
25
|
import {getStaleLockCutoff} from './stale-lock-cutoff';
|
|
23
26
|
import type {ExclusifyUnion, ReadonlyDeep} from 'type-fest';
|
|
24
27
|
|
|
25
28
|
const HOUR_MS = 60 * 60 * 1000;
|
|
26
|
-
const DATABASE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
|
27
29
|
const DEFAULT_WELCOME_EMAIL_AUTOMATIONS = [{
|
|
28
30
|
name: 'Free member welcome flow',
|
|
29
31
|
slug: MEMBER_WELCOME_EMAIL_SLUGS.free
|
|
@@ -50,9 +52,6 @@ interface AutomationRow {
|
|
|
50
52
|
updated_at: string;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
function toDatabaseDate(date: Date | string): string {
|
|
54
|
-
return moment(date).format(DATABASE_DATE_FORMAT);
|
|
55
|
-
}
|
|
56
55
|
|
|
57
56
|
interface ActionRow {
|
|
58
57
|
id: string;
|
|
@@ -70,6 +69,11 @@ type ActionStatsRow = {
|
|
|
70
69
|
email_opened_count: number | null;
|
|
71
70
|
};
|
|
72
71
|
|
|
72
|
+
type ActionLinkRow = {
|
|
73
|
+
url: string;
|
|
74
|
+
clicked_count: string | number;
|
|
75
|
+
};
|
|
76
|
+
|
|
73
77
|
type ActionRevisionRow = {
|
|
74
78
|
action_id: string;
|
|
75
79
|
created_at: string;
|
|
@@ -166,6 +170,37 @@ export function createDatabaseAutomationsRepository({
|
|
|
166
170
|
});
|
|
167
171
|
},
|
|
168
172
|
|
|
173
|
+
async getAutomationActionLinks(automationId, actionId) {
|
|
174
|
+
const action = await knex('automation_actions')
|
|
175
|
+
.select('id')
|
|
176
|
+
.where({
|
|
177
|
+
id: actionId,
|
|
178
|
+
automation_id: automationId
|
|
179
|
+
})
|
|
180
|
+
.whereNull('deleted_at')
|
|
181
|
+
.first();
|
|
182
|
+
|
|
183
|
+
if (!action) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rows = await knex('redirects as redirects')
|
|
188
|
+
.countDistinct({clicked_count: 'members_click_events.member_id'})
|
|
189
|
+
.select<ActionLinkRow[]>(knex.raw('MIN(??) as ??', ['redirects.to', 'url']))
|
|
190
|
+
.innerJoin('automation_action_revisions as revisions', 'revisions.id', 'redirects.automation_action_revision_id')
|
|
191
|
+
.leftJoin('members_click_events', 'members_click_events.redirect_id', 'redirects.id')
|
|
192
|
+
.where('revisions.action_id', actionId)
|
|
193
|
+
.whereNotNull('redirects.to_hash')
|
|
194
|
+
.groupBy('redirects.to_hash')
|
|
195
|
+
.orderBy('clicked_count', 'desc')
|
|
196
|
+
.orderBy('url', 'asc');
|
|
197
|
+
|
|
198
|
+
return rows.map(row => ({
|
|
199
|
+
url: urlUtils.transformReadyToAbsolute(row.url),
|
|
200
|
+
clicked_count: Number(row.clicked_count)
|
|
201
|
+
}));
|
|
202
|
+
},
|
|
203
|
+
|
|
169
204
|
async edit(id: string, data: EditAutomationData): Promise<Automation | null> {
|
|
170
205
|
return await knex.transaction(async (trx) => {
|
|
171
206
|
const automation = await loadAutomation(trx, id);
|
|
@@ -241,6 +276,7 @@ export function createDatabaseAutomationsRepository({
|
|
|
241
276
|
member_email: options.memberEmail,
|
|
242
277
|
member_name: options.memberName,
|
|
243
278
|
automation_action_revision_id: options.automationActionRevisionId,
|
|
279
|
+
automation_run_step_id: options.automationRunStepId,
|
|
244
280
|
...(options.mailgunMessageId ? {mailgun_message_id: options.mailgunMessageId} : {}),
|
|
245
281
|
track_clicks: options.trackClicks,
|
|
246
282
|
track_opens: options.trackOpens,
|
|
@@ -266,6 +302,15 @@ export function createDatabaseAutomationsRepository({
|
|
|
266
302
|
}
|
|
267
303
|
|
|
268
304
|
await knex.transaction(async (trx) => {
|
|
305
|
+
const revisionIds = new Set<string>();
|
|
306
|
+
for (const {openedAt, automationActionRevisionId} of eventsByAutomatedEmailRecipientId.values()) {
|
|
307
|
+
if (openedAt) {
|
|
308
|
+
revisionIds.add(automationActionRevisionId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const orderedRevisionIds = await lockActionRevisions(trx, revisionIds);
|
|
313
|
+
|
|
269
314
|
const notYetOpened = await lockNotYetOpened(trx, eventsByAutomatedEmailRecipientId);
|
|
270
315
|
const newOpensPerRevision = new Map<string, number>();
|
|
271
316
|
|
|
@@ -292,11 +337,11 @@ export function createDatabaseAutomationsRepository({
|
|
|
292
337
|
}
|
|
293
338
|
}
|
|
294
339
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
340
|
+
for (const id of orderedRevisionIds) {
|
|
341
|
+
const opens = newOpensPerRevision.get(id);
|
|
342
|
+
if (!opens) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
300
345
|
await trx('automation_action_revisions')
|
|
301
346
|
.where({id})
|
|
302
347
|
.update({
|
|
@@ -304,10 +349,77 @@ export function createDatabaseAutomationsRepository({
|
|
|
304
349
|
});
|
|
305
350
|
}
|
|
306
351
|
});
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
async trackEmailClicked({automationActionRevisionId, automationRunStepId, memberId, clickedAt}, {transacting} = {}) {
|
|
355
|
+
const trackClick = async (trx: Knex.Transaction) => {
|
|
356
|
+
await lockActionRevisions(trx, [automationActionRevisionId]);
|
|
357
|
+
|
|
358
|
+
const recipient = await trx('automated_email_recipients')
|
|
359
|
+
.select('id', 'clicked_at')
|
|
360
|
+
.where({
|
|
361
|
+
automation_action_revision_id: automationActionRevisionId,
|
|
362
|
+
automation_run_step_id: automationRunStepId,
|
|
363
|
+
member_id: memberId,
|
|
364
|
+
track_clicks: true
|
|
365
|
+
})
|
|
366
|
+
.forUpdate()
|
|
367
|
+
.first();
|
|
368
|
+
|
|
369
|
+
if (!recipient || recipient.clicked_at !== null) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const updated = await trx('automated_email_recipients')
|
|
374
|
+
.where({id: recipient.id})
|
|
375
|
+
.whereNull('clicked_at')
|
|
376
|
+
.update({clicked_at: clickedAt});
|
|
377
|
+
|
|
378
|
+
if (updated === 0) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await trx('automation_action_revisions')
|
|
383
|
+
.where({id: automationActionRevisionId})
|
|
384
|
+
.update({
|
|
385
|
+
email_clicked_count: trx.raw('COALESCE(email_clicked_count, 0) + 1')
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (transacting) {
|
|
391
|
+
await trackClick(transacting);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await knex.transaction(trackClick);
|
|
307
396
|
}
|
|
308
397
|
};
|
|
309
398
|
}
|
|
310
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Lock revisions before recipients because inserting a recipient takes a shared
|
|
402
|
+
* foreign-key lock on its revision. Updating that revision later can deadlock
|
|
403
|
+
* with another transaction that has already locked the revision and is waiting
|
|
404
|
+
* for the recipient. Keep multi-revision lock acquisition deterministic.
|
|
405
|
+
*/
|
|
406
|
+
async function lockActionRevisions(
|
|
407
|
+
trx: Knex.Transaction,
|
|
408
|
+
revisionIds: Iterable<string>
|
|
409
|
+
): Promise<string[]> {
|
|
410
|
+
const sortedRevisionIds = [...new Set(revisionIds)]
|
|
411
|
+
.sort((left, right) => left.localeCompare(right));
|
|
412
|
+
|
|
413
|
+
if (sortedRevisionIds.length > 0) {
|
|
414
|
+
await trx('automation_action_revisions')
|
|
415
|
+
.select('id')
|
|
416
|
+
.whereIn('id', sortedRevisionIds)
|
|
417
|
+
.forUpdate();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return sortedRevisionIds;
|
|
421
|
+
}
|
|
422
|
+
|
|
311
423
|
/**
|
|
312
424
|
* Which of these recipients have yet to open, and so should count towards their
|
|
313
425
|
* revision's open count. Locks them for the transaction, so a worker racing on
|
|
@@ -921,7 +1033,7 @@ async function updateAutomation(trx: Knex.Transaction, automation: AutomationRow
|
|
|
921
1033
|
async function replaceAutomationGraph(trx: Knex.Transaction, automationId: string, submittedActions: AutomationAction[], edges: AutomationEdge[]): Promise<void> {
|
|
922
1034
|
const existingActions = await loadAutomationActionRows(trx, automationId);
|
|
923
1035
|
const existingActionById = new Map(existingActions.map(action => [action.id, action]));
|
|
924
|
-
const actions = await resolveEmailDesignSettingIds(trx, submittedActions);
|
|
1036
|
+
const actions = (await resolveEmailDesignSettingIds(trx, submittedActions)).map(formatActionOnWrite);
|
|
925
1037
|
const submittedActionIds = new Set(actions.map(action => action.id));
|
|
926
1038
|
const actionIdsWithOwners = await loadActionIdsWithOwners(trx, [...submittedActionIds]);
|
|
927
1039
|
const latestRevisionByActionId = new Map((await loadLatestActionRevisions(trx, [...submittedActionIds])).map(revision => [revision.action_id, revision]));
|
|
@@ -982,6 +1094,23 @@ async function replaceAutomationGraph(trx: Knex.Transaction, automationId: strin
|
|
|
982
1094
|
await insertActionEdges(trx, edges);
|
|
983
1095
|
}
|
|
984
1096
|
|
|
1097
|
+
function formatActionOnWrite(action: AutomationAction): AutomationAction {
|
|
1098
|
+
if (action.type !== 'send_email') {
|
|
1099
|
+
return action;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return {
|
|
1103
|
+
...action,
|
|
1104
|
+
data: {
|
|
1105
|
+
...action.data,
|
|
1106
|
+
email_lexical: urlUtils.lexicalToTransformReady(action.data.email_lexical, {
|
|
1107
|
+
nodes: lexicalLib.nodes,
|
|
1108
|
+
transformMap: lexicalLib.urlTransformMap
|
|
1109
|
+
})
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
985
1114
|
async function resolveEmailDesignSettingIds(trx: Knex.Transaction, actions: ReadonlyArray<AutomationAction>): Promise<AutomationAction[]> {
|
|
986
1115
|
if (!actions.some(action => action.type === 'send_email' && action.data.email_design_setting_id === DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE)) {
|
|
987
1116
|
return [...actions];
|
|
@@ -1312,7 +1441,7 @@ function buildActionPayload(row: ActionRow, stats: AutomationEmailStats | null):
|
|
|
1312
1441
|
type: 'send_email',
|
|
1313
1442
|
data: {
|
|
1314
1443
|
email_subject: requireValue(row, 'email_subject'),
|
|
1315
|
-
email_lexical: requireValue(row, 'email_lexical'),
|
|
1444
|
+
email_lexical: urlUtils.transformReadyToAbsolute(requireValue(row, 'email_lexical')),
|
|
1316
1445
|
email_design_setting_id: requireValue(row, 'email_design_setting_id')
|
|
1317
1446
|
},
|
|
1318
1447
|
stats: stats ?? EMPTY_EMAIL_STATS
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.toDatabaseDate = void 0;
|
|
7
|
+
const moment_1 = __importDefault(require("moment"));
|
|
8
|
+
const DATABASE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
|
9
|
+
const toDatabaseDate = (date) => (0, moment_1.default)(date).format(DATABASE_DATE_FORMAT);
|
|
10
|
+
exports.toDatabaseDate = toDatabaseDate;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getSchedulerIdempotencyKey = getSchedulerIdempotencyKey;
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
|
+
function getSchedulerIdempotencyKey(date, url) {
|
|
9
|
+
const hash = node_crypto_1.default.createHash('sha256');
|
|
10
|
+
hash.update(date.toISOString());
|
|
11
|
+
hash.update(url.href);
|
|
12
|
+
return `ghost-automations-${hash.digest('hex')}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function getSchedulerIdempotencyKey(date: Readonly<Date>, url: Readonly<URL>): string {
|
|
4
|
+
const hash = crypto.createHash('sha256');
|
|
5
|
+
hash.update(date.toISOString());
|
|
6
|
+
hash.update(url.href);
|
|
7
|
+
return `ghost-automations-${hash.digest('hex')}`;
|
|
8
|
+
}
|
|
@@ -12,6 +12,7 @@ const constants_2 = require("./constants");
|
|
|
12
12
|
// @ts-expect-error Models currently lack type definitions.
|
|
13
13
|
const models_1 = require("../../models");
|
|
14
14
|
const settingsCache = require('../../../shared/settings-cache');
|
|
15
|
+
const labs = require('../../../shared/labs');
|
|
15
16
|
const slugToMemberStatus = new Map(Object.entries(constants_1.MEMBER_WELCOME_EMAIL_SLUGS).map(([status, slug]) => [slug, status]));
|
|
16
17
|
const hasUpdatesAndAnnouncementsEnabled = (member) => {
|
|
17
18
|
const preference = member.get('enable_updates_and_announcements');
|
|
@@ -112,7 +113,7 @@ const processStep = async ({ automationsApi, memberWelcomeEmailService, schedule
|
|
|
112
113
|
break;
|
|
113
114
|
}
|
|
114
115
|
memberWelcomeEmailService.init();
|
|
115
|
-
const trackClicks = Boolean(settingsCache.get('email_track_clicks'));
|
|
116
|
+
const trackClicks = labs.isSet('automationAnalytics') && Boolean(settingsCache.get('email_track_clicks'));
|
|
116
117
|
const trackOpens = Boolean(settingsCache.get('email_track_opens'));
|
|
117
118
|
const sendResult = await memberWelcomeEmailService.api.sendAutomationEmail({
|
|
118
119
|
email: {
|
|
@@ -126,7 +127,10 @@ const processStep = async ({ automationsApi, memberWelcomeEmailService, schedule
|
|
|
126
127
|
uuid: member.get('uuid')
|
|
127
128
|
},
|
|
128
129
|
memberStatus,
|
|
129
|
-
trackOpens
|
|
130
|
+
trackOpens,
|
|
131
|
+
trackClicks,
|
|
132
|
+
automationActionRevisionId: step.automation_action_revision_id,
|
|
133
|
+
automationRunStepId: step.id
|
|
130
134
|
});
|
|
131
135
|
const mailgunMessageId = (0, mailgun_message_id_1.getMailgunMessageId)(sendResult);
|
|
132
136
|
// Only Mailgun sends can produce open events for automation emails
|
|
@@ -134,6 +138,7 @@ const processStep = async ({ automationsApi, memberWelcomeEmailService, schedule
|
|
|
134
138
|
try {
|
|
135
139
|
await automationsApi.recordEmailSent({
|
|
136
140
|
automationActionRevisionId: step.automation_action_revision_id,
|
|
141
|
+
automationRunStepId: step.id,
|
|
137
142
|
...(mailgunMessageId ? { mailgunMessageId } : {}),
|
|
138
143
|
memberEmail: member.get('email'),
|
|
139
144
|
memberId: step.member_id,
|
|
@@ -8,6 +8,7 @@ import {MAX_ATTEMPTS, MAX_STEPS_PER_BATCH, RETRY_DELAY_MS} from './constants';
|
|
|
8
8
|
import {Member} from '../../models';
|
|
9
9
|
|
|
10
10
|
const settingsCache = require('../../../shared/settings-cache');
|
|
11
|
+
const labs = require('../../../shared/labs');
|
|
11
12
|
|
|
12
13
|
type MemberWelcomeEmailService = {
|
|
13
14
|
init: () => unknown;
|
|
@@ -25,6 +26,9 @@ type MemberWelcomeEmailService = {
|
|
|
25
26
|
};
|
|
26
27
|
memberStatus: 'free' | 'paid';
|
|
27
28
|
trackOpens: boolean;
|
|
29
|
+
trackClicks: boolean;
|
|
30
|
+
automationActionRevisionId: string;
|
|
31
|
+
automationRunStepId: string;
|
|
28
32
|
}) => Promise<unknown>;
|
|
29
33
|
};
|
|
30
34
|
};
|
|
@@ -183,7 +187,7 @@ const processStep = async ({
|
|
|
183
187
|
break;
|
|
184
188
|
}
|
|
185
189
|
memberWelcomeEmailService.init();
|
|
186
|
-
const trackClicks = Boolean(settingsCache.get('email_track_clicks'));
|
|
190
|
+
const trackClicks = labs.isSet('automationAnalytics') && Boolean(settingsCache.get('email_track_clicks'));
|
|
187
191
|
const trackOpens = Boolean(settingsCache.get('email_track_opens'));
|
|
188
192
|
const sendResult = await memberWelcomeEmailService.api.sendAutomationEmail({
|
|
189
193
|
email: {
|
|
@@ -197,7 +201,10 @@ const processStep = async ({
|
|
|
197
201
|
uuid: member.get('uuid')
|
|
198
202
|
},
|
|
199
203
|
memberStatus,
|
|
200
|
-
trackOpens
|
|
204
|
+
trackOpens,
|
|
205
|
+
trackClicks,
|
|
206
|
+
automationActionRevisionId: step.automation_action_revision_id,
|
|
207
|
+
automationRunStepId: step.id
|
|
201
208
|
});
|
|
202
209
|
const mailgunMessageId = getMailgunMessageId(sendResult);
|
|
203
210
|
// Only Mailgun sends can produce open events for automation emails
|
|
@@ -205,6 +212,7 @@ const processStep = async ({
|
|
|
205
212
|
try {
|
|
206
213
|
await automationsApi.recordEmailSent({
|
|
207
214
|
automationActionRevisionId: step.automation_action_revision_id,
|
|
215
|
+
automationRunStepId: step.id,
|
|
208
216
|
...(mailgunMessageId ? {mailgunMessageId} : {}),
|
|
209
217
|
memberEmail: member.get('email'),
|
|
210
218
|
memberId: step.member_id,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSchedulerPollTime = void 0;
|
|
4
|
+
const SCHEDULER_POLL_INTERVAL_MS = 15 * 60 * 1000;
|
|
5
|
+
/**
|
|
6
|
+
* An implementation of the [FNV-1a hash][0].
|
|
7
|
+
*
|
|
8
|
+
* We need something that generates per-site jitter within a 15-minute
|
|
9
|
+
* interval. We want a 32-bit number which we can modulo at the end.
|
|
10
|
+
*
|
|
11
|
+
* FNV-1a is good for this purpose because:
|
|
12
|
+
*
|
|
13
|
+
* - it is simple
|
|
14
|
+
* - it generates a 32-bit number
|
|
15
|
+
* - it is [more efficient than CRC32, MD5, and SHA][1], using less CPU and RAM
|
|
16
|
+
*
|
|
17
|
+
* This implementation assumes ASCII strings, which we expect callers to always
|
|
18
|
+
* use. If that assumption is ever violated, we'll compute incorrect jitter,
|
|
19
|
+
* which is acceptable.
|
|
20
|
+
*
|
|
21
|
+
* [0]: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash
|
|
22
|
+
* [1]: https://gist.github.com/EvanHahn/20d845c8157691d24977831159487950
|
|
23
|
+
*/
|
|
24
|
+
const fnv1a32 = (asciiStr) => {
|
|
25
|
+
let hash = 0x811c9dc5;
|
|
26
|
+
for (let i = 0; i < asciiStr.length; i++) {
|
|
27
|
+
hash ^= asciiStr.charCodeAt(i);
|
|
28
|
+
hash = Math.imul(hash, 0x01000193);
|
|
29
|
+
}
|
|
30
|
+
return hash >>> 0;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Round the time up to a 15-minute interval with site-specific jitter.
|
|
34
|
+
*/
|
|
35
|
+
const getSchedulerPollTime = (date, siteIdentifier) => {
|
|
36
|
+
const siteOffset = fnv1a32(siteIdentifier) % SCHEDULER_POLL_INTERVAL_MS;
|
|
37
|
+
const interval = Math.ceil((date.getTime() - siteOffset) / SCHEDULER_POLL_INTERVAL_MS);
|
|
38
|
+
return new Date(interval * SCHEDULER_POLL_INTERVAL_MS + siteOffset);
|
|
39
|
+
};
|
|
40
|
+
exports.getSchedulerPollTime = getSchedulerPollTime;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const SCHEDULER_POLL_INTERVAL_MS = 15 * 60 * 1000;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An implementation of the [FNV-1a hash][0].
|
|
5
|
+
*
|
|
6
|
+
* We need something that generates per-site jitter within a 15-minute
|
|
7
|
+
* interval. We want a 32-bit number which we can modulo at the end.
|
|
8
|
+
*
|
|
9
|
+
* FNV-1a is good for this purpose because:
|
|
10
|
+
*
|
|
11
|
+
* - it is simple
|
|
12
|
+
* - it generates a 32-bit number
|
|
13
|
+
* - it is [more efficient than CRC32, MD5, and SHA][1], using less CPU and RAM
|
|
14
|
+
*
|
|
15
|
+
* This implementation assumes ASCII strings, which we expect callers to always
|
|
16
|
+
* use. If that assumption is ever violated, we'll compute incorrect jitter,
|
|
17
|
+
* which is acceptable.
|
|
18
|
+
*
|
|
19
|
+
* [0]: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash
|
|
20
|
+
* [1]: https://gist.github.com/EvanHahn/20d845c8157691d24977831159487950
|
|
21
|
+
*/
|
|
22
|
+
const fnv1a32 = (asciiStr: string): number => {
|
|
23
|
+
let hash = 0x811c9dc5;
|
|
24
|
+
for (let i = 0; i < asciiStr.length; i++) {
|
|
25
|
+
hash ^= asciiStr.charCodeAt(i);
|
|
26
|
+
hash = Math.imul(hash, 0x01000193);
|
|
27
|
+
}
|
|
28
|
+
return hash >>> 0;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Round the time up to a 15-minute interval with site-specific jitter.
|
|
33
|
+
*/
|
|
34
|
+
export const getSchedulerPollTime = (date: Readonly<Date>, siteIdentifier: string): Date => {
|
|
35
|
+
const siteOffset = fnv1a32(siteIdentifier) % SCHEDULER_POLL_INTERVAL_MS;
|
|
36
|
+
const interval = Math.ceil((date.getTime() - siteOffset) / SCHEDULER_POLL_INTERVAL_MS);
|
|
37
|
+
return new Date(interval * SCHEDULER_POLL_INTERVAL_MS + siteOffset);
|
|
38
|
+
}
|
|
@@ -41,8 +41,10 @@ const strict_1 = __importDefault(require("node:assert/strict"));
|
|
|
41
41
|
const one_at_a_time_1 = require("../../../shared/one-at-a-time");
|
|
42
42
|
const poll_1 = require("./poll");
|
|
43
43
|
const automationsApi = __importStar(require("./automations-api"));
|
|
44
|
+
const get_scheduler_idempotency_key_1 = require("./get-scheduler-idempotency-key");
|
|
44
45
|
const promises_1 = require("node:timers/promises");
|
|
45
46
|
const soonest_timer_1 = require("../../lib/soonest-timer");
|
|
47
|
+
const scheduler_poll_time_1 = require("./scheduler-poll-time");
|
|
46
48
|
// @ts-expect-error This module currently lacks type definitions.
|
|
47
49
|
const jobs_1 = __importDefault(require("../email-analytics/jobs"));
|
|
48
50
|
const urlUtils = require('../../../shared/url-utils').default;
|
|
@@ -54,11 +56,13 @@ const memberWelcomeEmailService = require('../member-welcome-emails/service');
|
|
|
54
56
|
const scheduleAutomationEmailAnalyticsJob = () => (jobs_1.default.scheduleRecurringAutomationsJob(true));
|
|
55
57
|
class AutomationsService {
|
|
56
58
|
#enqueuePollAt;
|
|
57
|
-
init({ domainEvents, apiUrl, schedulerAdapter, internalKeys }) {
|
|
59
|
+
init({ domainEvents, apiUrl, schedulerAdapter, internalKeys, siteUuid }) {
|
|
58
60
|
const isInitialized = Boolean(this.#enqueuePollAt);
|
|
59
61
|
if (isInitialized) {
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
64
|
+
// If we don't get a valid site UUID for some reason, compute jitter with the API URL.
|
|
65
|
+
const siteIdentifier = typeof siteUuid === 'string' && siteUuid.length ? siteUuid : apiUrl;
|
|
62
66
|
const enqueuePollNow = () => domainEvents.dispatch(StartAutomationsPollEvent.create());
|
|
63
67
|
const soonestTimer = new soonest_timer_1.SoonestTimer(enqueuePollNow);
|
|
64
68
|
/**
|
|
@@ -86,11 +90,23 @@ class AutomationsService {
|
|
|
86
90
|
}
|
|
87
91
|
soonestTimer.scheduleAt(date);
|
|
88
92
|
try {
|
|
93
|
+
const schedulerPollTime = (0, scheduler_poll_time_1.getSchedulerPollTime)(date, siteIdentifier);
|
|
89
94
|
const key = await internalKeys.get('ghost-scheduler');
|
|
90
|
-
const signedAdminToken = getSignedAdminToken({
|
|
95
|
+
const signedAdminToken = getSignedAdminToken({
|
|
96
|
+
publishedAt: schedulerPollTime.toISOString(),
|
|
97
|
+
apiUrl,
|
|
98
|
+
key
|
|
99
|
+
});
|
|
91
100
|
const url = new URL(urlUtils.urlJoin(apiUrl, 'automations', 'poll'));
|
|
92
101
|
url.searchParams.set('token', signedAdminToken);
|
|
93
|
-
schedulerAdapter.schedule({
|
|
102
|
+
schedulerAdapter.schedule({
|
|
103
|
+
time: schedulerPollTime.getTime(),
|
|
104
|
+
url: url.toString(),
|
|
105
|
+
extra: {
|
|
106
|
+
httpMethod: 'PUT',
|
|
107
|
+
idempotencyKey: (0, get_scheduler_idempotency_key_1.getSchedulerIdempotencyKey)(date, url)
|
|
108
|
+
}
|
|
109
|
+
});
|
|
94
110
|
}
|
|
95
111
|
catch (err) {
|
|
96
112
|
logging.error({ event: { name: 'automations.enqueue-poll.error' }, err, at: date.toISOString() }, 'Failed to enqueue automations poll');
|
|
@@ -6,8 +6,10 @@ import type DomainEvents from '@tryghost/domain-events';
|
|
|
6
6
|
import {oneAtATime} from '../../../shared/one-at-a-time';
|
|
7
7
|
import {poll} from './poll';
|
|
8
8
|
import * as automationsApi from './automations-api';
|
|
9
|
+
import {getSchedulerIdempotencyKey} from './get-scheduler-idempotency-key';
|
|
9
10
|
import {setImmediate as flushEventLoop} from 'node:timers/promises';
|
|
10
11
|
import {SoonestTimer} from '../../lib/soonest-timer';
|
|
12
|
+
import {getSchedulerPollTime} from './scheduler-poll-time';
|
|
11
13
|
// @ts-expect-error This module currently lacks type definitions.
|
|
12
14
|
import emailAnalyticsJobs from '../email-analytics/jobs';
|
|
13
15
|
|
|
@@ -23,6 +25,7 @@ type AutomationsServiceOptions = {
|
|
|
23
25
|
domainEvents: Pick<DomainEvents, 'dispatch' | 'subscribe'>;
|
|
24
26
|
internalKeys: InternalKeys;
|
|
25
27
|
schedulerAdapter: Pick<SchedulerAdapter, 'schedule' | 'register'>;
|
|
28
|
+
siteUuid: unknown;
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
const scheduleAutomationEmailAnalyticsJob = () => (
|
|
@@ -32,12 +35,14 @@ const scheduleAutomationEmailAnalyticsJob = () => (
|
|
|
32
35
|
export class AutomationsService {
|
|
33
36
|
#enqueuePollAt: undefined | ((date: Readonly<Date>) => Promise<void>);
|
|
34
37
|
|
|
35
|
-
init({domainEvents, apiUrl, schedulerAdapter, internalKeys}: AutomationsServiceOptions): void {
|
|
38
|
+
init({domainEvents, apiUrl, schedulerAdapter, internalKeys, siteUuid}: AutomationsServiceOptions): void {
|
|
36
39
|
const isInitialized = Boolean(this.#enqueuePollAt);
|
|
37
40
|
if (isInitialized) {
|
|
38
41
|
return;
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
// If we don't get a valid site UUID for some reason, compute jitter with the API URL.
|
|
45
|
+
const siteIdentifier = typeof siteUuid === 'string' && siteUuid.length ? siteUuid : apiUrl;
|
|
41
46
|
const enqueuePollNow = () => domainEvents.dispatch(StartAutomationsPollEvent.create());
|
|
42
47
|
|
|
43
48
|
const soonestTimer = new SoonestTimer(enqueuePollNow);
|
|
@@ -69,11 +74,23 @@ export class AutomationsService {
|
|
|
69
74
|
soonestTimer.scheduleAt(date);
|
|
70
75
|
|
|
71
76
|
try {
|
|
77
|
+
const schedulerPollTime = getSchedulerPollTime(date, siteIdentifier);
|
|
72
78
|
const key = await internalKeys.get('ghost-scheduler');
|
|
73
|
-
const signedAdminToken = getSignedAdminToken({
|
|
79
|
+
const signedAdminToken = getSignedAdminToken({
|
|
80
|
+
publishedAt: schedulerPollTime.toISOString(),
|
|
81
|
+
apiUrl,
|
|
82
|
+
key
|
|
83
|
+
});
|
|
74
84
|
const url = new URL(urlUtils.urlJoin(apiUrl, 'automations', 'poll'));
|
|
75
85
|
url.searchParams.set('token', signedAdminToken);
|
|
76
|
-
schedulerAdapter.schedule({
|
|
86
|
+
schedulerAdapter.schedule({
|
|
87
|
+
time: schedulerPollTime.getTime(),
|
|
88
|
+
url: url.toString(),
|
|
89
|
+
extra: {
|
|
90
|
+
httpMethod: 'PUT',
|
|
91
|
+
idempotencyKey: getSchedulerIdempotencyKey(date, url)
|
|
92
|
+
}
|
|
93
|
+
});
|
|
77
94
|
} catch (err) {
|
|
78
95
|
logging.error({event: {name: 'automations.enqueue-poll.error'}, err, at: date.toISOString()}, 'Failed to enqueue automations poll');
|
|
79
96
|
}
|
|
@@ -87,12 +87,13 @@ class CommentsServiceEmails {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
/**
|
|
91
|
+
* @returns {Promise<string|undefined>} The id of the member that was notified, or undefined if no email was sent
|
|
92
|
+
*/
|
|
93
|
+
async notifyParentCommentAuthor(reply, {type = 'parent', parent = null} = {}) {
|
|
94
|
+
if (!parent) {
|
|
95
|
+
const parentId = type === 'in_reply_to' ? reply.get('in_reply_to_id') : reply.get('parent_id');
|
|
96
|
+
parent = await this.models.Comment.findOne({id: parentId});
|
|
96
97
|
}
|
|
97
98
|
const parentMember = parent.related('member');
|
|
98
99
|
|
|
@@ -140,12 +141,14 @@ class CommentsServiceEmails {
|
|
|
140
141
|
text
|
|
141
142
|
} = await this.commentsServiceEmailRenderer.renderEmailTemplate('new-comment-reply', templateData);
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
await this.sendMail({
|
|
144
145
|
to: parentMemberEmail,
|
|
145
146
|
subject,
|
|
146
147
|
html,
|
|
147
148
|
text
|
|
148
149
|
});
|
|
150
|
+
|
|
151
|
+
return parentMember.id;
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
/**
|
|
@@ -378,11 +378,16 @@ class CommentsService {
|
|
|
378
378
|
async sendNewCommentNotifications(comment) {
|
|
379
379
|
await this.emails.notifyPostAuthors(comment);
|
|
380
380
|
|
|
381
|
-
|
|
382
|
-
await this.emails.notifyParentCommentAuthor(comment, {type: 'parent'})
|
|
383
|
-
|
|
381
|
+
const notifiedMemberId = comment.get('parent_id')
|
|
382
|
+
? await this.emails.notifyParentCommentAuthor(comment, {type: 'parent'})
|
|
383
|
+
: null;
|
|
384
|
+
|
|
384
385
|
if (comment.get('in_reply_to_id')) {
|
|
385
|
-
await this.
|
|
386
|
+
const inReplyTo = await this.models.Comment.findOne({id: comment.get('in_reply_to_id')});
|
|
387
|
+
|
|
388
|
+
if (inReplyTo && (!notifiedMemberId || inReplyTo.get('member_id') !== notifiedMemberId)) {
|
|
389
|
+
await this.emails.notifyParentCommentAuthor(comment, {type: 'in_reply_to', parent: inReplyTo});
|
|
390
|
+
}
|
|
386
391
|
}
|
|
387
392
|
}
|
|
388
393
|
|
|
@@ -84,7 +84,9 @@
|
|
|
84
84
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
85
85
|
<tr>
|
|
86
86
|
<td>
|
|
87
|
-
<
|
|
87
|
+
<h1 class="post-title-heading">
|
|
88
|
+
<a href="{{post.url}}" class="{{classes.titleLink}}">{{post.title}}</a>
|
|
89
|
+
</h1>
|
|
88
90
|
</td>
|
|
89
91
|
</tr>
|
|
90
92
|
</table>
|
|
@@ -6,7 +6,12 @@ const events = require('../../lib/common/events');
|
|
|
6
6
|
class EmailServiceWrapper {
|
|
7
7
|
getPostUrl(post) {
|
|
8
8
|
const jsonModel = post.toJSON();
|
|
9
|
-
|
|
9
|
+
// The URL service routes by resource type. Pages and posts share the
|
|
10
|
+
// Post model, so the page's own type must reach forPost — otherwise it
|
|
11
|
+
// defaults to 'posts', matches no post collection, and 404s under the
|
|
12
|
+
// lazy service.
|
|
13
|
+
const type = jsonModel.type === 'page' ? 'pages' : 'posts';
|
|
14
|
+
url.forPost(post.id, jsonModel, {options: {}}, type);
|
|
10
15
|
return jsonModel.url;
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -161,6 +161,14 @@ img.is-dark-background {
|
|
|
161
161
|
color: {{postTitleColor}};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/* Outlook's Word engine ignores `inherit`, so repeat the `.post-title > table td` values */
|
|
165
|
+
.post-title-heading {
|
|
166
|
+
margin: 0;
|
|
167
|
+
font-size: 36px;
|
|
168
|
+
line-height: 1.1;
|
|
169
|
+
font-weight: {{titleWeight}};
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
.post-title a {
|
|
165
173
|
color: {{postTitleColor}} !important;
|
|
166
174
|
}
|