ghost 6.54.0 → 6.55.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/ghost-storage-base-3.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
- package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.1.1.tgz +0 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
- package/core/boot.js +3 -6
- package/core/built/admin/assets/{account-migration-DLvVA5yq.js → account-migration-sjHLx5dy.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-DHbZcxGM.js → account-migration-DoYXtnZ8.js} +4 -4
- package/core/built/admin/assets/activitypub/activitypub.js +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-zfxUey_L.js → bluesky-sharing-Bxeh6MKW.js} +4 -4
- package/core/built/admin/assets/activitypub/{dist-BqkDUQxJ2.js → dist-ByFmDSNP2.js} +64 -63
- package/core/built/admin/assets/activitypub/{dist-Ba_QxHHl.js → dist-D8sFfdpC.js} +2 -2
- package/core/built/admin/assets/activitypub/{domain-CmwSZLHK.js → domain-CWueFW6q.js} +5 -5
- package/core/built/admin/assets/activitypub/{edit-profile-CXdwNmVG.js → edit-profile-BGxPU7N5.js} +3 -3
- package/core/built/admin/assets/activitypub/{explore-DfbjzGM1.js → explore-DBK-u0Ch.js} +3 -3
- package/core/built/admin/assets/activitypub/{feed-DSEBJOqf.js → feed-Bcir5RQZ.js} +3 -3
- package/core/built/admin/assets/activitypub/{field-CY7XVcx6.js → field-BV-CIT3d.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DtQ0kytT.js → inbox-yC0nfvzl.js} +4 -4
- package/core/built/admin/assets/activitypub/{label-Dw96GQ7u.js → label-DTRS9TDk.js} +2 -2
- package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-Dg_-I-8C.js → moderation-Dwl9t0pb.js} +4 -4
- package/core/built/admin/assets/activitypub/{note-Cy1-uKMY.js → note-B-zKsZ7r.js} +3 -3
- package/core/built/admin/assets/activitypub/{notifications-B_fvoLIM.js → notifications-DjJZD54t.js} +3 -3
- package/core/built/admin/assets/activitypub/{preferences-ByJseZp6.js → preferences-CoU8vs6d.js} +6 -6
- package/core/built/admin/assets/activitypub/{profile-BFtbnIjz.js → profile-BAsaJekx.js} +6 -6
- package/core/built/admin/assets/activitypub/{routes-Dh3xXfce.js → routes-CLH3OVTP.js} +28 -25
- package/core/built/admin/assets/activitypub/{settings-DTgqJPxM.js → settings-DA6pNJ-Q.js} +3 -3
- package/core/built/admin/assets/activitypub/{site-CdL9ZLqm.js → site-CQD42Pw5.js} +2 -2
- package/core/built/admin/assets/activitypub/{tabs-ogqwoBlb.js → tabs-Cg0saow1.js} +7 -7
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-CxonteFD.js → use-reply-chain-data-Br4aOWOh.js} +2 -2
- package/core/built/admin/assets/audience-DjEWNhp3.js +1 -0
- package/core/built/admin/assets/{automated-emails-Cn87L5Md.js → automated-emails-DRy7qY-J.js} +2 -2
- package/core/built/admin/assets/{automations-BP_aHamJ.js → automations-Bx3VcFIL.js} +1 -1
- package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-Bh36XPh4.js → bluesky-sharing-ClMd0-Ft.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-D34YWnaw.js → breadcrumb-BD0U2ygg.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-DbklaJw9.js → chart-helpers-fjQKRVyY.js} +1 -1
- package/core/built/admin/assets/checkbox-CWa28lgO.js +1 -0
- package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js → chunk.211.6f7328d63c41536451b3.js} +737 -559
- package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js.LICENSE.txt → chunk.211.6f7328d63c41536451b3.js.LICENSE.txt} +2 -0
- package/core/built/admin/assets/{chunk.589.096f4d0bb775e5229aa9.js → chunk.481.6c7395a49484bb4583aa.js} +41 -38
- package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +25 -0
- package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +27 -0
- package/core/built/admin/assets/code-editor-view-CCr0rBWS.js +1 -0
- package/core/built/admin/assets/comments-DyUSMUdj.js +1 -0
- package/core/built/admin/assets/{date-range-select-B5vSsrpx.js → date-range-select-BFp58EMk.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-NkOIoMue.js → disabled-sources-indicator-CUxMfiPa.js} +1 -1
- package/core/built/admin/assets/dist-2Tnz2Yfq.js +1 -0
- package/core/built/admin/assets/dist-CjY-f5Z1.js +1 -0
- package/core/built/admin/assets/dist-DIC8yz5C.js +1 -0
- package/core/built/admin/assets/domain-C0bV2UV8.js +1 -0
- package/core/built/admin/assets/dropzone-DlK-K2DQ.js +5 -0
- package/core/built/admin/assets/{edit-profile-rvDGrS8J.js → edit-profile-BKT-REJw.js} +1 -1
- package/core/built/admin/assets/editor-Dp865O-4.js +8 -0
- package/core/built/admin/assets/{explore-BZAZOg5X.js → explore-ClUpJZOp.js} +1 -1
- package/core/built/admin/assets/{feed-CFV8xDw9.js → feed-DF92dUMP.js} +1 -1
- package/core/built/admin/assets/field-Do7lieBA.js +1 -0
- package/core/built/admin/assets/{filter-sources-Fx8wiYYw.js → filter-sources-BV2rnpfA.js} +1 -1
- package/core/built/admin/assets/filters-CUXJ2Nm9.js +1 -0
- package/core/built/admin/assets/{filters-CGX_pf2N.js → filters-DfbIWc_l.js} +1 -1
- package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css} +1 -1
- package/core/built/admin/assets/{ghost-b1dce91ad3232cc259ee5fbbf9526243.js → ghost-c4057474943effc568c5659ccc4909d7.js} +407 -406
- package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-db36d795d21c61426837df40af270966.css} +1 -1
- package/core/built/admin/assets/gift-link-modal-f9VIMNDv.js +1 -0
- package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
- package/core/built/admin/assets/growth-B3I6gNLm.js +1 -0
- package/core/built/admin/assets/{Growth-CSaHoSVD.js → growth-CVw1My4S.js} +2 -2
- package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
- package/core/built/admin/assets/{inbox-BRp1_aaC.js → inbox-CFBSu4Ed.js} +12 -12
- package/core/built/admin/assets/index-9Cw8z3rA.css +1 -0
- package/core/built/admin/assets/index-BGG7NckO.js +67 -0
- package/core/built/admin/assets/input-group-BSgG_lVw.js +1 -0
- package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +974 -937
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +28 -28
- package/core/built/admin/assets/koenig-lexical/style.css +1 -1
- package/core/built/admin/assets/{koenig-lexical-CM7UFNdL.js → koenig-lexical-f8vHRYvz.js} +28 -28
- package/core/built/admin/assets/{kpi-card-BrGH6FsT.js → kpi-card-DSap9rAy.js} +1 -1
- package/core/built/admin/assets/{kpi-card-CIGyijjx.js → kpi-card-DU0rn9Cj.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-_HRRsklE.js → kpi-helpers-Dz33AV6h.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-BSvh9eoq.js → kpi-tabs-BtTTrKvJ.js} +1 -1
- package/core/built/admin/assets/{label-BOR9XdKl.js → label-DS8Mi6G5.js} +1 -1
- package/core/built/admin/assets/label-picker-1DvSuLA1.js +1 -0
- package/core/built/admin/assets/labels-CDSUt4J_.js +1 -0
- package/core/built/admin/assets/layout-yQEi2qDM.js +1 -0
- package/core/built/admin/assets/{link-helpers-tOlxmuH5.js → link-helpers-7EYqvvHs.js} +1 -1
- package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +1 -0
- package/core/built/admin/assets/member-detail-BAq7gDaa.js +1 -0
- package/core/built/admin/assets/members-CQpVFJu4.js +10 -0
- package/core/built/admin/assets/modals-ReORvWnu.js +31 -0
- package/core/built/admin/assets/moderation-DySbgyUS.js +1 -0
- package/core/built/admin/assets/newsletter-CJQvtHqN.js +1 -0
- package/core/built/admin/assets/{Newsletters-C5cs6FGo.js → newsletters-B3QQXfGT.js} +1 -1
- package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
- package/core/built/admin/assets/note-C8kxrz6w.js +1 -0
- package/core/built/admin/assets/notifications-CgtSfX7t.js +1 -0
- package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
- package/core/built/admin/assets/onboarding-route-4qtziA9d.js +1 -0
- package/core/built/admin/assets/overview-Bf2sbznq.js +1 -0
- package/core/built/admin/assets/overview-CBfePxcr.js +1 -0
- package/core/built/admin/assets/{page-header-C0jVxtS8.js → page-header-B3Wb9Jqv.js} +1 -1
- package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
- package/core/built/admin/assets/{post-analytics-Iw7dk1mT.js → post-analytics-We-ko5Cb.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-B6UynTGb.js +1 -0
- package/core/built/admin/assets/{post-share-modal-DBfKl2GZ.js → post-share-modal-DFfO93_0.js} +1 -1
- package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
- package/core/built/admin/assets/{preferences-D4EANGoc.js → preferences-DJPfHEGu.js} +2 -2
- package/core/built/admin/assets/profile-DqBQp-gT.js +1 -0
- package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
- package/core/built/admin/assets/select-C5_xEt5b.js +1 -0
- package/core/built/admin/assets/settings-BwiVIXau.js +4 -0
- package/core/built/admin/assets/{settings-Bz3RZRmE.js → settings-WXNeWyU1.js} +1 -1
- package/core/built/admin/assets/shade-app-B3heOAfz-CGXQwWeO.js +3 -0
- package/core/built/admin/assets/{share-modal-CkdTTMJd.js → share-modal-DsgW3wbg.js} +1 -1
- package/core/built/admin/assets/social-urls-BQPFEk1_.js +8 -0
- package/core/built/admin/assets/{sort-button-DZp9Y2r1.js → sort-button-Bb_FCl7e.js} +1 -1
- package/core/built/admin/assets/{source-icon-CHr9qJ2Y.js → source-icon-BeaRxUn9.js} +1 -1
- package/core/built/admin/assets/{stats-DGoXZxWE.js → stats-Bjj_kmJ1.js} +1 -1
- package/core/built/admin/assets/{stats-view-D2-1L0ms.js → stats-view-BxPbW6K1.js} +1 -1
- package/core/built/admin/assets/{step-1-RdahbwyG.js → step-1-B1e3-1F8.js} +1 -1
- package/core/built/admin/assets/{step-2-D83PeRwd.js → step-2-C5WaeBJo.js} +1 -1
- package/core/built/admin/assets/{step-3-p_Op4wv4.js → step-3-B46iVPwu.js} +2 -2
- package/core/built/admin/assets/switch-z5Y6Uzt6.js +1 -0
- package/core/built/admin/assets/{table-B8LgTQJZ.js → table-CB-4_HBJ.js} +1 -1
- package/core/built/admin/assets/tabs-CCEgKYzq.js +1 -0
- package/core/built/admin/assets/{tags-B4l891z2.js → tags-Cg2VJGmG.js} +1 -1
- package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
- package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
- package/core/built/admin/assets/{toggle-group-DNXDkAwf.js → toggle-group-b3IlxvhV.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DZGjEcz3.js → topic-filter-DAFT87tQ.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-CaT8RirX.js → use-analytics-data-S0FTd43s.js} +1 -1
- package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-9Db2CuJe.js → use-growth-stats-CIaiHOVb.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-BV54YcQS.js → use-post-referrers-Cq2vBPrA.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DuYZGDan.js → use-reply-chain-data-DBDRtGu-.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-8fTua9Nt.js → use-simple-pagination-CjYNxdwe.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-Dz156HvZ.js → use-tinybird-query-falHAtuK.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-C9RUdL_n.js → use-tinybird-token-D7-8ND1y.js} +1 -1
- package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
- package/core/built/admin/assets/{virtual-list-BCvP0YF-.js → virtual-list-CvnC9zF2.js} +1 -1
- package/core/built/admin/assets/web-B_5GZRvH.js +1 -0
- package/core/built/admin/assets/web-jFYWCKDn.js +2 -0
- package/core/built/admin/index.html +14 -10
- package/core/frontend/services/proxy.js +10 -1
- package/core/frontend/services/routing/router-manager.js +14 -14
- package/core/frontend/services/routing/static-routes-router.js +8 -10
- package/core/server/adapters/storage/LocalFilesStorage.js +13 -11
- package/core/server/adapters/storage/LocalFilesStorage.ts +24 -0
- package/core/server/adapters/storage/LocalImagesStorage.js +14 -12
- package/core/server/adapters/storage/LocalImagesStorage.ts +24 -0
- package/core/server/adapters/storage/LocalMediaStorage.js +13 -11
- package/core/server/adapters/storage/LocalMediaStorage.ts +24 -0
- package/core/server/adapters/storage/LocalStorageBase.js +107 -167
- package/core/server/adapters/storage/LocalStorageBase.ts +342 -0
- package/core/server/adapters/storage/utils.js +36 -50
- package/core/server/adapters/storage/utils.ts +60 -0
- 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/db.js +6 -0
- package/core/server/api/endpoints/index.js +4 -0
- package/core/server/api/endpoints/members.js +25 -28
- package/core/server/api/endpoints/pages.js +6 -1
- package/core/server/api/endpoints/posts.js +11 -2
- package/core/server/api/endpoints/settings.js +0 -2
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +4 -1
- package/core/server/api/endpoints/utils/serializers/output/members.js +18 -87
- package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
- package/core/server/data/exporter/export-filename.js +2 -1
- package/core/server/data/importer/importers/data/revue-subscriber-importer.js +6 -10
- package/core/server/data/importer/importers/json-to-html.js +24 -4
- 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/schema/commands.js +5 -0
- package/core/server/data/schema/default-settings/default-settings.json +0 -4
- package/core/server/data/schema/schema.js +5 -1
- package/core/server/lib/image/image-size.js +3 -17
- package/core/server/lib/image/image-utils.js +15 -1
- package/core/server/lib/request-external.js +1 -1
- package/core/server/models/user.js +46 -70
- package/core/server/services/auth/session/session-service.js +28 -1
- package/core/server/services/automations/automations-api.js +21 -6
- package/core/server/services/automations/automations-api.ts +25 -6
- package/core/server/services/automations/automations-repository.ts +19 -0
- package/core/server/services/automations/database-automations-repository.js +116 -28
- package/core/server/services/automations/database-automations-repository.ts +118 -9
- 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/poll.js +5 -2
- package/core/server/services/automations/poll.ts +7 -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 +15 -3
- package/core/server/services/automations/service.ts +16 -3
- package/core/server/services/comments/comments-service-emails.js +14 -8
- 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/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 +30 -9
- package/core/server/services/link-tracking/link-click-tracking-service.js +44 -3
- package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +19 -4
- package/core/server/services/member-welcome-emails/service.js +11 -3
- package/core/server/services/members/{importer → import-export}/config.js +4 -9
- package/core/server/services/members/{importer → import-export}/config.ts +4 -9
- 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 +12 -0
- package/core/server/services/members/import-export/csv/index.ts +3 -0
- package/core/server/services/members/import-export/csv/parse.js +64 -0
- package/core/server/services/members/import-export/csv/parse.ts +69 -0
- package/core/server/services/members/import-export/csv/serialize.js +13 -0
- package/core/server/services/members/import-export/csv/serialize.ts +18 -0
- package/core/server/services/members/import-export/export/exporter.js +202 -0
- package/core/server/services/members/import-export/export/exporter.ts +325 -0
- package/core/server/services/members/import-export/import/completion-email.js +84 -0
- package/core/server/services/members/import-export/import/completion-email.ts +141 -0
- package/core/server/services/members/import-export/import/importer.js +297 -0
- package/core/server/services/members/import-export/import/importer.ts +468 -0
- package/core/server/services/members/import-export/import/reader.js +32 -0
- package/core/server/services/members/import-export/import/reader.ts +33 -0
- package/core/server/services/members/import-export/import/row.js +33 -0
- package/core/server/services/members/import-export/import/row.ts +52 -0
- package/core/server/services/members/import-export/import/spool.js +30 -0
- package/core/server/services/members/import-export/import/spool.ts +38 -0
- package/core/server/services/members/import-export/index.js +99 -0
- package/core/server/services/members/import-export/index.ts +137 -0
- package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
- package/core/server/services/members/service.js +25 -20
- package/core/server/services/members-custom-fields/values-service.js +19 -7
- package/core/server/services/members-custom-fields/values-service.ts +19 -7
- package/core/server/services/mentions/service.js +5 -1
- package/core/server/services/posts/posts-exporter.js +6 -4
- package/core/server/services/posts/posts-service.js +2 -2
- package/core/server/services/route-settings/activation-bridge.js +59 -49
- package/core/server/services/route-settings/activation-bridge.ts +91 -51
- package/core/server/services/route-settings/dynamic-routing-service.js +22 -51
- package/core/server/services/route-settings/index.js +1 -16
- package/core/server/services/settings/settings-service.js +0 -18
- package/core/server/services/themes/storage.js +9 -0
- package/core/server/services/themes/theme-storage.js +1 -1
- 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/shared/config/defaults.json +2 -1
- package/core/shared/express.js +6 -1
- package/core/shared/labs.js +2 -1
- package/package.json +11 -11
- package/pnpm-lock.yaml +18 -18
- package/pnpm-workspace.yaml +8 -7
- package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
- package/core/built/admin/assets/Overview-j4NaeUNm.js +0 -1
- package/core/built/admin/assets/Web-_fD935OX.js +0 -2
- package/core/built/admin/assets/activitypub/layout-IdVq3lRy.js +0 -1
- package/core/built/admin/assets/audience-BKRdsFmy.js +0 -1
- package/core/built/admin/assets/checkbox-DA4-1UiI.js +0 -1
- package/core/built/admin/assets/chunk.524.45e0875f41b71e46e849.js +0 -25
- package/core/built/admin/assets/chunk.582.51defd4b009f0de8d4d5.js +0 -27
- package/core/built/admin/assets/code-editor-view-BKBrkffZ.js +0 -1
- package/core/built/admin/assets/comments-CzVNIT8b.js +0 -1
- package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
- package/core/built/admin/assets/dist-CT46_f_J.js +0 -1
- package/core/built/admin/assets/domain-B7_n5nfx.js +0 -1
- package/core/built/admin/assets/dropzone-DcuJEEUf.js +0 -5
- package/core/built/admin/assets/editor-BCiIyFjm.js +0 -8
- package/core/built/admin/assets/field-BW-M2Zwp.js +0 -1
- package/core/built/admin/assets/filters-DtcXMZDU.js +0 -1
- package/core/built/admin/assets/gift-link-modal-Bk8qpnMw.js +0 -1
- package/core/built/admin/assets/growth-Ck2s3JjR.js +0 -1
- package/core/built/admin/assets/index-BBf502Wg.js +0 -69
- package/core/built/admin/assets/index-BCCZKxP2.css +0 -1
- package/core/built/admin/assets/input-group-DjuYnNCd.js +0 -1
- package/core/built/admin/assets/label-picker-BdNAWuUX.js +0 -1
- package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
- package/core/built/admin/assets/layout-Bw4GnxXp.js +0 -1
- package/core/built/admin/assets/member-custom-fields-DvFyamLu.js +0 -1
- package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +0 -1
- package/core/built/admin/assets/members-DsPxYAVm.js +0 -10
- package/core/built/admin/assets/modals-Bf_fyvHZ.js +0 -31
- package/core/built/admin/assets/moderation-Dlp7M5sa.js +0 -1
- package/core/built/admin/assets/multi-select-combobox-CH1o8XXQ.js +0 -1
- package/core/built/admin/assets/newsletter-CQ5CAYUI.js +0 -1
- package/core/built/admin/assets/note-BrAZ7Z0o.js +0 -1
- package/core/built/admin/assets/notifications-BnbHLOPC.js +0 -1
- package/core/built/admin/assets/onboarding-route-eU146kuA.js +0 -1
- package/core/built/admin/assets/overview-BpbaK-xu.js +0 -1
- package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-BDqcfBor.js +0 -1
- package/core/built/admin/assets/profile-dJ44Bn_W.js +0 -1
- package/core/built/admin/assets/select--2uVN9_-.js +0 -1
- package/core/built/admin/assets/settings-SD9bxIpt.js +0 -4
- package/core/built/admin/assets/social-urls-BvFZwYcE.js +0 -8
- package/core/built/admin/assets/switch-qp518gFQ.js +0 -1
- package/core/built/admin/assets/tabs-D3ZOYkM7.js +0 -1
- package/core/built/admin/assets/textarea-CTlb3Uu6.js +0 -1
- package/core/built/admin/assets/use-filter-params-DeQ8sX4N.js +0 -1
- package/core/built/admin/assets/web-DTZnnUGr.js +0 -1
- package/core/server/services/mail/templates/newsletter.html +0 -876
- package/core/server/services/members/csv/index.js +0 -10
- package/core/server/services/members/csv/index.ts +0 -3
- package/core/server/services/members/csv/parse.js +0 -94
- package/core/server/services/members/csv/parse.ts +0 -111
- package/core/server/services/members/csv/types.js +0 -2
- package/core/server/services/members/csv/types.ts +0 -39
- package/core/server/services/members/csv/unparse.js +0 -69
- package/core/server/services/members/csv/unparse.ts +0 -79
- package/core/server/services/members/exporter/query.js +0 -233
- package/core/server/services/members/importer/index.js +0 -30
- package/core/server/services/members/importer/labels.js +0 -125
- package/core/server/services/members/importer/members-csv-importer.js +0 -520
- 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
- /package/core/server/services/members/{importer → import-export/import}/email-template.js +0 -0
- /package/core/server/services/members/{importer/members-csv-importer-stripe-utils.js → import-export/import/stripe-utils.js} +0 -0
|
@@ -9,11 +9,13 @@ const tpl_1 = __importDefault(require("@tryghost/tpl"));
|
|
|
9
9
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
10
|
const bson_objectid_1 = __importDefault(require("bson-objectid"));
|
|
11
11
|
const dequal_1 = require("dequal");
|
|
12
|
-
|
|
12
|
+
// @ts-expect-error This module currently lacks type definitions.
|
|
13
|
+
const lexical_1 = __importDefault(require("../../lib/lexical"));
|
|
14
|
+
const url_utils_1 = __importDefault(require("../../../shared/url-utils"));
|
|
13
15
|
const constants_1 = require("../member-welcome-emails/constants");
|
|
16
|
+
const database_date_1 = require("./database-date");
|
|
14
17
|
const stale_lock_cutoff_1 = require("./stale-lock-cutoff");
|
|
15
18
|
const HOUR_MS = 60 * 60 * 1000;
|
|
16
|
-
const DATABASE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
|
17
19
|
const DEFAULT_WELCOME_EMAIL_AUTOMATIONS = [{
|
|
18
20
|
name: 'Free member welcome flow',
|
|
19
21
|
slug: constants_1.MEMBER_WELCOME_EMAIL_SLUGS.free
|
|
@@ -28,9 +30,6 @@ const messages = {
|
|
|
28
30
|
defaultEmailDesignSettingNotFound: 'Default automated email design setting not found.'
|
|
29
31
|
};
|
|
30
32
|
const DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE = constants_1.DEFAULT_EMAIL_DESIGN_SETTING_SLUG;
|
|
31
|
-
function toDatabaseDate(date) {
|
|
32
|
-
return (0, moment_1.default)(date).format(DATABASE_DATE_FORMAT);
|
|
33
|
-
}
|
|
34
33
|
function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier }) {
|
|
35
34
|
return {
|
|
36
35
|
async browse() {
|
|
@@ -54,6 +53,33 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
54
53
|
return await buildAutomation(trx, automation);
|
|
55
54
|
});
|
|
56
55
|
},
|
|
56
|
+
async getAutomationActionLinks(automationId, actionId) {
|
|
57
|
+
const action = await knex('automation_actions')
|
|
58
|
+
.select('id')
|
|
59
|
+
.where({
|
|
60
|
+
id: actionId,
|
|
61
|
+
automation_id: automationId
|
|
62
|
+
})
|
|
63
|
+
.whereNull('deleted_at')
|
|
64
|
+
.first();
|
|
65
|
+
if (!action) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const rows = await knex('redirects as redirects')
|
|
69
|
+
.countDistinct({ clicked_count: 'members_click_events.member_id' })
|
|
70
|
+
.select(knex.raw('MIN(??) as ??', ['redirects.to', 'url']))
|
|
71
|
+
.innerJoin('automation_action_revisions as revisions', 'revisions.id', 'redirects.automation_action_revision_id')
|
|
72
|
+
.leftJoin('members_click_events', 'members_click_events.redirect_id', 'redirects.id')
|
|
73
|
+
.where('revisions.action_id', actionId)
|
|
74
|
+
.whereNotNull('redirects.to_hash')
|
|
75
|
+
.groupBy('redirects.to_hash')
|
|
76
|
+
.orderBy('clicked_count', 'desc')
|
|
77
|
+
.orderBy('url', 'asc');
|
|
78
|
+
return rows.map(row => ({
|
|
79
|
+
url: url_utils_1.default.transformReadyToAbsolute(row.url),
|
|
80
|
+
clicked_count: Number(row.clicked_count)
|
|
81
|
+
}));
|
|
82
|
+
},
|
|
57
83
|
async edit(id, data) {
|
|
58
84
|
return await knex.transaction(async (trx) => {
|
|
59
85
|
const automation = await loadAutomation(trx, id);
|
|
@@ -64,7 +90,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
64
90
|
const updatedAutomation = await updateAutomation(trx, {
|
|
65
91
|
...automation,
|
|
66
92
|
status: data.status,
|
|
67
|
-
updated_at: toDatabaseDate(now)
|
|
93
|
+
updated_at: (0, database_date_1.toDatabaseDate)(now)
|
|
68
94
|
});
|
|
69
95
|
await replaceAutomationGraph(trx, updatedAutomation.id, data.actions, data.edges);
|
|
70
96
|
if (updatedAutomation.status === 'inactive') {
|
|
@@ -101,7 +127,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
101
127
|
.update({
|
|
102
128
|
email_sent_count: trx.raw('COALESCE(??, 0) + ?', ['email_sent_count', 1])
|
|
103
129
|
});
|
|
104
|
-
const now = toDatabaseDate(new Date());
|
|
130
|
+
const now = (0, database_date_1.toDatabaseDate)(new Date());
|
|
105
131
|
await trx('automated_email_recipients').insert({
|
|
106
132
|
id: (0, bson_objectid_1.default)().toHexString(),
|
|
107
133
|
member_id: options.memberId,
|
|
@@ -162,6 +188,48 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
162
188
|
});
|
|
163
189
|
}
|
|
164
190
|
});
|
|
191
|
+
},
|
|
192
|
+
async trackEmailClicked({ automationActionRevisionId, memberId, clickedAt }, { transacting } = {}) {
|
|
193
|
+
const trackClick = async (trx) => {
|
|
194
|
+
// Match recordEmailSent's lock order to avoid deadlocks.
|
|
195
|
+
await trx('automation_action_revisions')
|
|
196
|
+
.select('id')
|
|
197
|
+
.where({ id: automationActionRevisionId })
|
|
198
|
+
.forUpdate()
|
|
199
|
+
.first();
|
|
200
|
+
const recipient = await trx('automated_email_recipients')
|
|
201
|
+
.select('id', 'clicked_at')
|
|
202
|
+
.where({
|
|
203
|
+
automation_action_revision_id: automationActionRevisionId,
|
|
204
|
+
member_id: memberId,
|
|
205
|
+
track_clicks: true
|
|
206
|
+
})
|
|
207
|
+
.where('created_at', '<=', (0, database_date_1.toDatabaseDate)(clickedAt))
|
|
208
|
+
.orderBy('created_at', 'desc')
|
|
209
|
+
.orderBy('id', 'desc')
|
|
210
|
+
.forUpdate()
|
|
211
|
+
.first();
|
|
212
|
+
if (!recipient || recipient.clicked_at !== null) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const updated = await trx('automated_email_recipients')
|
|
216
|
+
.where({ id: recipient.id })
|
|
217
|
+
.whereNull('clicked_at')
|
|
218
|
+
.update({ clicked_at: clickedAt });
|
|
219
|
+
if (updated === 0) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
await trx('automation_action_revisions')
|
|
223
|
+
.where({ id: automationActionRevisionId })
|
|
224
|
+
.update({
|
|
225
|
+
email_clicked_count: trx.raw('COALESCE(email_clicked_count, 0) + 1')
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
if (transacting) {
|
|
229
|
+
await trackClick(transacting);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
await knex.transaction(trackClick);
|
|
165
233
|
}
|
|
166
234
|
};
|
|
167
235
|
}
|
|
@@ -194,7 +262,7 @@ async function ensureDefaultAutomations(trx) {
|
|
|
194
262
|
}
|
|
195
263
|
}
|
|
196
264
|
async function ensureAutomation(trx, defaults) {
|
|
197
|
-
const now = toDatabaseDate(new Date());
|
|
265
|
+
const now = (0, database_date_1.toDatabaseDate)(new Date());
|
|
198
266
|
const id = (0, bson_objectid_1.default)().toHexString();
|
|
199
267
|
await trx('automations')
|
|
200
268
|
.insert({
|
|
@@ -233,7 +301,7 @@ async function ensureWelcomeEmailAction(trx, automationId) {
|
|
|
233
301
|
if (!email) {
|
|
234
302
|
return;
|
|
235
303
|
}
|
|
236
|
-
const now = toDatabaseDate(new Date());
|
|
304
|
+
const now = (0, database_date_1.toDatabaseDate)(new Date());
|
|
237
305
|
const actionId = (0, bson_objectid_1.default)().toHexString();
|
|
238
306
|
await insertActions(trx, [{
|
|
239
307
|
id: actionId,
|
|
@@ -263,7 +331,7 @@ async function trigger(trx, options) {
|
|
|
263
331
|
return;
|
|
264
332
|
}
|
|
265
333
|
const now = new Date();
|
|
266
|
-
const nowString = toDatabaseDate(now);
|
|
334
|
+
const nowString = (0, database_date_1.toDatabaseDate)(now);
|
|
267
335
|
const readyAt = getReadyAtForAction(firstAction, now, fakeWaitHoursMultiplier);
|
|
268
336
|
const run = {
|
|
269
337
|
id: (0, bson_objectid_1.default)().toHexString(),
|
|
@@ -282,14 +350,14 @@ async function trigger(trx, options) {
|
|
|
282
350
|
});
|
|
283
351
|
}
|
|
284
352
|
async function insertRunStep(trx, { automationRunId, automationActionRevisionId, now, readyAt }) {
|
|
285
|
-
const nowString = toDatabaseDate(now);
|
|
353
|
+
const nowString = (0, database_date_1.toDatabaseDate)(now);
|
|
286
354
|
await trx('automation_run_steps').insert({
|
|
287
355
|
id: (0, bson_objectid_1.default)().toHexString(),
|
|
288
356
|
created_at: nowString,
|
|
289
357
|
updated_at: nowString,
|
|
290
358
|
automation_run_id: automationRunId,
|
|
291
359
|
automation_action_revision_id: automationActionRevisionId,
|
|
292
|
-
ready_at: toDatabaseDate(readyAt)
|
|
360
|
+
ready_at: (0, database_date_1.toDatabaseDate)(readyAt)
|
|
293
361
|
});
|
|
294
362
|
}
|
|
295
363
|
async function fetchAndLockSteps(trx, limit) {
|
|
@@ -304,9 +372,9 @@ async function fetchAndLockSteps(trx, limit) {
|
|
|
304
372
|
// 2. Try to lock those rows.
|
|
305
373
|
// 3. Select any rows we successfully locked.
|
|
306
374
|
const now = new Date();
|
|
307
|
-
const nowString = toDatabaseDate(now);
|
|
375
|
+
const nowString = (0, database_date_1.toDatabaseDate)(now);
|
|
308
376
|
const staleLockCutoff = (0, stale_lock_cutoff_1.getStaleLockCutoff)(now);
|
|
309
|
-
const staleLockCutoffString = toDatabaseDate(staleLockCutoff);
|
|
377
|
+
const staleLockCutoffString = (0, database_date_1.toDatabaseDate)(staleLockCutoff);
|
|
310
378
|
const lockId = node_crypto_1.default.randomUUID();
|
|
311
379
|
// 1. Select up to `limit` candidate rows.
|
|
312
380
|
const candidates = await trx('automation_run_steps')
|
|
@@ -374,7 +442,7 @@ async function findNextPendingReadyAt(trx, staleLockCutoff) {
|
|
|
374
442
|
.where((builder) => {
|
|
375
443
|
builder
|
|
376
444
|
.whereNull('locked_by')
|
|
377
|
-
.orWhere('locked_at', '<', toDatabaseDate(staleLockCutoff));
|
|
445
|
+
.orWhere('locked_at', '<', (0, database_date_1.toDatabaseDate)(staleLockCutoff));
|
|
378
446
|
})
|
|
379
447
|
.orderBy('ready_at')
|
|
380
448
|
.first();
|
|
@@ -479,7 +547,7 @@ async function findNextActionRevision(trx, sourceActionId) {
|
|
|
479
547
|
return row ?? null;
|
|
480
548
|
}
|
|
481
549
|
async function markStepTerminal(trx, step, status) {
|
|
482
|
-
const nowString = toDatabaseDate(new Date());
|
|
550
|
+
const nowString = (0, database_date_1.toDatabaseDate)(new Date());
|
|
483
551
|
return await updateStep(trx, step, {
|
|
484
552
|
status,
|
|
485
553
|
finished_at: nowString,
|
|
@@ -491,12 +559,12 @@ async function retryStep(trx, step, retryAt) {
|
|
|
491
559
|
await markStepTerminal(trx, step, 'automation disabled');
|
|
492
560
|
return false;
|
|
493
561
|
}
|
|
494
|
-
const nowString = toDatabaseDate(new Date());
|
|
562
|
+
const nowString = (0, database_date_1.toDatabaseDate)(new Date());
|
|
495
563
|
return await updateStep(trx, step, {
|
|
496
564
|
status: 'pending',
|
|
497
565
|
started_at: null,
|
|
498
566
|
finished_at: null,
|
|
499
|
-
ready_at: toDatabaseDate(retryAt),
|
|
567
|
+
ready_at: (0, database_date_1.toDatabaseDate)(retryAt),
|
|
500
568
|
updated_at: nowString
|
|
501
569
|
});
|
|
502
570
|
}
|
|
@@ -521,8 +589,8 @@ function getReadyAtForAction(action, now, fakeWaitHoursMultiplier) {
|
|
|
521
589
|
}
|
|
522
590
|
}
|
|
523
591
|
async function cancelCancelablePendingStepsForAutomation(trx, automationId, now) {
|
|
524
|
-
const nowString = toDatabaseDate(now);
|
|
525
|
-
const staleLockCutoff = toDatabaseDate((0, stale_lock_cutoff_1.getStaleLockCutoff)(now));
|
|
592
|
+
const nowString = (0, database_date_1.toDatabaseDate)(now);
|
|
593
|
+
const staleLockCutoff = (0, database_date_1.toDatabaseDate)((0, stale_lock_cutoff_1.getStaleLockCutoff)(now));
|
|
526
594
|
await trx('automation_run_steps')
|
|
527
595
|
.update({
|
|
528
596
|
status: 'automation disabled',
|
|
@@ -626,11 +694,11 @@ async function updateAutomation(trx, automation) {
|
|
|
626
694
|
async function replaceAutomationGraph(trx, automationId, submittedActions, edges) {
|
|
627
695
|
const existingActions = await loadAutomationActionRows(trx, automationId);
|
|
628
696
|
const existingActionById = new Map(existingActions.map(action => [action.id, action]));
|
|
629
|
-
const actions = await resolveEmailDesignSettingIds(trx, submittedActions);
|
|
697
|
+
const actions = (await resolveEmailDesignSettingIds(trx, submittedActions)).map(formatActionOnWrite);
|
|
630
698
|
const submittedActionIds = new Set(actions.map(action => action.id));
|
|
631
699
|
const actionIdsWithOwners = await loadActionIdsWithOwners(trx, [...submittedActionIds]);
|
|
632
700
|
const latestRevisionByActionId = new Map((await loadLatestActionRevisions(trx, [...submittedActionIds])).map(revision => [revision.action_id, revision]));
|
|
633
|
-
const now = toDatabaseDate(new Date());
|
|
701
|
+
const now = (0, database_date_1.toDatabaseDate)(new Date());
|
|
634
702
|
const actionsToInsert = [];
|
|
635
703
|
const revisionsToInsert = [];
|
|
636
704
|
for (const action of actions) {
|
|
@@ -680,6 +748,21 @@ async function replaceAutomationGraph(trx, automationId, submittedActions, edges
|
|
|
680
748
|
await deleteAutomationEdges(trx, automationId);
|
|
681
749
|
await insertActionEdges(trx, edges);
|
|
682
750
|
}
|
|
751
|
+
function formatActionOnWrite(action) {
|
|
752
|
+
if (action.type !== 'send_email') {
|
|
753
|
+
return action;
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
...action,
|
|
757
|
+
data: {
|
|
758
|
+
...action.data,
|
|
759
|
+
email_lexical: url_utils_1.default.lexicalToTransformReady(action.data.email_lexical, {
|
|
760
|
+
nodes: lexical_1.default.nodes,
|
|
761
|
+
transformMap: lexical_1.default.urlTransformMap
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
683
766
|
async function resolveEmailDesignSettingIds(trx, actions) {
|
|
684
767
|
if (!actions.some(action => action.type === 'send_email' && action.data.email_design_setting_id === DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE)) {
|
|
685
768
|
return [...actions];
|
|
@@ -793,14 +876,14 @@ async function insertActionRevisions(trx, revisions) {
|
|
|
793
876
|
}
|
|
794
877
|
function getNextRevisionCreatedAt(latestCreatedAt, requestedCreatedAt) {
|
|
795
878
|
if (!latestCreatedAt) {
|
|
796
|
-
return toDatabaseDate(requestedCreatedAt);
|
|
879
|
+
return (0, database_date_1.toDatabaseDate)(requestedCreatedAt);
|
|
797
880
|
}
|
|
798
881
|
const requestedTime = new Date(requestedCreatedAt).getTime();
|
|
799
882
|
const latestTime = new Date(latestCreatedAt).getTime();
|
|
800
883
|
if (requestedTime > latestTime) {
|
|
801
|
-
return toDatabaseDate(requestedCreatedAt);
|
|
884
|
+
return (0, database_date_1.toDatabaseDate)(requestedCreatedAt);
|
|
802
885
|
}
|
|
803
|
-
return toDatabaseDate(new Date(latestTime + 1000));
|
|
886
|
+
return (0, database_date_1.toDatabaseDate)(new Date(latestTime + 1000));
|
|
804
887
|
}
|
|
805
888
|
function buildActionRevision(actionId, action, createdAt) {
|
|
806
889
|
switch (action.type) {
|
|
@@ -902,6 +985,7 @@ async function loadActionStats(trx, actionIds) {
|
|
|
902
985
|
const rows = await trx('automation_action_revisions')
|
|
903
986
|
.select('action_id')
|
|
904
987
|
.sum({
|
|
988
|
+
email_clicked_count: 'email_clicked_count',
|
|
905
989
|
email_sent_count: 'email_sent_count',
|
|
906
990
|
email_opened_count: 'email_opened_count'
|
|
907
991
|
})
|
|
@@ -945,7 +1029,7 @@ function buildActionPayload(row, stats) {
|
|
|
945
1029
|
type: 'send_email',
|
|
946
1030
|
data: {
|
|
947
1031
|
email_subject: requireValue(row, 'email_subject'),
|
|
948
|
-
email_lexical: requireValue(row, 'email_lexical'),
|
|
1032
|
+
email_lexical: url_utils_1.default.transformReadyToAbsolute(requireValue(row, 'email_lexical')),
|
|
949
1033
|
email_design_setting_id: requireValue(row, 'email_design_setting_id')
|
|
950
1034
|
},
|
|
951
1035
|
stats: stats ?? EMPTY_EMAIL_STATS
|
|
@@ -953,22 +1037,26 @@ function buildActionPayload(row, stats) {
|
|
|
953
1037
|
}
|
|
954
1038
|
}
|
|
955
1039
|
const EMPTY_EMAIL_STATS = {
|
|
1040
|
+
email_clicked_count: 0,
|
|
956
1041
|
email_sent_count: 0,
|
|
957
1042
|
email_opened_count: 0,
|
|
958
1043
|
opened_rate: null,
|
|
959
1044
|
clicked_rate: null
|
|
960
1045
|
};
|
|
961
1046
|
function buildEmailStats(row) {
|
|
1047
|
+
const emailClickedCount = row.email_clicked_count ?? 0;
|
|
962
1048
|
const emailSentCount = row.email_sent_count ?? 0;
|
|
963
1049
|
const emailOpenedCount = row.email_opened_count ?? 0;
|
|
964
1050
|
return {
|
|
1051
|
+
email_clicked_count: emailClickedCount,
|
|
965
1052
|
email_sent_count: emailSentCount,
|
|
966
1053
|
email_opened_count: emailOpenedCount,
|
|
967
1054
|
opened_rate: emailSentCount
|
|
968
1055
|
? Math.round(emailOpenedCount / emailSentCount * 100)
|
|
969
1056
|
: null,
|
|
970
|
-
|
|
971
|
-
|
|
1057
|
+
clicked_rate: emailSentCount
|
|
1058
|
+
? Math.round(emailClickedCount / emailSentCount * 100)
|
|
1059
|
+
: null
|
|
972
1060
|
};
|
|
973
1061
|
}
|
|
974
1062
|
function requireValue(row, field) {
|
|
@@ -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;
|
|
@@ -65,10 +64,16 @@ interface ActionRow {
|
|
|
65
64
|
|
|
66
65
|
type ActionStatsRow = {
|
|
67
66
|
action_id: string;
|
|
67
|
+
email_clicked_count: number | null;
|
|
68
68
|
email_sent_count: number | null;
|
|
69
69
|
email_opened_count: number | null;
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
type ActionLinkRow = {
|
|
73
|
+
url: string;
|
|
74
|
+
clicked_count: string | number;
|
|
75
|
+
};
|
|
76
|
+
|
|
72
77
|
type ActionRevisionRow = {
|
|
73
78
|
action_id: string;
|
|
74
79
|
created_at: string;
|
|
@@ -165,6 +170,37 @@ export function createDatabaseAutomationsRepository({
|
|
|
165
170
|
});
|
|
166
171
|
},
|
|
167
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
|
+
|
|
168
204
|
async edit(id: string, data: EditAutomationData): Promise<Automation | null> {
|
|
169
205
|
return await knex.transaction(async (trx) => {
|
|
170
206
|
const automation = await loadAutomation(trx, id);
|
|
@@ -303,6 +339,57 @@ export function createDatabaseAutomationsRepository({
|
|
|
303
339
|
});
|
|
304
340
|
}
|
|
305
341
|
});
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
async trackEmailClicked({automationActionRevisionId, memberId, clickedAt}, {transacting} = {}) {
|
|
345
|
+
const trackClick = async (trx: Knex.Transaction) => {
|
|
346
|
+
// Match recordEmailSent's lock order to avoid deadlocks.
|
|
347
|
+
await trx('automation_action_revisions')
|
|
348
|
+
.select('id')
|
|
349
|
+
.where({id: automationActionRevisionId})
|
|
350
|
+
.forUpdate()
|
|
351
|
+
.first();
|
|
352
|
+
|
|
353
|
+
const recipient = await trx('automated_email_recipients')
|
|
354
|
+
.select('id', 'clicked_at')
|
|
355
|
+
.where({
|
|
356
|
+
automation_action_revision_id: automationActionRevisionId,
|
|
357
|
+
member_id: memberId,
|
|
358
|
+
track_clicks: true
|
|
359
|
+
})
|
|
360
|
+
.where('created_at', '<=', toDatabaseDate(clickedAt))
|
|
361
|
+
.orderBy('created_at', 'desc')
|
|
362
|
+
.orderBy('id', 'desc')
|
|
363
|
+
.forUpdate()
|
|
364
|
+
.first();
|
|
365
|
+
|
|
366
|
+
if (!recipient || recipient.clicked_at !== null) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const updated = await trx('automated_email_recipients')
|
|
371
|
+
.where({id: recipient.id})
|
|
372
|
+
.whereNull('clicked_at')
|
|
373
|
+
.update({clicked_at: clickedAt});
|
|
374
|
+
|
|
375
|
+
if (updated === 0) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await trx('automation_action_revisions')
|
|
380
|
+
.where({id: automationActionRevisionId})
|
|
381
|
+
.update({
|
|
382
|
+
email_clicked_count: trx.raw('COALESCE(email_clicked_count, 0) + 1')
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (transacting) {
|
|
388
|
+
await trackClick(transacting);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await knex.transaction(trackClick);
|
|
306
393
|
}
|
|
307
394
|
};
|
|
308
395
|
}
|
|
@@ -920,7 +1007,7 @@ async function updateAutomation(trx: Knex.Transaction, automation: AutomationRow
|
|
|
920
1007
|
async function replaceAutomationGraph(trx: Knex.Transaction, automationId: string, submittedActions: AutomationAction[], edges: AutomationEdge[]): Promise<void> {
|
|
921
1008
|
const existingActions = await loadAutomationActionRows(trx, automationId);
|
|
922
1009
|
const existingActionById = new Map(existingActions.map(action => [action.id, action]));
|
|
923
|
-
const actions = await resolveEmailDesignSettingIds(trx, submittedActions);
|
|
1010
|
+
const actions = (await resolveEmailDesignSettingIds(trx, submittedActions)).map(formatActionOnWrite);
|
|
924
1011
|
const submittedActionIds = new Set(actions.map(action => action.id));
|
|
925
1012
|
const actionIdsWithOwners = await loadActionIdsWithOwners(trx, [...submittedActionIds]);
|
|
926
1013
|
const latestRevisionByActionId = new Map((await loadLatestActionRevisions(trx, [...submittedActionIds])).map(revision => [revision.action_id, revision]));
|
|
@@ -981,6 +1068,23 @@ async function replaceAutomationGraph(trx: Knex.Transaction, automationId: strin
|
|
|
981
1068
|
await insertActionEdges(trx, edges);
|
|
982
1069
|
}
|
|
983
1070
|
|
|
1071
|
+
function formatActionOnWrite(action: AutomationAction): AutomationAction {
|
|
1072
|
+
if (action.type !== 'send_email') {
|
|
1073
|
+
return action;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return {
|
|
1077
|
+
...action,
|
|
1078
|
+
data: {
|
|
1079
|
+
...action.data,
|
|
1080
|
+
email_lexical: urlUtils.lexicalToTransformReady(action.data.email_lexical, {
|
|
1081
|
+
nodes: lexicalLib.nodes,
|
|
1082
|
+
transformMap: lexicalLib.urlTransformMap
|
|
1083
|
+
})
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
984
1088
|
async function resolveEmailDesignSettingIds(trx: Knex.Transaction, actions: ReadonlyArray<AutomationAction>): Promise<AutomationAction[]> {
|
|
985
1089
|
if (!actions.some(action => action.type === 'send_email' && action.data.email_design_setting_id === DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE)) {
|
|
986
1090
|
return [...actions];
|
|
@@ -1264,6 +1368,7 @@ async function loadActionStats(
|
|
|
1264
1368
|
const rows: ActionStatsRow[] = await trx('automation_action_revisions')
|
|
1265
1369
|
.select('action_id')
|
|
1266
1370
|
.sum({
|
|
1371
|
+
email_clicked_count: 'email_clicked_count',
|
|
1267
1372
|
email_sent_count: 'email_sent_count',
|
|
1268
1373
|
email_opened_count: 'email_opened_count'
|
|
1269
1374
|
})
|
|
@@ -1310,7 +1415,7 @@ function buildActionPayload(row: ActionRow, stats: AutomationEmailStats | null):
|
|
|
1310
1415
|
type: 'send_email',
|
|
1311
1416
|
data: {
|
|
1312
1417
|
email_subject: requireValue(row, 'email_subject'),
|
|
1313
|
-
email_lexical: requireValue(row, 'email_lexical'),
|
|
1418
|
+
email_lexical: urlUtils.transformReadyToAbsolute(requireValue(row, 'email_lexical')),
|
|
1314
1419
|
email_design_setting_id: requireValue(row, 'email_design_setting_id')
|
|
1315
1420
|
},
|
|
1316
1421
|
stats: stats ?? EMPTY_EMAIL_STATS
|
|
@@ -1319,6 +1424,7 @@ function buildActionPayload(row: ActionRow, stats: AutomationEmailStats | null):
|
|
|
1319
1424
|
}
|
|
1320
1425
|
|
|
1321
1426
|
const EMPTY_EMAIL_STATS: AutomationEmailStats = {
|
|
1427
|
+
email_clicked_count: 0,
|
|
1322
1428
|
email_sent_count: 0,
|
|
1323
1429
|
email_opened_count: 0,
|
|
1324
1430
|
opened_rate: null,
|
|
@@ -1326,16 +1432,19 @@ const EMPTY_EMAIL_STATS: AutomationEmailStats = {
|
|
|
1326
1432
|
};
|
|
1327
1433
|
|
|
1328
1434
|
function buildEmailStats(row: ActionStatsRow): AutomationEmailStats {
|
|
1435
|
+
const emailClickedCount = row.email_clicked_count ?? 0;
|
|
1329
1436
|
const emailSentCount = row.email_sent_count ?? 0;
|
|
1330
1437
|
const emailOpenedCount = row.email_opened_count ?? 0;
|
|
1331
1438
|
return {
|
|
1439
|
+
email_clicked_count: emailClickedCount,
|
|
1332
1440
|
email_sent_count: emailSentCount,
|
|
1333
1441
|
email_opened_count: emailOpenedCount,
|
|
1334
1442
|
opened_rate: emailSentCount
|
|
1335
1443
|
? Math.round(emailOpenedCount / emailSentCount * 100)
|
|
1336
1444
|
: null,
|
|
1337
|
-
|
|
1338
|
-
|
|
1445
|
+
clicked_rate: emailSentCount
|
|
1446
|
+
? Math.round(emailClickedCount / emailSentCount * 100)
|
|
1447
|
+
: null
|
|
1339
1448
|
};
|
|
1340
1449
|
}
|
|
1341
1450
|
|
|
@@ -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;
|
|
@@ -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,9 @@ 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
|
|
130
133
|
});
|
|
131
134
|
const mailgunMessageId = (0, mailgun_message_id_1.getMailgunMessageId)(sendResult);
|
|
132
135
|
// Only Mailgun sends can produce open events for automation emails
|
|
@@ -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,8 @@ type MemberWelcomeEmailService = {
|
|
|
25
26
|
};
|
|
26
27
|
memberStatus: 'free' | 'paid';
|
|
27
28
|
trackOpens: boolean;
|
|
29
|
+
trackClicks: boolean;
|
|
30
|
+
automationActionRevisionId: string;
|
|
28
31
|
}) => Promise<unknown>;
|
|
29
32
|
};
|
|
30
33
|
};
|
|
@@ -183,7 +186,7 @@ const processStep = async ({
|
|
|
183
186
|
break;
|
|
184
187
|
}
|
|
185
188
|
memberWelcomeEmailService.init();
|
|
186
|
-
const trackClicks = Boolean(settingsCache.get('email_track_clicks'));
|
|
189
|
+
const trackClicks = labs.isSet('automationAnalytics') && Boolean(settingsCache.get('email_track_clicks'));
|
|
187
190
|
const trackOpens = Boolean(settingsCache.get('email_track_opens'));
|
|
188
191
|
const sendResult = await memberWelcomeEmailService.api.sendAutomationEmail({
|
|
189
192
|
email: {
|
|
@@ -197,7 +200,9 @@ const processStep = async ({
|
|
|
197
200
|
uuid: member.get('uuid')
|
|
198
201
|
},
|
|
199
202
|
memberStatus,
|
|
200
|
-
trackOpens
|
|
203
|
+
trackOpens,
|
|
204
|
+
trackClicks,
|
|
205
|
+
automationActionRevisionId: step.automation_action_revision_id
|
|
201
206
|
});
|
|
202
207
|
const mailgunMessageId = getMailgunMessageId(sendResult);
|
|
203
208
|
// Only Mailgun sends can produce open events for automation emails
|
|
@@ -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;
|