diff --git a/.env b/.env index a8f3c9080..826c0d631 100644 --- a/.env +++ b/.env @@ -53,7 +53,6 @@ COOKIE_SECRET=this-is-not-so-secret-change-it # Notification related variables APP_ROOT_DOMAIN=gleev.xyz -NOTIFICATION_CENTER_PATH=notification-center # https://${APP_ROOT_DOMAIN}/${NOTIFICATION_CENTER_PATH} TRUST_PROXY=uniquelocal @@ -64,3 +63,6 @@ SENDGRID_FROM_EMAIL=gateway@example.com # Debug settings SQD_DEBUG=api:* OPENAPI_PLAYGROUND=true + +# max number of attempts to deliver email notification +EMAIL_NOTIFICATION_DELIVERY_MAX_ATTEMPTS=5 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 59752b7f0..0cc2cf9f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,3 +62,22 @@ jobs: run: make prepare - name: Run tests run: npm run tests:notifications + mail-scheduler: + name: Mail scheduler tests + needs: [migrations] + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node-version: [16.x] + fail-fast: true + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{matrix.node-version}} + uses: actions/setup-node@v1 + with: + node-version: ${{matrix.node-version}} + - name: Prepare workspace + run: make prepare + - name: Run tests + run: npm run tests:mail-scheduler diff --git a/.gitignore b/.gitignore index 60649bbc0..602a72616 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ /.vscode /.idea src/model/generated +src/auth-server/emails/templates/*.mst +src/auth-server/emails/templates/preview /schema.graphql /db/persisted /scripts/orion-v1-migration/data diff --git a/db/migrations/1695920086688-Data.js b/db/migrations/1696003676131-Data.js similarity index 98% rename from db/migrations/1695920086688-Data.js rename to db/migrations/1696003676131-Data.js index 1e2167130..f64dfc2b8 100644 --- a/db/migrations/1695920086688-Data.js +++ b/db/migrations/1696003676131-Data.js @@ -1,5 +1,5 @@ -module.exports = class Data1695920086688 { - name = 'Data1695920086688' +module.exports = class Data1696003676131 { + name = 'Data1696003676131' async up(db) { await db.query(`CREATE TABLE "channel_follow" ("id" character varying NOT NULL, "user_id" character varying, "channel_id" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_9410df2b9a316af3f0d216f9487" PRIMARY KEY ("id"))`) @@ -118,7 +118,9 @@ module.exports = class Data1695920086688 { await db.query(`CREATE TABLE "nft_activity" ("id" character varying NOT NULL, "member_id" character varying, "event_id" character varying, CONSTRAINT "PK_1553b1bbf8000039875a6e31536" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_18a65713a9fd0715c7a980f5d5" ON "nft_activity" ("member_id") `) await db.query(`CREATE INDEX "IDX_94d325a753f2c08fdd416eb095" ON "nft_activity" ("event_id") `) - await db.query(`CREATE TABLE "notification_email_delivery" ("id" character varying NOT NULL, "notification_id" character varying, "delivery_status" character varying(7) NOT NULL, CONSTRAINT "PK_60dc7ff42a7abf7b0d44bf60516" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "email_delivery_attempt" ("id" character varying NOT NULL, "notification_delivery_id" character varying, "status" jsonb NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_876948339083a2f1092245f7a32" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_f985b9b362249af72cac0f52a3" ON "email_delivery_attempt" ("notification_delivery_id") `) + await db.query(`CREATE TABLE "notification_email_delivery" ("id" character varying NOT NULL, "notification_id" character varying, "discard" boolean NOT NULL, CONSTRAINT "PK_60dc7ff42a7abf7b0d44bf60516" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_3b756627c3146db150d66d1292" ON "notification_email_delivery" ("notification_id") `) await db.query(`CREATE TABLE "video_hero" ("id" character varying NOT NULL, "video_id" character varying, "hero_title" text NOT NULL, "hero_video_cut_url" text NOT NULL, "hero_poster_url" text NOT NULL, "activated_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_f3b63979879773378afac0b9495" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_9feac5d9713a9f07e32eb8ba7a" ON "video_hero" ("video_id") `) @@ -207,6 +209,7 @@ module.exports = class Data1695920086688 { await db.query(`ALTER TABLE "nft_history_entry" ADD CONSTRAINT "FK_d1a28b178f5d028d048d40ce208" FOREIGN KEY ("event_id") REFERENCES "event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) await db.query(`ALTER TABLE "nft_activity" ADD CONSTRAINT "FK_18a65713a9fd0715c7a980f5d54" FOREIGN KEY ("member_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) await db.query(`ALTER TABLE "nft_activity" ADD CONSTRAINT "FK_94d325a753f2c08fdd416eb095f" FOREIGN KEY ("event_id") REFERENCES "event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) + await db.query(`ALTER TABLE "email_delivery_attempt" ADD CONSTRAINT "FK_f985b9b362249af72cac0f52a3b" FOREIGN KEY ("notification_delivery_id") REFERENCES "notification_email_delivery"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) await db.query(`ALTER TABLE "notification_email_delivery" ADD CONSTRAINT "FK_3b756627c3146db150d66d12929" FOREIGN KEY ("notification_id") REFERENCES "notification"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) await db.query(`ALTER TABLE "video_hero" ADD CONSTRAINT "FK_9feac5d9713a9f07e32eb8ba7a1" FOREIGN KEY ("video_id") REFERENCES "video"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) await db.query(`ALTER TABLE "video_media_metadata" ADD CONSTRAINT "FK_5944dc5896cb16bd395414a0ce0" FOREIGN KEY ("encoding_id") REFERENCES "video_media_encoding"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`) @@ -336,6 +339,8 @@ module.exports = class Data1695920086688 { await db.query(`DROP TABLE "nft_activity"`) await db.query(`DROP INDEX "public"."IDX_18a65713a9fd0715c7a980f5d5"`) await db.query(`DROP INDEX "public"."IDX_94d325a753f2c08fdd416eb095"`) + await db.query(`DROP TABLE "email_delivery_attempt"`) + await db.query(`DROP INDEX "public"."IDX_f985b9b362249af72cac0f52a3"`) await db.query(`DROP TABLE "notification_email_delivery"`) await db.query(`DROP INDEX "public"."IDX_3b756627c3146db150d66d1292"`) await db.query(`DROP TABLE "video_hero"`) @@ -425,6 +430,7 @@ module.exports = class Data1695920086688 { await db.query(`ALTER TABLE "nft_history_entry" DROP CONSTRAINT "FK_d1a28b178f5d028d048d40ce208"`) await db.query(`ALTER TABLE "nft_activity" DROP CONSTRAINT "FK_18a65713a9fd0715c7a980f5d54"`) await db.query(`ALTER TABLE "nft_activity" DROP CONSTRAINT "FK_94d325a753f2c08fdd416eb095f"`) + await db.query(`ALTER TABLE "email_delivery_attempt" DROP CONSTRAINT "FK_f985b9b362249af72cac0f52a3b"`) await db.query(`ALTER TABLE "notification_email_delivery" DROP CONSTRAINT "FK_3b756627c3146db150d66d12929"`) await db.query(`ALTER TABLE "video_hero" DROP CONSTRAINT "FK_9feac5d9713a9f07e32eb8ba7a1"`) await db.query(`ALTER TABLE "video_media_metadata" DROP CONSTRAINT "FK_5944dc5896cb16bd395414a0ce0"`) diff --git a/db/migrations/2000000000000-Views.js b/db/migrations/2000000000000-Views.js index a64ca5514..a12612734 100644 --- a/db/migrations/2000000000000-Views.js +++ b/db/migrations/2000000000000-Views.js @@ -79,6 +79,7 @@ module.exports = class Views2000000000000 { token: ['FALSE'], nft_featuring_request: ['FALSE'], gateway_config: ['FALSE'], + email_delivery_attempt: ['FALSE'], // TODO (notifications v2): make this part of the admin schema with appropriate resolver for queries // notification: ['FALSE'], } diff --git a/package-lock.json b/package-lock.json index 0c27303ef..656423bff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@subsquid/typeorm-migration": "^0.1.4", "@subsquid/typeorm-store": "^0.2.0", "@types/lodash": "^4.14.189", + "@types/node-schedule": "^2.1.0", "@typescript/analyze-trace": "^0.9.1", "ajv": "^6.11.0", "async-lock": "^1.3.1", @@ -36,6 +37,7 @@ "haversine-distance": "^1.2.1", "lodash": "^4.17.21", "node-cache": "^5.1.2", + "node-schedule": "^2.1.1", "p-limit": "3.1.0", "patch-package": "^6.5.0", "pg": "8.8.0", @@ -79,6 +81,7 @@ "eslint-plugin-promise": "^6.1.1", "eslint-plugin-standard": "^5.0.0", "graphql-ws": "^5.14.0", + "mjml": "^4.14.1", "mocha": "^10.2.0", "openapi-typescript": "^6.1.1", "prettier": "^2.7.1", @@ -3577,6 +3580,12 @@ "npm": ">=5.0.0" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, "node_modules/@openapitools/openapi-generator-cli": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.7.0.tgz", @@ -5259,6 +5268,14 @@ "node": ">= 6" } }, + "node_modules/@types/node-schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -6008,6 +6025,12 @@ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6776,6 +6799,12 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -7137,6 +7166,69 @@ "node": "*" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -7201,6 +7293,18 @@ "validator": "^13.7.0" } }, + "node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7524,6 +7628,16 @@ "node": ">=10" } }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", @@ -7695,6 +7809,17 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "devOptional": true }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -7723,6 +7848,34 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssfilter": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", @@ -7925,6 +8078,12 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -7973,6 +8132,61 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -8033,6 +8247,57 @@ "tweetnacl": "1.x.x" } }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8099,6 +8364,18 @@ "node": ">=0.10.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -8232,6 +8509,18 @@ "node": ">=6" } }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -10015,6 +10304,92 @@ "react-is": "^16.7.0" } }, + "node_modules/html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "dev": true, + "dependencies": { + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" + }, + "bin": { + "html-minifier": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/html-minifier/node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "dev": true, + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/html-minifier/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/html-minifier/node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", + "dev": true + }, + "node_modules/html-minifier/node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/html-minifier/node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "dev": true, + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/html-minifier/node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -10227,6 +10602,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "node_modules/inquirer": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", @@ -10854,6 +11235,66 @@ "node": ">=6" } }, + "node_modules/js-beautify": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.9.tgz", + "integrity": "sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.3", + "glob": "^8.1.0", + "nopt": "^6.0.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/js-sdsl": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", @@ -11038,6 +11479,34 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/juice": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-9.1.0.tgz", + "integrity": "sha512-odblShmPrUoHUwRuC8EmLji5bPP2MLO1GL+gt4XU3tT2ECmbSrrMjtMQaqg3wgMFP2zvUzdPZGfxc5Trk3Z+fQ==", + "dev": true, + "dependencies": { + "cheerio": "^1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/juice/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -11283,6 +11752,11 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "node_modules/long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -11330,6 +11804,14 @@ "node": ">=12" } }, + "node_modules/luxon": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.1.tgz", + "integrity": "sha512-2USspxOCXWGIKHwuQ9XElxPPYrDOJHDQ5DQ870CoD+CxJbBnRDIBCfhioUJJjct7BKOy80Ia8cVstIcIMb/0+Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -11363,6 +11845,12 @@ "node": ">= 0.6" } }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "dev": true + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -11496,6 +11984,493 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mjml": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.14.1.tgz", + "integrity": "sha512-f/wnWWIVbeb/ge3ff7c/KYYizI13QbGIp03odwwkCThsJsacw4gpZZAU7V4gXY3HxSXP2/q3jxOfaHVbkfNpOQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "mjml-cli": "4.14.1", + "mjml-core": "4.14.1", + "mjml-migrate": "4.14.1", + "mjml-preset-core": "4.14.1", + "mjml-validator": "4.13.0" + }, + "bin": { + "mjml": "bin/mjml" + } + }, + "node_modules/mjml-accordion": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.14.1.tgz", + "integrity": "sha512-dpNXyjnhYwhM75JSjD4wFUa9JgHm86M2pa0CoTzdv1zOQz67ilc4BoK5mc2S0gOjJpjBShM5eOJuCyVIuAPC6w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-body": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.14.1.tgz", + "integrity": "sha512-YpXcK3o2o1U+fhI8f60xahrhXuHmav6BZez9vIN3ZEJOxPFSr+qgr1cT2iyFz50L5+ZsLIVj2ZY+ALQjdsg8ig==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-button": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.14.1.tgz", + "integrity": "sha512-V1Tl1vQ3lXYvvqHJHvGcc8URr7V1l/ZOsv7iLV4QRrh7kjKBXaRS7uUJtz6/PzEbNsGQCiNtXrODqcijLWlgaw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-carousel": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.14.1.tgz", + "integrity": "sha512-Ku3MUWPk/TwHxVgKEUtzspy/ePaWtN/3z6/qvNik0KIn0ZUIZ4zvR2JtaVL5nd30LHSmUaNj30XMPkCjYiKkFA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-cli": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.14.1.tgz", + "integrity": "sha512-Gy6MnSygFXs0U1qOXTHqBg2vZX2VL/fAacgQzD4MHq4OuybWaTNSzXRwxBXYCxT3IJB874n2Q0Mxp+Xka+tnZg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "chokidar": "^3.0.0", + "glob": "^7.1.1", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.14.1", + "mjml-migrate": "4.14.1", + "mjml-parser-xml": "4.14.1", + "mjml-validator": "4.13.0", + "yargs": "^16.1.0" + }, + "bin": { + "mjml-cli": "bin/mjml" + } + }, + "node_modules/mjml-cli/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mjml-cli/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mjml-cli/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/mjml-column": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.14.1.tgz", + "integrity": "sha512-iixVCIX1YJtpQuwG2WbDr7FqofQrlTtGQ4+YAZXGiLThs0En3xNIJFQX9xJ8sgLEGGltyooHiNICBRlzSp9fDg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-core": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.14.1.tgz", + "integrity": "sha512-di88rSfX+8r4r+cEqlQCO7CRM4mYZrfe2wSCu2je38i+ujjkLpF72cgLnjBlSG5aOUCZgYvlsZ85stqIz9LQfA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "cheerio": "1.0.0-rc.12", + "detect-node": "^2.0.4", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "juice": "^9.0.0", + "lodash": "^4.17.21", + "mjml-migrate": "4.14.1", + "mjml-parser-xml": "4.14.1", + "mjml-validator": "4.13.0" + } + }, + "node_modules/mjml-divider": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.14.1.tgz", + "integrity": "sha512-agqWY0aW2xaMiUOhYKDvcAAfOLalpbbtjKZAl1vWmNkURaoK4L7MgDilKHSJDFUlHGm2ZOArTrq8i6K0iyThBQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-group": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.14.1.tgz", + "integrity": "sha512-dJt5batgEJ7wxlxzqOfHOI94ABX+8DZBvAlHuddYO4CsLFHYv6XRIArLAMMnAKU76r6p3X8JxYeOjKZXdv49kg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-head": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.14.1.tgz", + "integrity": "sha512-KoCbtSeTAhx05Ugn9TB2UYt5sQinSCb7RGRer5iPQ3CrXj8hT5B5Svn6qvf/GACPkWl4auExHQh+XgLB+r3OEA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-head-attributes": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.14.1.tgz", + "integrity": "sha512-XdUNOp2csK28kBDSistInOyzWNwmu5HDNr4y1Z7vSQ1PfkmiuS6jWG7jHUjdoMhs27e6Leuyyc6a8gWSpqSWrg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-head-breakpoint": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.14.1.tgz", + "integrity": "sha512-Qw9l/W/I5Z9p7I4ShgnEpAL9if4472ejcznbBnp+4Gq+sZoPa7iYoEPsa9UCGutlaCh3N3tIi2qKhl9qD8DFxA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-head-font": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.14.1.tgz", + "integrity": "sha512-oBYm1gaOdEMjE5BoZouRRD4lCNZ1jcpz92NR/F7xDyMaKCGN6T/+r4S5dq1gOLm9zWqClRHaECdFJNEmrDpZqA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-head-html-attributes": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-html-attributes/-/mjml-head-html-attributes-4.14.1.tgz", + "integrity": "sha512-vlJsJc1Sm4Ml2XvLmp01zsdmWmzm6+jNCO7X3eYi9ngEh8LjMCLIQOncnOgjqm9uGpQu2EgUhwvYFZP2luJOVg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-head-preview": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.14.1.tgz", + "integrity": "sha512-89gQtt3fhl2dkYpHLF5HDQXz/RLpzecU6wmAIT7Dz6etjLGE1dgq2Ay6Bu/OeHjDcT1gbM131zvBwuXw8OydNw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-head-style": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.14.1.tgz", + "integrity": "sha512-XryOuf32EDuUCBT2k99C1+H87IOM919oY6IqxKFJCDkmsbywKIum7ibhweJdcxiYGONKTC6xjuibGD3fQTTYNQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-head-title": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.14.1.tgz", + "integrity": "sha512-aIfpmlQdf1eJZSSrFodmlC4g5GudBti2eMyG42M7/3NeLM6anEWoe+UkF/6OG4Zy0tCQ40BDJ5iBZlMsjQICzw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-hero": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.14.1.tgz", + "integrity": "sha512-TQJ3yfjrKYGkdEWjHLHhL99u/meKFYgnfJvlo9xeBvRjSM696jIjdqaPHaunfw4CP6d2OpCIMuacgOsvqQMWOA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-image": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.14.1.tgz", + "integrity": "sha512-jfKLPHXuFq83okwlNM1Um/AEWeVDgs2JXIOsWp2TtvXosnRvGGMzA5stKLYdy1x6UfKF4c1ovpMS162aYGp+xQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-migrate": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.14.1.tgz", + "integrity": "sha512-d+9HKQOhZi3ZFAaFSDdjzJX9eDQGjMf3BArLWNm2okC4ZgfJSpOc77kgCyFV8ugvwc8fFegPnSV60Jl4xtvK2A==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.14.1", + "mjml-parser-xml": "4.14.1", + "yargs": "^16.1.0" + }, + "bin": { + "migrate": "lib/cli.js" + } + }, + "node_modules/mjml-migrate/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mjml-migrate/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mjml-migrate/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/mjml-navbar": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.14.1.tgz", + "integrity": "sha512-rNy1Kw8CR3WQ+M55PFBAUDz2VEOjz+sk06OFnsnmNjoMVCjo1EV7OFLDAkmxAwqkC8h4zQWEOFY0MBqqoAg7+A==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-parser-xml": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.14.1.tgz", + "integrity": "sha512-9WQVeukbXfq9DUcZ8wOsHC6BTdhaVwTAJDYMIQglXLwKwN7I4pTCguDDHy5d0kbbzK5OCVxCdZe+bfVI6XANOQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "detect-node": "2.0.4", + "htmlparser2": "^8.0.1", + "lodash": "^4.17.15" + } + }, + "node_modules/mjml-parser-xml/node_modules/detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "node_modules/mjml-preset-core": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-preset-core/-/mjml-preset-core-4.14.1.tgz", + "integrity": "sha512-uUCqK9Z9d39rwB/+JDV2KWSZGB46W7rPQpc9Xnw1DRP7wD7qAfJwK6AZFCwfTgWdSxw0PwquVNcrUS9yBa9uhw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "mjml-accordion": "4.14.1", + "mjml-body": "4.14.1", + "mjml-button": "4.14.1", + "mjml-carousel": "4.14.1", + "mjml-column": "4.14.1", + "mjml-divider": "4.14.1", + "mjml-group": "4.14.1", + "mjml-head": "4.14.1", + "mjml-head-attributes": "4.14.1", + "mjml-head-breakpoint": "4.14.1", + "mjml-head-font": "4.14.1", + "mjml-head-html-attributes": "4.14.1", + "mjml-head-preview": "4.14.1", + "mjml-head-style": "4.14.1", + "mjml-head-title": "4.14.1", + "mjml-hero": "4.14.1", + "mjml-image": "4.14.1", + "mjml-navbar": "4.14.1", + "mjml-raw": "4.14.1", + "mjml-section": "4.14.1", + "mjml-social": "4.14.1", + "mjml-spacer": "4.14.1", + "mjml-table": "4.14.1", + "mjml-text": "4.14.1", + "mjml-wrapper": "4.14.1" + } + }, + "node_modules/mjml-raw": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.14.1.tgz", + "integrity": "sha512-9+4wzoXnCtfV6QPmjfJkZ50hxFB4Z8QZnl2Ac0D1Cn3dUF46UkmO5NLMu7UDIlm5DdFyycZrMOwvZS4wv9ksPw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-section": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.14.1.tgz", + "integrity": "sha512-Ik5pTUhpT3DOfB3hEmAWp8rZ0ilWtIivnL8XdUJRfgYE9D+MCRn+reIO+DAoJHxiQoI6gyeKkIP4B9OrQ7cHQw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-social": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.14.1.tgz", + "integrity": "sha512-G44aOZXgZHukirjkeQWTTV36UywtE2YvSwWGNfo/8d+k5JdJJhCIrlwaahyKEAyH63G1B0Zt8b2lEWx0jigYUw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-spacer": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.14.1.tgz", + "integrity": "sha512-5SfQCXTd3JBgRH1pUy6NVZ0lXBiRqFJPVHBdtC3OFvUS3q1w16eaAXlIUWMKTfy8CKhQrCiE6m65kc662ZpYxA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-table": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.14.1.tgz", + "integrity": "sha512-aVBdX3WpyKVGh/PZNn2KgRem+PQhWlvnD00DKxDejRBsBSKYSwZ0t3EfFvZOoJ9DzfHsN0dHuwd6Z18Ps44NFQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-text": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.14.1.tgz", + "integrity": "sha512-yZuvf5z6qUxEo5CqOhCUltJlR6oySKVcQNHwoV5sneMaKdmBiaU4VDnlYFera9gMD9o3KBHIX6kUg7EHnCwBRQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "node_modules/mjml-validator": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.13.0.tgz", + "integrity": "sha512-uURYfyQYtHJ6Qz/1A7/+E9ezfcoISoLZhYK3olsxKRViwaA2Mm8gy/J3yggZXnsUXWUns7Qymycm5LglLEIiQg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6" + } + }, + "node_modules/mjml-wrapper": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.14.1.tgz", + "integrity": "sha512-aA5Xlq6d0hZ5LY+RvSaBqmVcLkvPvdhyAv3vQf3G41Gfhel4oIPmkLnVpHselWhV14A0KwIOIAKVxHtSAxyOTQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1", + "mjml-section": "4.14.1" + } + }, "node_modules/mkdirp": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", @@ -11919,7 +12894,35 @@ "integrity": "sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA==", "dev": true }, - "node_modules/normalize-path": { + "node_modules/node-schedule": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", + "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==", + "dependencies": { + "cron-parser": "^4.2.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", @@ -11928,6 +12931,18 @@ "node": ">=0.10.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -12747,6 +13762,12 @@ "node": ">= 8" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, "node_modules/protobufjs": { "version": "6.11.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", @@ -12967,6 +13988,15 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/relay-runtime": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", @@ -13421,6 +14451,15 @@ "node": ">=8" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -13431,6 +14470,11 @@ "tslib": "^2.0.3" } }, + "node_modules/sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14380,7 +15424,7 @@ "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "optional": true, + "devOptional": true, "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -14597,6 +15641,15 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/validator": { "version": "13.7.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", @@ -14658,6 +15711,132 @@ "defaults": "^1.0.3" } }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/web-streams-polyfill": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", @@ -17761,6 +18940,12 @@ "node-fetch": "^2.6.1" } }, + "@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, "@openapitools/openapi-generator-cli": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.7.0.tgz", @@ -19163,6 +20348,14 @@ } } }, + "@types/node-schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg==", + "requires": { + "@types/node": "*" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -19693,6 +20886,12 @@ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -20260,6 +21459,12 @@ "unpipe": "1.0.0" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -20540,6 +21745,56 @@ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "dependencies": { + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + } + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -20592,6 +21847,15 @@ "validator": "^13.7.0" } }, + "clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + } + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -20848,6 +22112,16 @@ } } }, + "config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", @@ -20988,6 +22262,14 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "devOptional": true }, + "cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "requires": { + "luxon": "^3.2.1" + } + }, "cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -21013,6 +22295,25 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, "cssfilter": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", @@ -21168,6 +22469,12 @@ "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, "dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -21207,6 +22514,43 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -21258,6 +22602,44 @@ "tweetnacl": "1.x.x" } }, + "editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "requires": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true + }, + "minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -21322,6 +22704,12 @@ } } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -21433,6 +22821,12 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, + "escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "dev": true + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -22749,40 +24143,115 @@ "react-is": "^16.7.0" } }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", "dev": true, "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" }, "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", "dev": true, "requires": { - "ms": "2.1.2" + "no-case": "^2.2.0", + "upper-case": "^1.1.1" } }, - "ms": { - "version": "2.1.2", + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", + "dev": true + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "^1.1.1" + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "dev": true, + "requires": { + "no-case": "^2.2.0" + } + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", + "dev": true + } + } + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true @@ -22896,6 +24365,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "inquirer": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", @@ -23325,6 +24800,51 @@ "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==" }, + "js-beautify": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.9.tgz", + "integrity": "sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==", + "dev": true, + "requires": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.3", + "glob": "^8.1.0", + "nopt": "^6.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "js-sdsl": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", @@ -23473,6 +24993,27 @@ } } }, + "juice": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-9.1.0.tgz", + "integrity": "sha512-odblShmPrUoHUwRuC8EmLji5bPP2MLO1GL+gt4XU3tT2ECmbSrrMjtMQaqg3wgMFP2zvUzdPZGfxc5Trk3Z+fQ==", + "dev": true, + "requires": { + "cheerio": "^1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "dependencies": { + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true + } + } + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -23645,187 +25186,675 @@ "is-fullwidth-code-point": "^3.0.0" } }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "loglevel": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", + "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "devOptional": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "lower-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-2.0.2.tgz", + "integrity": "sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "lru-cache": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz", + "integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==" + }, + "luxon": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.1.tgz", + "integrity": "sha512-2USspxOCXWGIKHwuQ9XElxPPYrDOJHDQ5DQ870CoD+CxJbBnRDIBCfhioUJJjct7BKOy80Ia8cVstIcIMb/0+Q==" + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "merkletreejs": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.3.9.tgz", + "integrity": "sha512-NjlATjJr4NEn9s8v/VEHhgwRWaE1eA/Une07d9SEqKzULJi1Wsh0Y3svwJdP2bYLMmgSBHzOrNydMWM1NN9VeQ==", + "requires": { + "bignumber.js": "^9.0.1", + "buffer-reverse": "^1.0.1", + "crypto-js": "^3.1.9-1", + "treeify": "^1.1.0", + "web3-utils": "^1.3.4" + } + }, + "meros": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz", + "integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==", + "dev": true, + "requires": {} + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" + }, + "mjml": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.14.1.tgz", + "integrity": "sha512-f/wnWWIVbeb/ge3ff7c/KYYizI13QbGIp03odwwkCThsJsacw4gpZZAU7V4gXY3HxSXP2/q3jxOfaHVbkfNpOQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "mjml-cli": "4.14.1", + "mjml-core": "4.14.1", + "mjml-migrate": "4.14.1", + "mjml-preset-core": "4.14.1", + "mjml-validator": "4.13.0" + } + }, + "mjml-accordion": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.14.1.tgz", + "integrity": "sha512-dpNXyjnhYwhM75JSjD4wFUa9JgHm86M2pa0CoTzdv1zOQz67ilc4BoK5mc2S0gOjJpjBShM5eOJuCyVIuAPC6w==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-body": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.14.1.tgz", + "integrity": "sha512-YpXcK3o2o1U+fhI8f60xahrhXuHmav6BZez9vIN3ZEJOxPFSr+qgr1cT2iyFz50L5+ZsLIVj2ZY+ALQjdsg8ig==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-button": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.14.1.tgz", + "integrity": "sha512-V1Tl1vQ3lXYvvqHJHvGcc8URr7V1l/ZOsv7iLV4QRrh7kjKBXaRS7uUJtz6/PzEbNsGQCiNtXrODqcijLWlgaw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-carousel": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.14.1.tgz", + "integrity": "sha512-Ku3MUWPk/TwHxVgKEUtzspy/ePaWtN/3z6/qvNik0KIn0ZUIZ4zvR2JtaVL5nd30LHSmUaNj30XMPkCjYiKkFA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-cli": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.14.1.tgz", + "integrity": "sha512-Gy6MnSygFXs0U1qOXTHqBg2vZX2VL/fAacgQzD4MHq4OuybWaTNSzXRwxBXYCxT3IJB874n2Q0Mxp+Xka+tnZg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "chokidar": "^3.0.0", + "glob": "^7.1.1", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.14.1", + "mjml-migrate": "4.14.1", + "mjml-parser-xml": "4.14.1", + "mjml-validator": "4.13.0", + "yargs": "^16.1.0" + }, + "dependencies": { + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } + }, + "mjml-column": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.14.1.tgz", + "integrity": "sha512-iixVCIX1YJtpQuwG2WbDr7FqofQrlTtGQ4+YAZXGiLThs0En3xNIJFQX9xJ8sgLEGGltyooHiNICBRlzSp9fDg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-core": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.14.1.tgz", + "integrity": "sha512-di88rSfX+8r4r+cEqlQCO7CRM4mYZrfe2wSCu2je38i+ujjkLpF72cgLnjBlSG5aOUCZgYvlsZ85stqIz9LQfA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "cheerio": "1.0.0-rc.12", + "detect-node": "^2.0.4", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "juice": "^9.0.0", + "lodash": "^4.17.21", + "mjml-migrate": "4.14.1", + "mjml-parser-xml": "4.14.1", + "mjml-validator": "4.13.0" + } + }, + "mjml-divider": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.14.1.tgz", + "integrity": "sha512-agqWY0aW2xaMiUOhYKDvcAAfOLalpbbtjKZAl1vWmNkURaoK4L7MgDilKHSJDFUlHGm2ZOArTrq8i6K0iyThBQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-group": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.14.1.tgz", + "integrity": "sha512-dJt5batgEJ7wxlxzqOfHOI94ABX+8DZBvAlHuddYO4CsLFHYv6XRIArLAMMnAKU76r6p3X8JxYeOjKZXdv49kg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-head": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.14.1.tgz", + "integrity": "sha512-KoCbtSeTAhx05Ugn9TB2UYt5sQinSCb7RGRer5iPQ3CrXj8hT5B5Svn6qvf/GACPkWl4auExHQh+XgLB+r3OEA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-head-attributes": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.14.1.tgz", + "integrity": "sha512-XdUNOp2csK28kBDSistInOyzWNwmu5HDNr4y1Z7vSQ1PfkmiuS6jWG7jHUjdoMhs27e6Leuyyc6a8gWSpqSWrg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-head-breakpoint": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.14.1.tgz", + "integrity": "sha512-Qw9l/W/I5Z9p7I4ShgnEpAL9if4472ejcznbBnp+4Gq+sZoPa7iYoEPsa9UCGutlaCh3N3tIi2qKhl9qD8DFxA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-head-font": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.14.1.tgz", + "integrity": "sha512-oBYm1gaOdEMjE5BoZouRRD4lCNZ1jcpz92NR/F7xDyMaKCGN6T/+r4S5dq1gOLm9zWqClRHaECdFJNEmrDpZqA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-head-html-attributes": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-html-attributes/-/mjml-head-html-attributes-4.14.1.tgz", + "integrity": "sha512-vlJsJc1Sm4Ml2XvLmp01zsdmWmzm6+jNCO7X3eYi9ngEh8LjMCLIQOncnOgjqm9uGpQu2EgUhwvYFZP2luJOVg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-head-preview": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.14.1.tgz", + "integrity": "sha512-89gQtt3fhl2dkYpHLF5HDQXz/RLpzecU6wmAIT7Dz6etjLGE1dgq2Ay6Bu/OeHjDcT1gbM131zvBwuXw8OydNw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-head-style": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.14.1.tgz", + "integrity": "sha512-XryOuf32EDuUCBT2k99C1+H87IOM919oY6IqxKFJCDkmsbywKIum7ibhweJdcxiYGONKTC6xjuibGD3fQTTYNQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-head-title": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.14.1.tgz", + "integrity": "sha512-aIfpmlQdf1eJZSSrFodmlC4g5GudBti2eMyG42M7/3NeLM6anEWoe+UkF/6OG4Zy0tCQ40BDJ5iBZlMsjQICzw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-hero": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.14.1.tgz", + "integrity": "sha512-TQJ3yfjrKYGkdEWjHLHhL99u/meKFYgnfJvlo9xeBvRjSM696jIjdqaPHaunfw4CP6d2OpCIMuacgOsvqQMWOA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-image": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.14.1.tgz", + "integrity": "sha512-jfKLPHXuFq83okwlNM1Um/AEWeVDgs2JXIOsWp2TtvXosnRvGGMzA5stKLYdy1x6UfKF4c1ovpMS162aYGp+xQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-migrate": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.14.1.tgz", + "integrity": "sha512-d+9HKQOhZi3ZFAaFSDdjzJX9eDQGjMf3BArLWNm2okC4ZgfJSpOc77kgCyFV8ugvwc8fFegPnSV60Jl4xtvK2A==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.14.1", + "mjml-parser-xml": "4.14.1", + "yargs": "^16.1.0" + }, + "dependencies": { + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true } } }, - "loglevel": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", - "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==" - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "devOptional": true, + "mjml-navbar": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.14.1.tgz", + "integrity": "sha512-rNy1Kw8CR3WQ+M55PFBAUDz2VEOjz+sk06OFnsnmNjoMVCjo1EV7OFLDAkmxAwqkC8h4zQWEOFY0MBqqoAg7+A==", + "dev": true, "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" } }, - "loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "mjml-parser-xml": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.14.1.tgz", + "integrity": "sha512-9WQVeukbXfq9DUcZ8wOsHC6BTdhaVwTAJDYMIQglXLwKwN7I4pTCguDDHy5d0kbbzK5OCVxCdZe+bfVI6XANOQ==", "dev": true, "requires": { - "get-func-name": "^2.0.0" + "@babel/runtime": "^7.14.6", + "detect-node": "2.0.4", + "htmlparser2": "^8.0.1", + "lodash": "^4.17.15" + }, + "dependencies": { + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + } } }, - "lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "mjml-preset-core": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-preset-core/-/mjml-preset-core-4.14.1.tgz", + "integrity": "sha512-uUCqK9Z9d39rwB/+JDV2KWSZGB46W7rPQpc9Xnw1DRP7wD7qAfJwK6AZFCwfTgWdSxw0PwquVNcrUS9yBa9uhw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "mjml-accordion": "4.14.1", + "mjml-body": "4.14.1", + "mjml-button": "4.14.1", + "mjml-carousel": "4.14.1", + "mjml-column": "4.14.1", + "mjml-divider": "4.14.1", + "mjml-group": "4.14.1", + "mjml-head": "4.14.1", + "mjml-head-attributes": "4.14.1", + "mjml-head-breakpoint": "4.14.1", + "mjml-head-font": "4.14.1", + "mjml-head-html-attributes": "4.14.1", + "mjml-head-preview": "4.14.1", + "mjml-head-style": "4.14.1", + "mjml-head-title": "4.14.1", + "mjml-hero": "4.14.1", + "mjml-image": "4.14.1", + "mjml-navbar": "4.14.1", + "mjml-raw": "4.14.1", + "mjml-section": "4.14.1", + "mjml-social": "4.14.1", + "mjml-spacer": "4.14.1", + "mjml-table": "4.14.1", + "mjml-text": "4.14.1", + "mjml-wrapper": "4.14.1" + } + }, + "mjml-raw": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.14.1.tgz", + "integrity": "sha512-9+4wzoXnCtfV6QPmjfJkZ50hxFB4Z8QZnl2Ac0D1Cn3dUF46UkmO5NLMu7UDIlm5DdFyycZrMOwvZS4wv9ksPw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" + } + }, + "mjml-section": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.14.1.tgz", + "integrity": "sha512-Ik5pTUhpT3DOfB3hEmAWp8rZ0ilWtIivnL8XdUJRfgYE9D+MCRn+reIO+DAoJHxiQoI6gyeKkIP4B9OrQ7cHQw==", "dev": true, "requires": { - "tslib": "^2.0.3" + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" } }, - "lower-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-2.0.2.tgz", - "integrity": "sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==", + "mjml-social": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.14.1.tgz", + "integrity": "sha512-G44aOZXgZHukirjkeQWTTV36UywtE2YvSwWGNfo/8d+k5JdJJhCIrlwaahyKEAyH63G1B0Zt8b2lEWx0jigYUw==", "dev": true, "requires": { - "tslib": "^2.0.3" + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" } }, - "lru-cache": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz", - "integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==" - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "mjml-spacer": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.14.1.tgz", + "integrity": "sha512-5SfQCXTd3JBgRH1pUy6NVZ0lXBiRqFJPVHBdtC3OFvUS3q1w16eaAXlIUWMKTfy8CKhQrCiE6m65kc662ZpYxA==", + "dev": true, "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" } }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "merkletreejs": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.3.9.tgz", - "integrity": "sha512-NjlATjJr4NEn9s8v/VEHhgwRWaE1eA/Une07d9SEqKzULJi1Wsh0Y3svwJdP2bYLMmgSBHzOrNydMWM1NN9VeQ==", + "mjml-table": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.14.1.tgz", + "integrity": "sha512-aVBdX3WpyKVGh/PZNn2KgRem+PQhWlvnD00DKxDejRBsBSKYSwZ0t3EfFvZOoJ9DzfHsN0dHuwd6Z18Ps44NFQ==", + "dev": true, "requires": { - "bignumber.js": "^9.0.1", - "buffer-reverse": "^1.0.1", - "crypto-js": "^3.1.9-1", - "treeify": "^1.1.0", - "web3-utils": "^1.3.4" + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" } }, - "meros": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz", - "integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==", + "mjml-text": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.14.1.tgz", + "integrity": "sha512-yZuvf5z6qUxEo5CqOhCUltJlR6oySKVcQNHwoV5sneMaKdmBiaU4VDnlYFera9gMD9o3KBHIX6kUg7EHnCwBRQ==", "dev": true, - "requires": {} - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1" } }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "mjml-validator": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.13.0.tgz", + "integrity": "sha512-uURYfyQYtHJ6Qz/1A7/+E9ezfcoISoLZhYK3olsxKRViwaA2Mm8gy/J3yggZXnsUXWUns7Qymycm5LglLEIiQg==", + "dev": true, "requires": { - "mime-db": "1.52.0" + "@babel/runtime": "^7.14.6" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "mjml-wrapper": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.14.1.tgz", + "integrity": "sha512-aA5Xlq6d0hZ5LY+RvSaBqmVcLkvPvdhyAv3vQf3G41Gfhel4oIPmkLnVpHselWhV14A0KwIOIAKVxHtSAxyOTQ==", + "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.14.1", + "mjml-section": "4.14.1" } }, - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" - }, "mkdirp": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", @@ -24157,12 +26186,40 @@ "integrity": "sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA==", "dev": true }, + "node-schedule": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", + "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==", + "requires": { + "cron-parser": "^4.2.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + } + }, + "nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "requires": { + "abbrev": "^1.0.0" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, "nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -24773,6 +26830,12 @@ "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==" }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, "protobufjs": { "version": "6.11.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", @@ -24929,6 +26992,12 @@ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true + }, "relay-runtime": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", @@ -25273,6 +27342,12 @@ "is-fullwidth-code-point": "^3.0.0" } }, + "slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "dev": true + }, "snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -25283,6 +27358,11 @@ "tslib": "^2.0.3" } }, + "sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -25911,7 +27991,7 @@ "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "optional": true + "devOptional": true }, "uid": { "version": "2.0.1", @@ -26070,6 +28150,12 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "dev": true + }, "validator": { "version": "13.7.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", @@ -26114,6 +28200,99 @@ "defaults": "^1.0.3" } }, + "web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + } + } + }, + "domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "dependencies": { + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + } + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + } + } + }, "web-streams-polyfill": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", diff --git a/package.json b/package.json index 7ac17bc53..790be3bc8 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "generate:schema": "./scripts/generate-schema-file.sh", "generate:types:auth-api": "npx openapi-typescript ./src/auth-server/openapi.yml -o ./src/auth-server/generated/api-types.ts -c ./prettierrc.js", "generate:docs:auth-api": "rm -rf ./src/auth-server/docs/* && npx openapi-generator-cli generate -i ./src/auth-server/openapi.yml -g markdown -o ./src/auth-server/docs", - "build": "npm run generate:schema; rm -rf lib && tsc && ./scripts/copyfiles.sh", + "generate:emails": "./scripts/generate-emails.js -i ./src/auth-server/emails/templates/mjml/*.mjml -o ./src/auth-server/emails/templates -p ./src/auth-server/emails/templates/preview", + "watch:generate:emails": "node --watch ./scripts/generate-emails.js --watch -i ./src/auth-server/emails/templates/mjml/*.mjml -o ./src/auth-server/emails/templates -p ./src/auth-server/emails/templates/preview", + "build": "npm run generate:schema; npm run generate:emails; rm -rf lib && tsc && ./scripts/copyfiles.sh", "lint": "eslint --ext .ts ./src", "format": "prettier --write .", "checks": "prettier --check . && npm run lint && make codegen && tsc --noEmit --pretty", @@ -21,6 +23,7 @@ "tests:compareState": "npx ts-node ./src/tests/compareState.ts", "tests:benchmark": "npx ts-node ./src/tests/benchmarks/index.ts", "tests:auth-api": "./src/auth-server/tests/run.sh", + "tests:mail-scheduler": "./src/mail-scheduler/tests/run-tests.sh", "tests:notifications": "./src/tests/integration/run.sh", "offchain-state:export": "node ./lib/scripts/export.js", "get-public-key": "node ./lib/scripts/getPublicKey.js" @@ -45,6 +48,7 @@ "@subsquid/typeorm-migration": "^0.1.4", "@subsquid/typeorm-store": "^0.2.0", "@types/lodash": "^4.14.189", + "@types/node-schedule": "^2.1.0", "@typescript/analyze-trace": "^0.9.1", "ajv": "^6.11.0", "async-lock": "^1.3.1", @@ -61,6 +65,7 @@ "haversine-distance": "^1.2.1", "lodash": "^4.17.21", "node-cache": "^5.1.2", + "node-schedule": "^2.1.1", "p-limit": "3.1.0", "patch-package": "^6.5.0", "pg": "8.8.0", @@ -104,6 +109,7 @@ "eslint-plugin-promise": "^6.1.1", "eslint-plugin-standard": "^5.0.0", "graphql-ws": "^5.14.0", + "mjml": "^4.14.1", "mocha": "^10.2.0", "openapi-typescript": "^6.1.1", "prettier": "^2.7.1", diff --git a/schema/notifications.graphql b/schema/notifications.graphql index 184a43be7..44623eabd 100644 --- a/schema/notifications.graphql +++ b/schema/notifications.graphql @@ -6,13 +6,34 @@ type NotificationEmailDelivery @entity { notification: Notification! "notification delivery status" - deliveryStatus: EmailDeliveryStatus! + attempts: [EmailDeliveryAttempt!]! @derivedFrom(field: "notificationDelivery") + + "mark as discard after max attempts or successful attempt" + discard: Boolean! +} + +type EmailDeliveryAttempt @entity { + "UUID" + id: ID! + + "notification Fk" + notificationDelivery: NotificationEmailDelivery! + + "delivery status" + status: DeliveryStatus! + + "datetime" + timestamp: DateTime! +} + +union DeliveryStatus = EmailSuccess | EmailFailure + +type EmailSuccess @variant { + phantom: Int } -enum EmailDeliveryStatus { - Unsent - Success - Failure +type EmailFailure @variant { + errorStatus: String! } type Read { diff --git a/scripts/generate-emails.js b/scripts/generate-emails.js new file mode 100755 index 000000000..ff4042896 --- /dev/null +++ b/scripts/generate-emails.js @@ -0,0 +1,104 @@ +#!/usr/bin/env node + +const { existsSync, mkdirSync, readFileSync, watchFile, writeFileSync } = require('fs') +const { basename, join } = require('path') +const { merge } = require('lodash') +const mjml2html = require('mjml') +const { compile } = require('handlebars') + +const ghRaw = 'https://raw.githubusercontent.com/Joystream' +const gleevLogos = `${ghRaw}/atlas-notification-assets/main/logos/gleev` +const gleevRoot = 'https://atlas-git-fork-thesan-feature-orion-notifications-joystream.vercel.app' +const contexts = { + gleev: { + title: 'Hi alice,', + subTitle: 'You have a new notifications:', + + app: { + name: 'Gleev', + nameAlt: 'Gleev.xyz', + logo: `${gleevLogos}/header-viewer.png`, + logoAlt: `${gleevLogos}/footer.png`, + homeLink: gleevRoot, + notificationLink: `${gleevRoot}/notifications/member`, + unsubscribeLink: `${gleevRoot}/member/settings?tab=Notifications`, + }, + + notification: { + text: 'bob replied to your comment under the video: “Web3 Wages & Salaries Demystified”', + link: `${gleevRoot}/video/1?commentId=METAPROTOCOL-OLYMPIA-3882-2`, + date: '07 Jul 2023 at 10:41', + avatar: `${ghRaw}/founding-members/main/avatars/primary-avatar/1.png`, + icon: `${ghRaw}/atlas-notification-assets/main/icons/follow.png`, + iconColor: '#5A58FF', + }, + }, + + get studio() { + return merge({}, this.gleev, { + title: 'Your channel “Joyblocks”,', + subTitle: 'Has a new notifications:', + app: { + name: 'Studio', + logo: `${gleevLogos}/header-studio.png`, + notificationPage: `${gleevRoot}/studio/notifications/channel`, + unsubscribeLink: `${gleevRoot}/studio/channel?tab=Notifications`, + }, + }) + }, +} + +const args = process.argv +const inputStarts = args.indexOf('-i') + 1 +const inputEnds = args.indexOf('-o', inputStarts) +const inputs = args.slice(inputStarts, inputEnds) +const templateDir = getArg('-o') +const previewDir = getArg('-p') +const shouldWatch = args.indexOf('--watch') > -1 + +process.stdout.write('\nStarted!\n') + +if (!inputs.length) throw 'Missing input files!' + +const dirs = { templateDir, previewDir } +for (const key in dirs) { + const dir = dirs[key] + if (!dir) throw `Missing ${key}!` + if (existsSync(dir)) continue + process.stdout.write(`Create: ${dir}\n`) + mkdirSync(dir, { recursive: true }) +} + +for (const path of inputs) { + processFile(path) + if (shouldWatch) { + process.stdout.write(`Watching ${path}...\n`) + watchFile(path, { interval: 200 }, () => { + process.stdout.write(`\n${path} was updated!\n`) + processFile(path) + }) + } +} + +function processFile(path) { + const fileName = basename(path).replace(/\.mjml$/, '') + const content = readFileSync(path, { encoding: 'utf8' }) + + const template = mjml2html(content).html + const templatePath = join(templateDir, fileName) + writeFileSync(templatePath, template) + process.stdout.write(`Template file written at: ${templatePath}\n`) + + for (const app of Object.keys(contexts)) { + const context = contexts[app] + const previewPath = join(previewDir, fileName.replace(/\..*$/, `-${app}.html`)) + const preview = compile(template, { strict: true })(context) + writeFileSync(previewPath, preview) + process.stdout.write(`Preview file written at: ${previewPath}\n`) + } +} + +function getArg(key) { + const index = args.indexOf(key) + 1 + if (index) return args[index] +} diff --git a/src/auth-server/emails/index.ts b/src/auth-server/emails/index.ts index b8630d56f..3fce25be5 100644 --- a/src/auth-server/emails/index.ts +++ b/src/auth-server/emails/index.ts @@ -1,6 +1,7 @@ import { compile } from 'handlebars' import fs from 'fs' import path from 'path' +import { NotificationData } from '../../utils/notification/notificationsData' function getEmailTemplateData(templatePath: string): (data: T) => string { const fullPath = path.join(__dirname, 'templates/' + templatePath) @@ -23,10 +24,20 @@ export function registerEmailContent(data: RegisterEmailTemplateData): string { } export type NotificationEmailTemplateData = { - notificationText: string - notificationLink: string - preferencePageLink: string - appName: string + title: string + subTitle: string + + app: { + name: string + nameAlt: string + logo: string + logoAlt: string + homeLink: string + notificationLink: string + unsubscribeLink: string + } + + notification: NotificationData } export function notificationEmailContent(data: NotificationEmailTemplateData): string { return getEmailTemplateData('notification.html.mst')(data) diff --git a/src/auth-server/emails/templates/NotificationLogo.png b/src/auth-server/emails/templates/NotificationLogo.png deleted file mode 100644 index c36fdd62b..000000000 Binary files a/src/auth-server/emails/templates/NotificationLogo.png and /dev/null differ diff --git a/src/auth-server/emails/templates/mjml/notification.html.mst.mjml b/src/auth-server/emails/templates/mjml/notification.html.mst.mjml new file mode 100644 index 000000000..15ef9036d --- /dev/null +++ b/src/auth-server/emails/templates/mjml/notification.html.mst.mjml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ title }} + + {{ subTitle }} + + + + + + + + + +
+ notification icon +
+
+
+ + + + + {{ notification.text }} + + + {{ notification.date }} + + +
+
+ + + + + + View on {{ app.name }} + + + +
+ + + + + + + + + + + + + {{ app.nameAlt }} + + + + + Joystream.org + + + + + + + + + You can + + unsubscribe + + anytime. + + + +
+
diff --git a/src/auth-server/emails/templates/notification.html.mst b/src/auth-server/emails/templates/notification.html.mst deleted file mode 100644 index e322b88bb..000000000 --- a/src/auth-server/emails/templates/notification.html.mst +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - -
- -
- - - - - - -
- - - - - - -
- Logo -
-
-
- -
- - - - - - -
-
{{ appName }}
-
-
- -
-
- - - -
- - - - - - -
- -
- - - - - - -
-
{{ notificationText }}
-
-
- -
-
- -
- - - - - - -
- -
- - - -
-
- -
- - - - - - -
- - - - -
-

- View in {{ appName }}
-

-
-
-
- -
-
- - - -
- - - - - - -
- -
- - - - - - - - - - - - -
- - - - - - -
- Icon -
-
-
{{ appName }} | Joystream
-
-
You can - unsubscribe - at any time.
-
-
- -
-
- -
- - - \ No newline at end of file diff --git a/src/mail-scheduler/index.ts b/src/mail-scheduler/index.ts new file mode 100644 index 000000000..601ba1a45 --- /dev/null +++ b/src/mail-scheduler/index.ts @@ -0,0 +1,52 @@ +import { ConfigVariable, config } from '../utils/config' +import { EmailDeliveryAttempt, NotificationEmailDelivery } from '../model' +import { EntityManager } from 'typeorm' +import { globalEm } from '../utils/globalEm' +import { uniqueId } from '../utils/crypto' +import { executeMailDelivery } from './utils' + +export async function getMaxAttempts(em: EntityManager): Promise { + const maxAttempts = await config.get(ConfigVariable.EmailNotificationDeliveryMaxAttempts, em) + return maxAttempts +} + +export async function mailsToDeliver(em: EntityManager): Promise { + const result = await em.getRepository(NotificationEmailDelivery).find({ + where: { + discard: false, + }, + relations: { + notification: { account: true }, + attempts: true, + }, + }) + return result +} + +export async function deliverEmails() { + const em = await globalEm + const newEmailDeliveries = await mailsToDeliver(em) + for (const notificationDelivery of newEmailDeliveries) { + const toAccount = notificationDelivery.notification.account + const appName = await config.get(ConfigVariable.AppName, em) + const content = '' // await createMailContent(em, appName, notification) + const attempts = notificationDelivery.attempts + const status = await executeMailDelivery(appName, em, toAccount, content) + const newAttempt = new EmailDeliveryAttempt({ + id: uniqueId(), + timestamp: new Date(), + status, + }) + attempts.push(newAttempt) + notificationDelivery.attempts = attempts + if (status.isTypeOf === 'EmailSuccess') { + notificationDelivery.discard = true + } else { + if (attempts.length >= (await getMaxAttempts(em))) { + notificationDelivery.discard = true + } + } + await em.save(newAttempt) + } + await em.save(newEmailDeliveries) +} diff --git a/src/mail-scheduler/tests/.env b/src/mail-scheduler/tests/.env new file mode 100644 index 000000000..19af08042 --- /dev/null +++ b/src/mail-scheduler/tests/.env @@ -0,0 +1,69 @@ +# LOCAL DEV ENVIRONMENT + +ORION_ENV=development +DEV_DISABLE_SAME_SITE=true + +# Db config +DB_NAME=squid +DB_PASS=squid +DB_ADMIN_USER=admin +DB_ADMIN_PASS=admin +DB_PORT=23798 + +# Processor service prometheus port +PROCESSOR_PROMETHEUS_PORT=3337 +# Graphql server port +GQL_PORT=4350 +# Auth api port +AUTH_API_PORT=4074 + +# Archive gateway url +ARCHIVE_GATEWAY_URL=${CUSTOM_ARCHIVE_GATEWAY_URL:-http://localhost:8888/graphql} + +# ====================================================== +# ============== DEFAULT CONFIG VALUES ================= +# ====================================================== +APP_NAME=Gleev +SUPPORT_NO_CATEGORY_VIDEOS=true +SUPPORT_NEW_CATEGORIES=true +KILL_SWITCH_ON=false +# 10 seconds +VIDEO_VIEW_PER_USER_TIME_LIMIT=10 +# Operator API secret +OPERATOR_SECRET=this-is-not-so-secret-change-it +# every 50 views video relevance score will be recalculated +VIDEO_RELEVANCE_VIEWS_TICK=50 +# [ +# newness (negative number of days since created) weight, +# views weight, +# comments weight, +# rections weights, +# [joystream creation weight, YT creation weight] +# ] +RELEVANCE_WEIGHTS="[1, 0.03, 0.3, 0.5, [7,3]]" +MAX_CACHED_ENTITIES=1000 +APP_PRIVATE_KEY=this-is-not-so-secret-change-it +SESSION_EXPIRY_AFTER_INACTIVITY_MINUTES=60 +SESSION_MAX_DURATION_HOURS=720 +EMAIL_CONFIRMATION_ROUTE=http://localhost:4074/api/v1/confirm-email?token={token} +EMAIL_CONFIRMATION_TOKEN_EXPIRY_TIME_HOURS=24 +EMAIL_CONFIRMATION_TOKEN_RATE_LIMIT=5 +ACCOUNT_OWNERSHIP_PROOF_EXPIRY_TIME_SECONDS=300 # 5 minutes +COOKIE_SECRET=this-is-not-so-secret-change-it + +# Notification related variables +APP_ROOT_DOMAIN=gleev.xyz +NOTIFICATION_CENTER_PATH=notification-center # https://${APP_ROOT_DOMAIN}/${NOTIFICATION_CENTER_PATH} + +TRUST_PROXY=uniquelocal + +# Sendgrid API +SENDGRID_API_KEY= +SENDGRID_FROM_EMAIL=gateway@example.com + +# Debug settings +SQD_DEBUG=api:* +OPENAPI_PLAYGROUND=true + +TESTING=true +EMAIL_NOTIFICATION_DELIVERY_MAX_ATTEMPTS=5 diff --git a/src/mail-scheduler/tests/config.ts b/src/mail-scheduler/tests/config.ts new file mode 100644 index 000000000..7ca0b6179 --- /dev/null +++ b/src/mail-scheduler/tests/config.ts @@ -0,0 +1,6 @@ +import { config as dontenvConfig } from 'dotenv' +import path from 'path' + +dontenvConfig({ + path: path.resolve(__dirname, './.env'), +}) diff --git a/src/mail-scheduler/tests/run-tests.sh b/src/mail-scheduler/tests/run-tests.sh new file mode 100755 index 000000000..6701d9ecc --- /dev/null +++ b/src/mail-scheduler/tests/run-tests.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")" +cd $SCRIPT_PATH/../../.. + +if ! [[ "$(docker container inspect -f '{{.State.Running}}' orion_db)" = "true" ]]; then + docker network create joystream_default || true + docker-compose up -d orion_db + until docker-compose logs orion_db | grep "database system is ready to accept connections" >/dev/null; do + sleep 1 + done + make migrate >/dev/null +fi + +cleanup() { + docker-compose down -v +} + +# Run the tests +npx ts-mocha "./src/mail-scheduler/tests/*.ts" --timeout 60000 --exit + +trap cleanup EXIT diff --git a/src/mail-scheduler/tests/scheduler.test.ts b/src/mail-scheduler/tests/scheduler.test.ts new file mode 100644 index 000000000..360f534e9 --- /dev/null +++ b/src/mail-scheduler/tests/scheduler.test.ts @@ -0,0 +1,159 @@ +import { expect } from 'chai' +import { NotificationEmailDelivery, Notification } from '../../model' +import { clearDb, populateDbWithSeedData } from './testUtils' +import { globalEm } from '../../utils/globalEm' +import { getMaxAttempts, deliverEmails } from '..' +import { EntityManager } from 'typeorm' +import { RUNTIME_NOTIFICATION_ID_TAG } from '../../utils/notification/helpers' + +const getDeliveryFromNotificationId = async (em: EntityManager, notificationId: string) => { + const res = await em.getRepository(NotificationEmailDelivery).findOneOrFail({ + where: { notification: { id: notificationId } }, + relations: { attempts: true }, + }) + return res +} + +const correctRecipientAccountEmail = async (em: EntityManager, notificationId: string) => { + const accountId = (await em.getRepository(Notification).findOneByOrFail({ id: notificationId })) + .accountId + const account = await em.getRepository('Account').findOneByOrFail({ id: accountId }) + account.email = `correct-${accountId}@example.com` + await em.save(account) +} + +describe('Scheduler', () => { + const okNotificationId = RUNTIME_NOTIFICATION_ID_TAG + '-0' + const errNotificationId = RUNTIME_NOTIFICATION_ID_TAG + '-1' + const okAtSecondNotificationId = RUNTIME_NOTIFICATION_ID_TAG + '-2' + let successfulDelivery: NotificationEmailDelivery + let failingDelivery: NotificationEmailDelivery + let successfulAtSecondDelivery: NotificationEmailDelivery + let em: EntityManager + let maxAttempts: number + + before(async () => { + em = await globalEm + maxAttempts = await getMaxAttempts(em) + await populateDbWithSeedData() + }) + + after(async () => { + await clearDb() + }) + + describe('1️⃣ first run of deliverEmails', () => { + before(async () => { + await correctRecipientAccountEmail(em, okNotificationId) + await deliverEmails() + successfulDelivery = await getDeliveryFromNotificationId(em, okNotificationId) + failingDelivery = await getDeliveryFromNotificationId(em, errNotificationId) + successfulAtSecondDelivery = await getDeliveryFromNotificationId(em, okAtSecondNotificationId) + }) + describe('successful delivery case: 👍', () => { + it('should change email delivery to discard when success', async () => { + expect(successfulDelivery.discard).to.be.true + }) + it('should create a successful attempt', async () => { + expect(successfulDelivery.attempts).to.have.lengthOf(1) + expect(successfulDelivery.attempts[0].notificationDeliveryId).to.equal( + successfulDelivery.id + ) + expect(successfulDelivery.attempts[0].status.isTypeOf).to.equal('EmailSuccess') + }) + }) + describe('failing delivery case: 👎', () => { + it('should keep email delivery discard to false', () => { + expect(failingDelivery.discard).to.be.false + }) + it('should create a failed attempt', () => { + expect(failingDelivery.attempts).to.have.lengthOf(1) + expect(failingDelivery.attempts[0].notificationDeliveryId).to.equal(failingDelivery.id) + expect(failingDelivery.attempts[0].status.isTypeOf).to.equal('EmailFailure') + }) + }) + describe('successful at second attempt delivery case: 👎', () => { + it('should keep email delivery discard to false', () => { + expect(successfulAtSecondDelivery.discard).to.be.false + }) + it('should create a failed attempt', () => { + expect(successfulAtSecondDelivery.attempts).to.have.lengthOf(1) + expect(successfulAtSecondDelivery.attempts[0].notificationDeliveryId).to.equal( + successfulAtSecondDelivery.id + ) + expect(successfulAtSecondDelivery.attempts[0].status.isTypeOf).to.equal('EmailFailure') + }) + }) + describe('2️⃣ second run of deliverEmails', () => { + before(async () => { + await correctRecipientAccountEmail(em, okAtSecondNotificationId) + await deliverEmails() + failingDelivery = await getDeliveryFromNotificationId(em, errNotificationId) + successfulAtSecondDelivery = await getDeliveryFromNotificationId( + em, + okAtSecondNotificationId + ) + }) + describe('failing delivery case: 👎', () => { + it('should keep email delivery discard to false', () => { + expect(failingDelivery.discard).to.be.false + }) + it('should create another failed attempt', () => { + expect(failingDelivery.attempts).to.have.lengthOf(2) + expect(failingDelivery.attempts[0].notificationDeliveryId).to.equal(failingDelivery.id) + expect(failingDelivery.attempts[0].status.isTypeOf).to.equal('EmailFailure') + expect(failingDelivery.attempts[1].status.isTypeOf).to.equal('EmailFailure') + }) + }) + describe('successful at second delivery case: 👍', () => { + it('should change email delivery to discard when success', async () => { + expect(successfulAtSecondDelivery.discard).to.be.true + }) + it('should create a new successful attempt', async () => { + expect(successfulAtSecondDelivery.attempts).to.have.lengthOf(2) + expect(successfulAtSecondDelivery.attempts[1].notificationDeliveryId).to.equal( + successfulAtSecondDelivery.id + ) + expect(successfulAtSecondDelivery.attempts[1].status.isTypeOf).to.equal('EmailSuccess') + }) + }) + describe('3️⃣ MAX_ATTEMPTS - 2 runs of deliverEmails', () => { + before(async () => { + const maxAttempts = await getMaxAttempts(em) + expect(maxAttempts).to.be.greaterThan(2) + for (let i = 0; i < maxAttempts - 2; i++) { + await deliverEmails() + } + failingDelivery = await getDeliveryFromNotificationId(em, errNotificationId) + }) + describe('failing delivery case: 👎', () => { + it('should set the email delivery discard to true after MAX_ATTEMPTS', async () => { + expect(failingDelivery.discard).to.be.true + }) + it('should create MAX_ATTEMPTS failed attempts', async () => { + const attemptsStatus = failingDelivery.attempts.map( + (attempt) => attempt.status.isTypeOf + ) + expect(failingDelivery.attempts).to.have.lengthOf(maxAttempts) + expect(attemptsStatus.every((status) => status === 'EmailFailure')).to.be.true + }) + }) + describe('4️⃣ one more run of deliverEmails', () => { + before(async () => { + await deliverEmails() + failingDelivery = await getDeliveryFromNotificationId(em, errNotificationId) + }) + describe('failing delivery case: 👎', () => { + it('should not create more MAX_ATTEMPTS failed attempts', async () => { + const attemptsStatus = failingDelivery.attempts.map( + (attempt) => attempt.status.isTypeOf + ) + expect(failingDelivery.attempts).to.have.lengthOf(maxAttempts) + expect(attemptsStatus.every((status) => status === 'EmailFailure')).to.be.true + }) + }) + }) + }) + }) + }) +}) diff --git a/src/mail-scheduler/tests/seedData.test.bak b/src/mail-scheduler/tests/seedData.test.bak new file mode 100644 index 000000000..9358deb45 --- /dev/null +++ b/src/mail-scheduler/tests/seedData.test.bak @@ -0,0 +1,60 @@ +import { expect } from 'chai' +import { ConfigVariable, config } from '../../utils/config' +import { Account, Notification, NotificationEmailDelivery } from '../../model' +import { globalEm } from '../../utils/globalEm' +import { clearDb, populateDbWithSeedData } from './testUtils' +import { EntityManager } from 'typeorm' +import { idStringFromNumber } from '../../utils/misc' +import { RUNTIME_NOTIFICATION_ID_TAG } from '../../utils/notification/helpers' + +describe('Database seed data tests', () => { + let em: EntityManager + + describe('seed data database population', () => { + // populate the database with seed data + let account: Account + before(async () => { + em = await globalEm + await populateDbWithSeedData() + }) + + // check that seed date exists + it('check that seed data exists', async () => { + account = await em + .getRepository(Account) + .findOneOrFail({ where: { id: idStringFromNumber(1) }, relations: { membership: true } }) + expect(account).to.not.be.null + expect(account?.membership.id).to.equal('1') + expect(account?.membership.handle).to.equal('handle-1') + }) + it('check that notification delivery entity is correct', async () => { + const result = await em.getRepository(NotificationEmailDelivery).findOne({ + where: { notification: { id: RUNTIME_NOTIFICATION_ID_TAG + '-1' } }, + relations: { notification: { account: true } }, + }) + + expect(result).to.not.be.null + expect(result?.notification.account).to.not.be.null + }) + it('check that max attempt config variable is set', async () => { + const result = await config.get(ConfigVariable.EmailNotificationDeliveryMaxAttempts, em) + + expect(result).to.not.be.undefined + }) + }) + + describe('database cleanup', () => { + before(async () => { + await clearDb() + }) + it('should clear the database', async () => { + const accounts = await em.getRepository(Account).find({}) + const notifications = await em.getRepository(Notification).find({}) + const deliveries = await em.getRepository(NotificationEmailDelivery).find({}) + + expect(accounts).to.be.empty + expect(notifications).to.be.empty + expect(deliveries).to.be.empty + }) + }) +}) diff --git a/src/mail-scheduler/tests/testUtils.ts b/src/mail-scheduler/tests/testUtils.ts new file mode 100644 index 000000000..09e644178 --- /dev/null +++ b/src/mail-scheduler/tests/testUtils.ts @@ -0,0 +1,88 @@ +import { + Membership, + User, + Notification, + Account, + Unread, + MemberRecipient, + NotificationEmailDelivery, + GatewayConfig, + AuctionWon, + EmailDeliveryAttempt, + AuctionTypeOpen, +} from '../../model' +import { defaultNotificationPreferences } from '../../utils/notification' +import { globalEm } from '../../utils/globalEm' +import { idStringFromNumber } from '../../utils/misc' +import { RUNTIME_NOTIFICATION_ID_TAG } from '../../utils/notification/helpers' +import { uniqueId } from '../../utils/crypto' + +export const NUM_ENTITIES = 3 + +export async function populateDbWithSeedData() { + const em = await globalEm + for (let i = 0; i < NUM_ENTITIES; i++) { + // create memberships + const member: Membership = await em.getRepository(Membership).save({ + id: i.toString(), + createdAt: new Date(), + metadata: { + id: `metadat-id-${i}`, + }, + bannedFromChannels: [], + totalChannelsCreated: 0, + handle: `handle-${i}`, + controllerAccount: `j4${i}7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf`, + channels: [], + }) + // create users + const user = await em.getRepository(User).save({ + isRoot: false, + id: i.toString(), + }) + // create accounts + const account = await em.getRepository(Account).save({ + id: idStringFromNumber(i), + userId: user.id, + email: `incorrect${i}@example.com`, + isEmailConfirmed: false, + isBlocked: false, + registeredAt: member.createdAt, + membershipId: member.id, + joystreamAccount: member.controllerAccount, + notificationPreferences: defaultNotificationPreferences(), + }) + // create notifications + const notification = await em.getRepository(Notification).save({ + id: RUNTIME_NOTIFICATION_ID_TAG + '-' + i.toString(), + accountId: account.id, + status: new Unread(), + createdAt: new Date(), + recipient: new MemberRecipient({ membership: member.id }), + notificationType: new AuctionWon({ + type: new AuctionTypeOpen({ bidLockDuration: 10 }), + videoId: uniqueId(), + videoTitle: 'test', + }), + inApp: true, + }) + + await em.getRepository(NotificationEmailDelivery).save({ + id: uniqueId(), + notificationId: notification.id, + attempts: [], + discard: false, + }) + } +} + +export async function clearDb(): Promise { + const em = await globalEm + await em.getRepository(EmailDeliveryAttempt).delete({}) + await em.getRepository(NotificationEmailDelivery).delete({}) + await em.getRepository(Notification).delete({}) + await em.getRepository(Account).delete({}) + await em.getRepository(User).delete({}) + await em.getRepository(Membership).delete({}) + await em.getRepository(GatewayConfig).delete({}) +} diff --git a/src/mail-scheduler/utils.ts b/src/mail-scheduler/utils.ts new file mode 100644 index 000000000..f1dede0d6 --- /dev/null +++ b/src/mail-scheduler/utils.ts @@ -0,0 +1,146 @@ +import { EntityManager } from 'typeorm' +import { + Account, + Channel, + DeliveryStatus, + EmailFailure, + EmailSuccess, + Membership, + Notification, +} from '../model' +import { ConfigVariable, config } from '../utils/config' +import sgMail, { ClientResponse, ResponseError } from '@sendgrid/mail' +import { getNotificationData } from '../utils/notification/notificationsData' +import { notificationEmailContent, NotificationEmailTemplateData } from '../auth-server/emails' + +export const DEFAULT_STATUS_CODE = 'Undefined error code' + +export async function executeMailDelivery( + appName: string, + em: EntityManager, + toAccount: Account, + content: string +): Promise { + const resp = await sendGridSend({ + from: await config.get(ConfigVariable.SendgridFromEmail, em), + to: toAccount.email, + subject: `New notification from ${appName}!`, + content, + }) + const className = Object.prototype.toString.call(resp) + if (className === '[object Error]') { + return new EmailFailure({ + errorStatus: (resp as ResponseError).code?.toString() || DEFAULT_STATUS_CODE, + }) + } else { + return new EmailSuccess({}) + } +} + +export async function createMailContent( + em: EntityManager, + appName: string, + notification: Notification +): Promise { + const appRoot = `https://${await config.get(ConfigVariable.AppRootDomain, em)}` + + const appKey = notification.recipient.isTypeOf === 'MemberRecipient' ? 'viewer' : 'studio' + const notificationLink = + appKey === 'viewer' + ? `${appRoot}/notifications/member` + : `${appRoot}/studio/notifications/channel` + const unsubscribeLink = + appKey === 'viewer' + ? `${appRoot}/member/settings?tab=Notifications` + : `${appRoot}/studio/channel?tab=Notifications` + const name = appKey === 'viewer' ? appName : 'Studio' + + // TODO get these from the store: + const appAssetStorage = `https://raw.githubusercontent.com/Joystream/atlas-notification-assets/main/logos/gleev` + const appNameAlt = 'Gleev.xyz' + + const content = notificationEmailContent({ + ...(await getMessage(em, notification)), + app: { + name, + nameAlt: appNameAlt, + logo: `${appAssetStorage}/header-${appKey}.png`, + logoAlt: `${appAssetStorage}/footer.png`, + homeLink: appRoot, + notificationLink, + unsubscribeLink, + }, + notification: await getNotificationData(em, notification), + }) + return content +} + +async function getMessage( + em: EntityManager, + { recipient }: Notification +): Promise> { + switch (recipient.isTypeOf) { + case 'MemberRecipient': { + const member = await em.getRepository(Membership).findOneBy({ id: recipient.membership }) + return { + title: `Hi ${member?.handle ?? ''}`, + subTitle: 'You have a new notifications:', + } + } + case 'ChannelRecipient': { + const channel = await em.getRepository(Channel).findOneBy({ id: recipient.channel }) + return { + title: `Your channel “${channel?.title}”,`, + subTitle: 'Has a new notifications:', + } + } + } +} + +type SendMailArgs = { + from: string + to: string + subject: string + content: string +} + +type SendGridResponse = ClientResponse | ResponseError | Error + +export async function sendGridSend({ + from, + to, + subject, + content, +}: SendMailArgs): Promise { + const apiKey = process.env.SENDGRID_API_KEY + if (process.env.TESTING === 'true' || process.env.TESTING === '1') { + return mockSend(to) + } + if (apiKey) { + sgMail.setApiKey(apiKey) + } + + try { + const [sendGridSuccess] = await sgMail.send({ + from, + to, + subject, + html: content, + }) + return sendGridSuccess + } catch (sendGrideFailure) { + return sendGrideFailure as SendGridResponse + } +} + +const mockSend = (to: string): SendGridResponse => { + if (to.match(/incorrect/gi)) { + return new Error('Test error') + } else { + return { + statusCode: 202, + headers: {}, + body: {}, + } + } +} diff --git a/src/server-extension/resolvers/AdminResolver/index.ts b/src/server-extension/resolvers/AdminResolver/index.ts index 48883bb58..c602e0a86 100644 --- a/src/server-extension/resolvers/AdminResolver/index.ts +++ b/src/server-extension/resolvers/AdminResolver/index.ts @@ -1,5 +1,5 @@ import 'reflect-metadata' -import { Args, Query, Mutation, Resolver, UseMiddleware, Info, Ctx } from 'type-graphql' +import { Args, Query, Mutation, Resolver, UseMiddleware, Info, Ctx, Int } from 'type-graphql' import { EntityManager, In, Not } from 'typeorm' import { AppActionSignatureInput, @@ -9,7 +9,7 @@ import { ExcludeContentResult, GeneratedSignature, KillSwitch, - NotificationCenterPath, + MaxAttemptsOnMailDelivery, RestoreContentArgs, RestoreContentResult, SetCategoryFeaturedVideosArgs, @@ -17,7 +17,7 @@ import { SetFeaturedNftsInput, SetFeaturedNftsResult, SetKillSwitchInput, - SetNotificationCenterPathInput, + SetMaxAttemptsOnMailDeliveryInput, SetRootDomainInput, SetSupportedCategoriesInput, SetSupportedCategoriesResult, @@ -82,13 +82,16 @@ export class AdminResolver { } @UseMiddleware(OperatorOnly) - @Mutation(() => NotificationCenterPath) + @Mutation(() => Int) async setNewNotificationCenterPath( - @Args() args: SetNotificationCenterPathInput - ): Promise { + @Args() args: SetMaxAttemptsOnMailDeliveryInput + ): Promise { const em = await this.em() - await config.set(ConfigVariable.NotificationCenterPath, args.newPath, em) - return { isApplied: true } + if (args.newMaxAttempts < 1) { + throw new Error('Max attempts cannot be less than 1') + } + await config.set(ConfigVariable.EmailNotificationDeliveryMaxAttempts, args.newMaxAttempts, em) + return { maxAttempts: args.newMaxAttempts } } @UseMiddleware(OperatorOnly) diff --git a/src/server-extension/resolvers/AdminResolver/types.ts b/src/server-extension/resolvers/AdminResolver/types.ts index d7cc7bac5..6ead7bc81 100644 --- a/src/server-extension/resolvers/AdminResolver/types.ts +++ b/src/server-extension/resolvers/AdminResolver/types.ts @@ -23,15 +23,15 @@ export class SetVideoWeightsInput { } @ArgsType() -export class SetNotificationCenterPathInput { - @Field(() => String, { nullable: false }) - newPath!: string +export class SetMaxAttemptsOnMailDeliveryInput { + @Field(() => Int, { nullable: false }) + newMaxAttempts!: number } @ObjectType() -export class NotificationCenterPath { - @Field(() => Boolean, { nullable: false }) - isApplied!: boolean +export class MaxAttemptsOnMailDelivery { + @Field(() => Int, { nullable: false }) + maxAttempts!: number } @ArgsType() diff --git a/src/utils/config.ts b/src/utils/config.ts index e12f2d3f4..ee92c4cfe 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -11,7 +11,6 @@ export enum ConfigVariable { RelevanceWeights = 'RELEVANCE_WEIGHTS', AppPrivateKey = 'APP_PRIVATE_KEY', AppRootDomain = 'APP_ROOT_DOMAIN', - NotificationCenterPath = 'NOTIFICATION_CENTER_PATH', SessionExpiryAfterInactivityMinutes = 'SESSION_EXPIRY_AFTER_INACTIVITY_MINUTES', SessionMaxDurationHours = 'SESSION_MAX_DURATION_HOURS', SendgridApiKey = 'SENDGRID_API_KEY', @@ -21,6 +20,7 @@ export enum ConfigVariable { EmailConfirmationTokenExpiryTimeHours = 'EMAIL_CONFIRMATION_TOKEN_EXPIRY_TIME_HOURS', EmailConfirmationTokenRateLimit = 'EMAIL_CONFIRMATION_TOKEN_RATE_LIMIT', AccountOwnershipProofExpiryTimeSeconds = 'ACCOUNT_OWNERSHIP_PROOF_EXPIRY_TIME_SECONDS', + EmailNotificationDeliveryMaxAttempts = 'EMAIL_NOTIFICATION_DELIVERY_MAX_ATTEMPTS', } const boolType = { @@ -61,7 +61,7 @@ export const configVariables = { [ConfigVariable.AccountOwnershipProofExpiryTimeSeconds]: intType, [ConfigVariable.EmailConfirmationTokenRateLimit]: intType, [ConfigVariable.AppRootDomain]: stringType, - [ConfigVariable.NotificationCenterPath]: stringType, + [ConfigVariable.EmailNotificationDeliveryMaxAttempts]: intType, } as const type TypeOf = ReturnType diff --git a/src/utils/mail.ts b/src/utils/mail.ts index b7c8ab7d9..6189692f2 100644 --- a/src/utils/mail.ts +++ b/src/utils/mail.ts @@ -30,7 +30,7 @@ export async function sgSendMail({ from, to, subject, content }: SendMailArgs) { subject, html: content, }) - mailerLogger.info(`E-mail sent:\n${JSON.stringify({ from, to, subject, content }, null, 2)}`) + // mailerLogger.info(`E-mail sent:\n${JSON.stringify({ from, to, subject, content }, null, 2)}`) return clientResponse } diff --git a/src/utils/notification/helpers.ts b/src/utils/notification/helpers.ts index 105e66be9..10228e600 100644 --- a/src/utils/notification/helpers.ts +++ b/src/utils/notification/helpers.ts @@ -8,7 +8,6 @@ import { Event, Unread, NotificationEmailDelivery, - EmailDeliveryStatus, RecipientType, NotificationType, } from '../../model' @@ -194,7 +193,8 @@ async function createEmailNotification( const notificationDelivery = new NotificationEmailDelivery({ id: uniqueId(32), notificationId: notification.id, - deliveryStatus: EmailDeliveryStatus.Unsent, + attempts: [], + discard: false, }) if (store instanceof EntityManagerOverlay) { diff --git a/src/utils/notification/notificationAvatars.ts b/src/utils/notification/notificationAvatars.ts new file mode 100644 index 000000000..d3d44925f --- /dev/null +++ b/src/utils/notification/notificationAvatars.ts @@ -0,0 +1,34 @@ +import { EntityManager, FindOptionsWhere } from 'typeorm' +import { Channel, MemberMetadata } from '../../model' + +const PLACEHOLDER = 'https://example.com/avatar.png' + +export const getNotificationAvatar = async ( + em: EntityManager, + type: 'channelId' | 'membershipId' | 'membershipHandle', + param: string +): Promise => { + if (type === 'channelId') { + const channel = await em.getRepository(Channel).findOneBy({ id: param }) + const avatar = channel?.avatarPhoto + + if (!avatar || !avatar.isAccepted || !avatar.resolvedUrls[0]) { + return PLACEHOLDER + } + + return avatar.resolvedUrls[0] + } + + const where: FindOptionsWhere = + type === 'membershipId' ? { id: param } : { member: { handle: param } } + + const member = await em.getRepository(MemberMetadata).findOneBy(where) + const avatar = member?.avatar + + // AvatarObject is not yet supported + if (!avatar || avatar.isTypeOf === 'AvatarObject') { + return PLACEHOLDER + } + + return avatar.avatarUri +} diff --git a/src/utils/notification/notificationIcons.ts b/src/utils/notification/notificationIcons.ts new file mode 100644 index 000000000..42db1b54e --- /dev/null +++ b/src/utils/notification/notificationIcons.ts @@ -0,0 +1,21 @@ +import { EntityManager } from 'typeorm' + +type NotificationIconType = + | 'like' + | 'dislike' + | 'follow' + | 'warning' + | 'bell' + | 'nft' + | 'nft-alt' + | 'payout' + | 'reaction' + | 'video' + +// TODO get the notifiaction url from `await config.get(ConfigVariable.NotificationAssetRoot, em)` +const NOTIFICATION_ASSET_ROOT = + 'https://raw.githubusercontent.com/Joystream/atlas-notification-assets/main/icons' +export const getNotificationIcon = async ( + em: EntityManager, + icon: NotificationIconType +): Promise => `${NOTIFICATION_ASSET_ROOT}/${icon}.png` diff --git a/src/utils/notification/notificationLinks.ts b/src/utils/notification/notificationLinks.ts index e78c22b7c..be2b13bbb 100644 --- a/src/utils/notification/notificationLinks.ts +++ b/src/utils/notification/notificationLinks.ts @@ -1,170 +1,66 @@ -// TODO: uncomment and refactor for scheaduler feature -// import { CreatorNotificationData, MemberNotificationData } from '../../model' -// import { ConfigVariable, config } from '../config' -// import { EntityManager } from 'typeorm' - -// // expected like "gleev.xyz" -// const getRootDomain = async (em: EntityManager) => config.get(ConfigVariable.AppRootDomain, em) - -// export const channelExcludedLink = async (em: EntityManager) => -// `https://${await getRootDomain(em)}/legal/tos` - -// export const videoExcludedLink = async (em: EntityManager) => -// `https://${await getRootDomain(em)}/legal/tos` - -// export const videoFeaturedOnCategoryPageLink = async (em: EntityManager, categoryId: string) => -// `https://${await getRootDomain(em)}/category/${categoryId}` - -// export const videoFeaturedAsCategoryHeroLink = async (em: EntityManager, categoryId: string) => -// `https://${await getRootDomain(em)}/category/${categoryId}` - -// export const newChannelFollowerLink = async (em: EntityManager, memberHandle: string) => -// `https://${await getRootDomain(em)}/${memberHandle}` - -// // FIXME: edit link such that the page is focused to comment -// export const commentPostedToVideoLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const videoLikedLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const videoDislikedLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const channelVerifiedLink = async (em: EntityManager) => -// `https://${await getRootDomain(em)}/ypp` - -// export const yppSignupSuccessfulLink = async (em: EntityManager) => -// `https://${await getRootDomain(em)}/ypp` - -// export const channelSuspendedLink = async (em: EntityManager) => -// `https://${await getRootDomain(em)}/ypp` - -// export const nftSoldLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const royaltiesReceivedLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const directPaymentByMemberLink = async (em: EntityManager, memberHandle: string) => -// `https://${await getRootDomain(em)}/member/${memberHandle}` - -// // design specifies no-redirect -// export const channelFundsWithdrawnLink = () => '' - -// // FIXME: edit link such that the page is focused to comment -// export const commentReplyLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const commentReactionLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const newVideoPostedLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const nftOnSaleLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const nftOnAuctionLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const auctionBidMadeLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const higherBidPlacedLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const timedAuctionWonLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const openAuctionWonLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const timedAuctionExpiredLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const bidMadeCompletingAuctionLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const timedAuctionLostLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const openAuctionLostLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export const channelCreatedLink = async (em: EntityManager, channelId: string) => -// `https://${await getRootDomain(em)}/channel/${channelId}` - -// export const nftFeaturedOnMarketplaceLink = async (em: EntityManager, videoId: string) => -// `https://${await getRootDomain(em)}/video/${videoId}` - -// export async function linkForNotification( -// em: EntityManager, -// notificationType: CreatorNotificationData | MemberNotificationData -// ): Promise { -// // create a map of notification type to link -// switch (notificationType.isTypeOf) { -// case 'ChannelExcluded': -// return await channelExcludedLink(em) -// case 'VideoExcluded': -// return await videoExcludedLink(em) -// case 'VideoFeaturedOnCategoryPage': -// return await videoFeaturedOnCategoryPageLink(em, notificationType.categoryId) -// case 'VideoFeaturedAsCategoryHero': -// return await videoFeaturedOnCategoryPageLink(em, notificationType.categoryName) -// case 'NewChannelFollower': -// return await newChannelFollowerLink(em, notificationType.followerHandle) -// case 'CommentPostedToVideo': -// return await commentPostedToVideoLink(em, notificationType.videoId) -// case 'VideoLiked': -// return await videoLikedLink(em, notificationType.videoId) -// case 'VideoDisliked': -// return await videoDislikedLink(em, notificationType.videoId) -// case 'ChannelVerified': -// return await channelVerifiedLink(em) -// case 'ChannelSuspended': -// return await channelSuspendedLink(em) -// case 'BidMadeCompletingAuction': -// return await bidMadeCompletingAuctionLink(em, notificationType.videoId) -// case 'ChannelCreated': -// return await channelCreatedLink(em, notificationType.channelId) -// case 'ChannelFundsWithdrawn': -// return channelFundsWithdrawnLink() -// case 'CommentReply': -// return await commentReplyLink(em, notificationType.videoId) -// case 'ReactionToComment': -// return await commentReactionLink(em, notificationType.videoId) -// case 'CreatorReceivesAuctionBid': -// return await auctionBidMadeLink(em, notificationType.videoId) -// case 'HigherBidPlaced': -// return await higherBidPlacedLink(em, notificationType.videoId) -// case 'DirectChannelPaymentByMember': -// return await directPaymentByMemberLink(em, notificationType.payerHandle) -// case 'EnglishAuctionLost': -// return await timedAuctionLostLink(em, notificationType.videoId) -// case 'EnglishAuctionWon': -// return await timedAuctionWonLink(em, notificationType.videoId) -// case 'EnglishAuctionSettled': -// return await timedAuctionExpiredLink(em, notificationType.videoId) -// case 'OpenAuctionLost': -// return await openAuctionLostLink(em, notificationType.videoId) -// case 'OpenAuctionWon': -// return await openAuctionWonLink(em, notificationType.videoId) -// case 'NewAuction': -// return await nftOnAuctionLink(em, notificationType.videoId) -// case 'NewAuctionBid': -// return await auctionBidMadeLink(em, notificationType.videoId) -// case 'NewNftOnSale': -// return await nftOnSaleLink(em, notificationType.videoId) -// case 'NftFeaturedOnMarketPlace': -// return await nftFeaturedOnMarketplaceLink(em, notificationType.videoId) -// case 'NftPurchased': -// return await nftSoldLink(em, notificationType.videoId) -// case 'NftRoyaltyPaid': -// return await royaltiesReceivedLink(em, notificationType.videoId) -// case 'VideoPosted': -// return await newVideoPostedLink(em, notificationType.videoId) -// default: -// return '' -// } -// } +import { ConfigVariable, config } from '../config' +import { EntityManager } from 'typeorm' +import { join } from 'path' + +type LinkType = + | 'video-page' + | 'nft-page' + | 'channel-page' + | 'term-of-sevice-page' + | 'category-page' + | 'marketplace-page' + | 'member-page' + | 'ypp-dashboard' + | 'payments-page' + +export const getNotificationLink = async ( + em: EntityManager, + type: LinkType, + params: string[] = [] +): Promise => { + switch (type) { + case 'video-page': + return await getLink(em, `video/${params[0]}`, { commentId: params[1] }) + + case 'nft-page': + return await getLink(em, `video/${params[0]}`, { nftWidget: true }) + + case 'channel-page': + return await getLink(em, `channel/${params[0]}`) + + case 'member-page': + return await getLink(em, `member/${params[0]}`) + + case 'category-page': + return await getLink(em, `category/${params[0]}`) + + case 'marketplace-page': + return await getLink(em, 'marketplace') + + case 'payments-page': + return await getLink(em, 'payments') + + case 'ypp-dashboard': + return await getLink(em, 'ypp/dashboard') + + case 'term-of-sevice-page': + return await getLink(em, 'tos') + } +} + +const getLink = async ( + em: EntityManager, + pathname: string, + query?: Record +) => { + const domain = await config.get(ConfigVariable.AppRootDomain, em) // expected like "gleev.xyz" + const basePath = `https://${join(domain, pathname)}` + + if (!query) return basePath + + const queryParams = new URLSearchParams() + Object.entries(query).forEach(([key, value]) => { + if (typeof value !== 'undefined') queryParams.set(key, String(value)) + }) + return `${basePath}?${queryParams.toString()}` +} diff --git a/src/utils/notification/notificationTexts.ts b/src/utils/notification/notificationTexts.ts deleted file mode 100644 index 27ff9e099..000000000 --- a/src/utils/notification/notificationTexts.ts +++ /dev/null @@ -1,280 +0,0 @@ -// TODO: uncomment and refactor for scheduler featurej -// import { EntityManager } from 'typeorm' -// import { -// Channel, -// Video, -// Membership, -// ChannelExcluded, -// VideoExcluded, -// VideoFeaturedOnCategoryPage, -// VideoFeaturedAsCategoryHero, -// NewChannelFollower, -// CommentPostedToVideo, -// VideoLiked, -// VideoDisliked, -// BidMadeCompletingAuction, -// ChannelCreated, -// ChannelFundsWithdrawn, -// CommentReply, -// ReactionToComment, -// CreatorReceivesAuctionBid, -// HigherBidPlaced, -// DirectChannelPaymentByMember, -// EnglishAuctionLost, -// EnglishAuctionWon, -// EnglishAuctionSettled, -// OpenAuctionLost, -// OpenAuctionWon, -// NewAuction, -// } from '../../model' -// import { CreatorRecipientParams, MemberRecipientParams } from './helpers' - -// export const channelExcludedText = (channelTitle: string) => { -// return `Your channel ${channelTitle} has been excluded` -// } -// export const videoExcludedText = (videoTitle: string) => { -// return `Your video ${videoTitle} has been excluded` -// } - -// export const nftOfferedText = (videoTitle: string, price: string) => { -// return `Nft for ${videoTitle} has been offered to you for ${price}` -// } - -// export const videoFeaturedAsHeroText = (videoTitle: string) => { -// return `Your video ${videoTitle} has been featured as Hero` -// } - -// export const videoFeaturedOnCategoryPageText = (videoTitle: string, categoryTitle: string) => { -// return `Your video ${videoTitle} has been featured on the ${categoryTitle} category page` -// } - -// export const nftFeaturedOnMarketplaceText = (videoTitle: string) => { -// return `Your nft for ${videoTitle} has been featured on the marketplace` -// } - -// export const newChannelFollowerText = (channelTitle: string) => { -// return `You have a new follower on channel ${channelTitle}` -// } - -// export const commentPostedToVideoText = (videoTitle: string, memberHandle: string) => { -// return `${memberHandle} left a comment on Your video ${videoTitle}` -// } - -// export const videoLikedText = (videoTitle: string) => { -// return `Your video ${videoTitle} has a new like` -// } - -// export const videoDislikedText = (videoTitle: string) => { -// return `Your video ${videoTitle} has a new dislike` -// } - -// export const channelVerifiedViaYPPText = () => { -// return `Your channel has been verified via YPP` -// } - -// export const channelSuspendedViaYPPText = () => { -// return `Your channel has been suspended via YPP` -// } - -// export const nftPurchasedText = (videoTitle: string, memberHandle: string, nftPrice: string) => { -// return `Your NFT for ${videoTitle} has been purchased by ${memberHandle} for ${nftPrice} JOY` -// } - -// export const nftBidReceivedText = (videoTitle: string, memberHandle: string, nftPrice: string) => { -// return `${memberHandle} placed a bid of ${nftPrice} JOY on nft: ${videoTitle}` -// } - -// export const nftRoyaltyPaymentReceivedText = (videoTitle: string, royalties: string) => { -// return `you received ${royalties} JOY nominal royalties for your nft: ${videoTitle}` -// } - -// export const channelReceivedDirectPaymentText = (memberHandle: string, amount: string) => { -// return `${memberHandle} transferred ${amount} JOY to your channel` -// } - -// export const timedAuctionExpiredText = (videoTitle: string) => { -// return `Timed auction expired for your nft: ${videoTitle}` -// } - -// export const openAuctionExpiredText = (videoTitle: string) => { -// return `Open auction settled for your nft: ${videoTitle}` -// } - -// export const channelCreatedText = (channelTitle: string) => { -// return `${channelTitle} has been created` -// } - -// export const commentRepliedText = (videoTitle: string, memberHandle: string) => { -// return `${memberHandle} has replied to your comment under ${videoTitle}` -// } - -// export const commentReactedText = (memberHandle: string, videoTitle: string) => { -// return `${memberHandle} has reacted to your comment under ${videoTitle}` -// } - -// export const newVideoPostedText = (channelTitle: string, videoTitle: string) => { -// return `${channelTitle} just posted a new video ${videoTitle}` -// } - -// export const newNftOnAuctionText = (channelTitle: string, videoTitle: string) => { -// return `${channelTitle} just started an auction of nft: ${videoTitle}` -// } - -// export const newNftOnSaleText = (channelTitle: string, videoTitle: string) => { -// return `${channelTitle} just started the sale of nft ${videoTitle}` -// } - -// export const nftBidOutbidText = (videoTitle: string, memberHandle: string) => { -// return `${memberHandle} placed a higher bid on NFT ${videoTitle}` -// } - -// export const openAuctionBidWonText = (videoTitle: string) => { -// return `You won an open auction for nft: ${videoTitle}` -// } - -// export const timedAuctionBidWonText = (videoTitle: string) => { -// return `You won a timed auction for nft: ${videoTitle}` -// } - -// export const openAuctionBidLostText = (videoTitle: string) => { -// return `You lost an open auction for nft: ${videoTitle}` -// } - -// export const timedAuctionBidLostText = (videoTitle: string) => { -// return `You lost an timed auction for nft: ${videoTitle}` -// } - -// export const bidMadeCompletingAuctionText = ( -// videoTitle: string, -// memberHandle: string, -// price: string -// ) => { -// return `Member ${memberHandle} won auction for nft: ${videoTitle} by a buy now bid of ${price} JOY` -// } - -// export const fundsWithdrawnFromChannelText = (amount: string) => { -// return `Sucessfully transferred ${amount} JOY from your channel` -// } - -// export async function getChannelTitle(em: EntityManager, channelId: string): Promise { -// const channel = await em.getRepository(Channel).findOneOrFail({ where: { id: channelId } }) -// return channel.title || '' -// } - -// export async function getVideoTitle(em: EntityManager, videoId: string): Promise { -// const video = await em.getRepository(Video).findOneOrFail({ where: { id: videoId } }) -// return video.title || '' -// } - -// export async function getCategoryNameFromVideo( -// em: EntityManager, -// videoId: string -// ): Promise { -// const result = await em -// .getRepository(Video) -// .findOneOrFail({ where: { id: videoId }, relations: { category: true } }) -// return result.category?.name || '' -// } - -// export async function getMemberHandle(em: EntityManager, memberId: string): Promise { -// const member = await em.getRepository(Membership).findOneOrFail({ where: { id: memberId } }) -// return member.handle || '' -// } - -// export async function textForNotification( -// param: CreatorRecipientParams | MemberRecipientParams -// ): Promise { -// switch (param.data.isTypeOf) { -// case 'ChannelExcluded': -// return channelExcludedText((param.data as ChannelExcluded).channelTitle) -// case 'VideoExcluded': -// return videoExcludedText((param.data as VideoExcluded).videoTitle) -// case 'VideoFeaturedOnCategoryPage': -// return videoFeaturedOnCategoryPageText( -// (param.data as VideoFeaturedOnCategoryPage).videoTitle, -// (param.data as VideoFeaturedOnCategoryPage).categoryName -// ) -// case 'VideoFeaturedAsCategoryHero': -// return videoFeaturedAsHeroText((param.data as VideoFeaturedAsCategoryHero).videoTitle) -// case 'NewChannelFollower': -// return newChannelFollowerText((param.data as NewChannelFollower).channelTitle) -// case 'CommentPostedToVideo': -// return commentPostedToVideoText( -// (param.data as CommentPostedToVideo).videoTitle, -// (param.data as CommentPostedToVideo).memberHandle -// ) -// case 'VideoLiked': -// return videoLikedText((param.data as VideoLiked).videoTitle) -// case 'VideoDisliked': -// return videoLikedText((param.data as VideoDisliked).videoTitle) -// case 'ChannelVerified': -// return channelVerifiedViaYPPText() -// case 'ChannelSuspended': -// return channelSuspendedViaYPPText() -// case 'BidMadeCompletingAuction': -// return bidMadeCompletingAuctionText( -// (param.data as BidMadeCompletingAuction).videoTitle, -// (param.data as BidMadeCompletingAuction).bidderHandle, -// (param.data as BidMadeCompletingAuction).amount.toString() -// ) -// case 'ChannelCreated': -// return channelCreatedText((param.data as ChannelCreated).channelTitle) -// case 'ChannelFundsWithdrawn': -// return fundsWithdrawnFromChannelText((param.data as ChannelFundsWithdrawn).amount.toString()) -// case 'CommentReply': -// return commentRepliedText( -// (param.data as CommentReply).videoTitle, -// (param.data as CommentReply).memberHandle -// ) -// case 'ReactionToComment': -// return commentReactedText( -// (param.data as ReactionToComment).videoTitle, -// (param.data as ReactionToComment).memberHandle -// ) -// case 'CreatorReceivesAuctionBid': -// return nftBidReceivedText( -// (param.data as CreatorReceivesAuctionBid).videoTitle, -// (param.data as CreatorReceivesAuctionBid).bidderHandle, -// (param.data as CreatorReceivesAuctionBid).amount.toString() -// ) -// case 'HigherBidPlaced': -// return nftBidOutbidText( -// (param.data as HigherBidPlaced).videoTitle, -// (param.data as HigherBidPlaced).newBidderHandle -// ) -// case 'DirectChannelPaymentByMember': -// return channelReceivedDirectPaymentText( -// (param.data as DirectChannelPaymentByMember).channelTitle, -// (param.data as DirectChannelPaymentByMember).amount.toString() -// ) -// case 'EnglishAuctionLost': -// return timedAuctionBidLostText((param.data as EnglishAuctionLost).videoTitle) -// case 'EnglishAuctionWon': -// return timedAuctionBidWonText((param.data as EnglishAuctionWon).videoTitle) -// case 'EnglishAuctionSettled': -// return timedAuctionExpiredText((param.data as EnglishAuctionSettled).videoTitle) -// case 'OpenAuctionLost': -// return openAuctionBidLostText((param.data as OpenAuctionLost).videoTitle) -// case 'OpenAuctionWon': -// return openAuctionBidWonText((param.data as OpenAuctionWon).videoTitle) -// case 'NewAuction': -// return newNftOnAuctionText( -// (param.data as NewAuction).channelTitle, -// (param.data as NewAuction).videoTitle -// ) -// case 'NewAuctionBid': -// return nftBidReceivedText(param.videoTitle, param.bidderHandle, param.amount.toString()) -// case 'NewNftOnSale': -// return newNftOnSaleText(param.channelTitle, param.videoTitle) -// case 'NftFeaturedOnMarketPlace': -// return nftFeaturedOnMarketplaceText(param.videoTitle) -// case 'NftPurchased': -// return nftPurchasedText(param.videoTitle, param.buyerHandle, param.price.toString()) -// case 'NftRoyaltyPaid': -// return nftRoyaltyPaymentReceivedText(param.videoTitle, param.amount.toString()) -// case 'VideoPosted': -// return newVideoPostedText(param.channelTitle, param.videoTitle) -// default: -// return '' -// } -// } diff --git a/src/utils/notification/notificationsData.ts b/src/utils/notification/notificationsData.ts new file mode 100644 index 000000000..4c2bb4cce --- /dev/null +++ b/src/utils/notification/notificationsData.ts @@ -0,0 +1,274 @@ +import { EntityManager } from 'typeorm' +import { Channel, Notification } from '../../model' +import { getNotificationAvatar } from './notificationAvatars' +import { getNotificationIcon } from './notificationIcons' +import { getNotificationLink } from './notificationLinks' + +export type NotificationData = { + icon: string + link: string + avatar: string + text: string +} + +export const getNotificationData = async ( + em: EntityManager, + { notificationType, recipient }: Notification +): Promise => { + const recipientId = + recipient.isTypeOf === 'MemberRecipient' ? recipient.membership : recipient.channel + + switch (notificationType.isTypeOf) { + // + // Member notifications events + // + + // Generic + case 'ChannelCreated': { + const { channelId, channelTitle } = notificationType + return { + icon: await getNotificationIcon(em, 'bell'), + link: await getNotificationLink(em, 'channel-page', [channelId]), + avatar: await getNotificationAvatar(em, 'channelId', channelId), + text: `New channel created: “${channelTitle}“`, + } + } + + // Engagement + case 'CommentReply': { + const { videoId, videoTitle, commentId, memberHandle } = notificationType + return { + icon: await getNotificationIcon(em, 'follow'), + link: await getNotificationLink(em, 'video-page', [videoId, commentId]), + avatar: await getNotificationAvatar(em, 'membershipHandle', memberHandle), + text: `${memberHandle} replied to your comment under the video: “${videoTitle}”`, + } + } + case 'ReactionToComment': { + const { videoId, videoTitle, commentId, memberHandle } = notificationType + return { + icon: await getNotificationIcon(em, 'reaction'), + link: await getNotificationLink(em, 'video-page', [videoId, commentId]), + avatar: await getNotificationAvatar(em, 'membershipHandle', memberHandle), + text: `${memberHandle} reacted to your comment on the video: “${videoTitle}”`, + } + } + + // Followed channels + case 'VideoPosted': { + const { videoId, videoTitle, channelTitle, channelId } = notificationType + return { + icon: await getNotificationIcon(em, 'video'), + link: await getNotificationLink(em, 'video-page', [videoId]), + avatar: await getNotificationAvatar(em, 'channelId', channelId), + text: `${channelTitle} posted a new video: “${videoTitle}”`, + } + } + case 'NewNftOnSale': { + const { videoId, videoTitle, channelTitle, channelId } = notificationType + return { + icon: await getNotificationIcon(em, 'nft'), + link: await getNotificationLink(em, 'nft-page', [videoId]), + avatar: await getNotificationAvatar(em, 'channelId', channelId), + text: `${channelTitle} started the sale of NFT: “${videoTitle}”`, + } + } + case 'NewAuction': { + const { videoId, videoTitle, channelTitle, channelId } = notificationType + return { + icon: await getNotificationIcon(em, 'nft'), + link: await getNotificationLink(em, 'nft-page', [videoId]), + avatar: await getNotificationAvatar(em, 'channelId', channelId), + text: `${channelTitle} started an auction for NFT: “${videoTitle}”`, + } + } + + // NFT + case 'HigherBidPlaced': { + const { videoId, videoTitle, newBidderHandle } = notificationType + return { + icon: await getNotificationIcon(em, 'nft-alt'), + link: await getNotificationLink(em, 'nft-page', [videoId]), + avatar: await getNotificationAvatar(em, 'membershipHandle', newBidderHandle), + text: `${newBidderHandle} placed a higher bid in the auction for NFT: “${videoTitle}”`, + } + } + case 'AuctionWon': { + const { videoId, videoTitle, type } = notificationType + const auctionText = type.isTypeOf === 'AuctionTypeOpen' ? 'an open' : 'a timed' + return { + icon: await getNotificationIcon(em, 'nft'), + link: await getNotificationLink(em, 'nft-page', [videoId]), + avatar: await getNotificationAvatar(em, 'membershipId', recipientId), + text: `You won ${auctionText} auction for NFT: “${videoTitle}”`, + } + } + case 'AuctionLost': { + const { videoId, videoTitle, type } = notificationType + const auctionText = type.isTypeOf === 'AuctionTypeOpen' ? 'an open' : 'a timed' + return { + icon: await getNotificationIcon(em, 'nft-alt'), + link: await getNotificationLink(em, 'nft-page', [videoId]), + avatar: await getNotificationAvatar(em, 'membershipId', recipientId), + text: `You lost ${auctionText} auction for NFT: “${videoTitle}”. Withdraw your bid`, + } + } + + // + // Channel notifications events + // + + // Content moderation and featuring + case 'ChannelExcluded': { + const channel = await em.getRepository(Channel).findOneBy({ id: recipientId }) + return { + icon: await getNotificationIcon(em, 'warning'), + link: await getNotificationLink(em, 'term-of-sevice-page'), + avatar: await getNotificationAvatar(em, 'channelId', recipientId), + text: `Your channel “${channel?.title}” is excluded from App`, + } + } + case 'VideoExcluded': { + const { videoTitle } = notificationType + return { + icon: await getNotificationIcon(em, 'warning'), + link: await getNotificationLink(em, 'term-of-sevice-page'), + avatar: await getNotificationAvatar(em, 'channelId', recipientId), + text: `Your video is excluded from App: “${videoTitle}”`, + } + } + case 'NftFeaturedOnMarketPlace': { + const { videoTitle } = notificationType + return { + icon: await getNotificationIcon(em, 'bell'), + link: await getNotificationLink(em, 'marketplace-page'), + avatar: await getNotificationAvatar(em, 'channelId', recipientId), + text: `Your NFT was featured in the marketplace featured section: “${videoTitle}”`, + } + } + + // Engagement + case 'NewChannelFollower': { + const { followerHandle } = notificationType + return { + icon: await getNotificationIcon(em, 'follow'), + link: await getNotificationLink(em, 'member-page', [followerHandle]), + avatar: await getNotificationAvatar(em, 'membershipHandle', followerHandle), + text: `${followerHandle} followed your channel`, + } + } + case 'CommentPostedToVideo': { + const { videoId, videoTitle, memberHandle } = notificationType + return { + icon: await getNotificationIcon(em, 'follow'), + link: await getNotificationLink(em, 'nft-page', [videoId]), + avatar: await getNotificationAvatar(em, 'membershipHandle', memberHandle), + text: `${memberHandle} left a comment on your video: “${videoTitle}”`, + } + } + case 'VideoLiked': { + const { videoId, videoTitle, memberHandle } = notificationType + return { + icon: await getNotificationIcon(em, 'like'), + link: await getNotificationLink(em, 'video-page', [videoId]), + avatar: await getNotificationAvatar(em, 'membershipHandle', memberHandle), + text: `${memberHandle} liked your video: “${videoTitle}”`, + } + } + case 'VideoDisliked': { + const { videoId, videoTitle, memberHandle } = notificationType + return { + icon: await getNotificationIcon(em, 'dislike'), + link: await getNotificationLink(em, 'video-page', [videoId]), + avatar: await getNotificationAvatar(em, 'membershipHandle', memberHandle), + text: `${memberHandle} disliked your video: “${videoTitle}”`, + } + } + + // Youtube Partnership Program + case 'ChannelVerified': { + return { + icon: await getNotificationIcon(em, 'bell'), + link: await getNotificationLink(em, 'ypp-dashboard'), + avatar: await getNotificationAvatar(em, 'channelId', recipientId), + text: `Your channel got verified in our Youtube Partnership Program`, + } + } + case 'ChannelSuspended': { + return { + icon: await getNotificationIcon(em, 'warning'), + link: await getNotificationLink(em, 'ypp-dashboard'), + avatar: await getNotificationAvatar(em, 'channelId', recipientId), + text: `Your channel got suspended in our Youtube Partnership Program`, + } + } + + // NFTs Auctions + case 'NftPurchased': { + const { videoId, videoTitle, buyerHandle, price } = notificationType + return { + icon: await getNotificationIcon(em, 'nft'), + link: await getNotificationLink(em, 'nft-page', [videoId]), + avatar: await getNotificationAvatar(em, 'membershipHandle', buyerHandle), + text: `${buyerHandle} purchased for ${formatJOY(price)} your NFT: “${videoTitle}”`, + } + } + case 'NftRoyaltyPaid': { + const { videoId, videoTitle, amount } = notificationType + return { + icon: await getNotificationIcon(em, 'nft'), + link: await getNotificationLink(em, 'nft-page', [videoId]), + avatar: await getNotificationAvatar(em, 'channelId', recipientId), + text: `You received ${formatJOY(amount)} royalties from your NFT: “${videoTitle}”`, + } + } + case 'CreatorReceivesAuctionBid': { + const { videoId, videoTitle, amount, bidderHandle } = notificationType + return { + icon: await getNotificationIcon(em, 'nft'), + link: await getNotificationLink(em, 'nft-page', [videoId]), + avatar: await getNotificationAvatar(em, 'membershipHandle', bidderHandle), + text: `${bidderHandle} placed a bid of ${formatJOY(amount)} for your NFT: “${videoTitle}”`, + } + } + + // Payouts + case 'DirectChannelPaymentByMember': { + const { amount, payerHandle } = notificationType + return { + icon: await getNotificationIcon(em, 'payout'), + link: await getNotificationLink(em, 'member-page', [payerHandle]), + avatar: await getNotificationAvatar(em, 'membershipHandle', payerHandle), + text: `${payerHandle} transferred ${formatJOY(amount)} to your channel`, + } + } + case 'ChannelFundsWithdrawn': { + const { amount } = notificationType + return { + icon: await getNotificationIcon(em, 'payout'), + link: await getNotificationLink(em, 'payments-page'), + avatar: await getNotificationAvatar(em, 'membershipId', recipientId), + text: `${formatJOY(amount)} were withdrawn from your channel account`, + } + } + } +} + +const JOY_DECIMAL = 10 +const formatJOY = (hapiAmount: bigint): string => { + const [intPart, decPart] = splitInt(String(hapiAmount), JOY_DECIMAL) + const formatedIntPart = chunkFromEnd(intPart, 3).join(' ') + const roundedDec = Math.round(Number(splitInt(decPart, 2).join('.'))) + const _decPart = formatedIntPart === '0' && roundedDec === 0 ? Number(decPart) : roundedDec + const joyAmount = _decPart ? `${formatedIntPart}.${_decPart}` : formatedIntPart + + return `${joyAmount} $JOY` +} +const splitInt = (numStr: string, decimal: number) => { + return [numStr.slice(0, -decimal) ?? '0', numStr.slice(-decimal).padStart(decimal)] +} +const chunkFromEnd = (str: string, interval: number): string[] => + Array.from({ length: Math.floor((str.length - 1) / interval) }).reduce( + ([head, ...tail]: string[]) => [head.slice(0, -interval), head.slice(-interval), ...tail], + [str] + )