diff --git a/package-lock.json b/package-lock.json index 80565f1572..dffd10991d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "express": "^4.19.2", "express-basic-auth": "^1.2.1", "express-validator": "^5.2.0", + "firebase-admin": "^12.1.1", "glob": "^8.1.0", "got": "^11.8.6", "gulp": "^4.0.0", @@ -2195,6 +2196,115 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==" + }, + "node_modules/@firebase/component": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.7.tgz", + "integrity": "sha512-baH1AA5zxfaz4O8w0vDwETByrKTQqB5CDjRls79Sa4eAGAoERw4Tnung7XbMl3jbJ4B/dmmtsMrdki0KikwDYA==", + "dependencies": { + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/component/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@firebase/database": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.5.tgz", + "integrity": "sha512-cAfwBqMQuW6HbhwI3Cb/gDqZg7aR0OmaJ85WUxlnoYW2Tm4eR0hFl5FEijI3/gYPUiUcUPQvTkGV222VkT7KPw==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.5.tgz", + "integrity": "sha512-NDSMaDjQ+TZEMDMmzJwlTL05kh1+0Y84C+kVMaOmNOzRGRM7VHi29I6YUhCetXH+/b1Wh4ZZRyp1CuWkd8s6hg==", + "dependencies": { + "@firebase/component": "0.6.7", + "@firebase/database": "1.0.5", + "@firebase/database-types": "1.0.3", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@firebase/database-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.3.tgz", + "integrity": "sha512-39V/Riv2R3O/aUjYKh0xypj7NTNXNAK1bcgY5Kx+hdQPRS/aPTS8/5c0CGFYKgVuFbYlnlnhrCTYsh2uNhGwzA==", + "dependencies": { + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.9.6" + } + }, + "node_modules/@firebase/database/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/logger/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@firebase/util": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.6.tgz", + "integrity": "sha512-IBr1MZbp4d5MjBCXL3TW1dK/PDXX4yOGbiwRNh1oAbE/+ci5Uuvy9KIrsFYY80as1I0iOaD5oOMA9Q8j4TJWcw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/@google-cloud/common": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-4.0.3.tgz", @@ -2214,6 +2324,34 @@ "node": ">=12.0.0" } }, + "node_modules/@google-cloud/firestore": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.7.0.tgz", + "integrity": "sha512-41/vBFXOeSYjFI/2mJuJrDwg2umGk+FDrI/SCGzBRUe+UZWDN4GoahIbGZ19YQsY0ANNl6DRiAy4wD6JezK02g==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", + "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@google-cloud/projectify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", @@ -2230,6 +2368,226 @@ "node": ">=12" } }, + "node_modules/@google-cloud/storage": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.11.1.tgz", + "integrity": "sha512-tibLSvgw7nDohMyIelt26kBpJ59YGWA2Rzep++DFNzEzKaSuCSp56Se9iM13ZlM3j5nLzR21IBkpRN58CmvCIw==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.3.0", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "optional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@google-cloud/storage/node_modules/gaxios": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.6.0.tgz", + "integrity": "sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "optional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@google-cloud/storage/node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-auth-library": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.10.0.tgz", + "integrity": "sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@google-cloud/storage/node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@google-cloud/trace-agent": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@google-cloud/trace-agent/-/trace-agent-7.1.2.tgz", @@ -2286,6 +2644,146 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/@grpc/grpc-js": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.8.tgz", + "integrity": "sha512-vYVqYzHicDqyKB+NQhAc54I1QWCBLCrYG6unqOIcBTHx+7x8C9lcoLj3KVJXs2VB4lUbpWY+Kk9NipcbXYWmvg==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@grpc/proto-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, + "node_modules/@grpc/proto-loader/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "optional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2368,6 +2866,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -2667,6 +3175,70 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "optional": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "optional": true + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2802,6 +3374,12 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "optional": true + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -2900,6 +3478,12 @@ "@types/node": "*" } }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2944,6 +3528,32 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -2971,6 +3581,12 @@ "@types/node": "*" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "optional": true + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -3174,6 +3790,18 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3986,6 +4614,15 @@ "semver": "bin/semver" } }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", @@ -5236,7 +5873,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "devOptional": true, "funding": [ { "type": "github", @@ -6696,7 +7332,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "devOptional": true, "engines": { "node": ">=4.0.0" } @@ -7109,14 +7744,14 @@ "optional": true }, "node_modules/duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" + "stream-shift": "^1.0.2" } }, "node_modules/each-props": { @@ -8396,6 +9031,15 @@ "through": "~2.3.1" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -8740,7 +9384,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "devOptional": true, "engines": { "node": ">=6" } @@ -9056,6 +9699,90 @@ "node": ">= 0.10" } }, + "node_modules/farmhash": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/farmhash/-/farmhash-3.3.1.tgz", + "integrity": "sha512-XUizHanzlr/v7suBr/o85HSakOoWh6HKXZjFYl5C2+Gj0f0rkw+XTUZzrd9odDsgI9G5tRUcF4wSbKaX04T0DQ==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^5.1.0", + "prebuild-install": "^7.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/farmhash/node_modules/node-abi": { + "version": "3.62.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz", + "integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/farmhash/node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/farmhash/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/farmhash/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9146,6 +9873,17 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -9358,6 +10096,62 @@ "node": ">= 0.10" } }, + "node_modules/firebase-admin": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.1.1.tgz", + "integrity": "sha512-Nuoxk//gaYrspS7TvwBINdGvFhh2QeiaWpRW6+PJ+tWyn2/CugBc7jKa1NaBg0AvhGSOXFOCIsXhzCzHA47Rew==", + "dependencies": { + "@fastify/busboy": "^2.1.0", + "@firebase/database-compat": "^1.0.2", + "@firebase/database-types": "^1.0.0", + "@types/node": "^20.10.3", + "farmhash": "^3.3.1", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "long": "^5.2.3", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.7.0", + "@google-cloud/storage": "^7.7.0" + } + }, + "node_modules/firebase-admin/node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/firebase-admin/node_modules/jose": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/firebase-admin/node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/first-chunk-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", @@ -9611,8 +10405,7 @@ "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "devOptional": true + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "node_modules/fs-extra": { "version": "10.1.0", @@ -9995,8 +10788,7 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "devOptional": true + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "node_modules/glob": { "version": "8.1.0", @@ -10516,6 +11308,192 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/google-gax": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.3.tgz", + "integrity": "sha512-f4F2Y9X4+mqsrJuLZsuTljYuQpcBnQsCt9ScvZpdM8jGjqrcxyJi5JUiqtq0jtpdHVPzyit0N7f5t07e+kH5EA==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.10.3", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.6", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "optional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-gax/node_modules/gaxios": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.6.0.tgz", + "integrity": "sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.10.0.tgz", + "integrity": "sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/google-gax/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "optional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-gax/node_modules/protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/google-gax/node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/google-gax/node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/google-p12-pem": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", @@ -11153,6 +12131,22 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "optional": true + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -11173,6 +12167,11 @@ "node": ">= 0.8" } }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -11248,7 +12247,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true, "funding": [ { "type": "github", @@ -13089,6 +14087,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "optional": true + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -13195,6 +14199,11 @@ "node": ">= 12.0.0" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -13814,8 +14823,7 @@ "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "devOptional": true + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "node_modules/mocha": { "version": "5.2.0", @@ -14366,8 +15374,7 @@ "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "devOptional": true + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -14951,6 +15958,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -16108,6 +17124,42 @@ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "optional": true }, + "node_modules/proto3-json-serializer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz", + "integrity": "sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA==", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -16320,7 +17372,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "devOptional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -16335,7 +17386,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -16879,6 +17929,15 @@ "node": ">=0.12" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/retry-request": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", @@ -17475,7 +18534,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "devOptional": true, "funding": [ { "type": "github", @@ -18665,7 +19723,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "devOptional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -18677,7 +19734,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "devOptional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -18687,14 +19743,12 @@ "node_modules/tar-fs/node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "devOptional": true + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/tar-fs/node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "devOptional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -20187,6 +21241,27 @@ "node": ">=10.13.0" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index ab58654179..cfc758f47f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "express": "^4.19.2", "express-basic-auth": "^1.2.1", "express-validator": "^5.2.0", + "firebase-admin": "^12.1.1", "glob": "^8.1.0", "got": "^11.8.6", "gulp": "^4.0.0", diff --git a/test/api/unit/libs/pushNotifications.js b/test/api/unit/libs/pushNotifications.js deleted file mode 100644 index ba719e09a3..0000000000 --- a/test/api/unit/libs/pushNotifications.js +++ /dev/null @@ -1,184 +0,0 @@ -import apn from '@parse/node-apn/mock'; -import _ from 'lodash'; -import nconf from 'nconf'; -import gcmLib from 'node-gcm'; // works with FCM notifications too -import { model as User } from '../../../../website/server/models/user'; -import { - sendNotification as sendPushNotification, - MAX_MESSAGE_LENGTH, -} from '../../../../website/server/libs/pushNotifications'; - -describe('pushNotifications', () => { - let user; - let fcmSendSpy; - let apnSendSpy; - - const identifier = 'identifier'; - const title = 'title'; - const message = 'message'; - - beforeEach(() => { - user = new User(); - fcmSendSpy = sinon.spy(); - apnSendSpy = sinon.spy(); - - sandbox.stub(nconf, 'get').returns('true-key'); - - sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy); - - sandbox.stub(apn.Provider.prototype, 'send').returns({ - on: () => null, - send: apnSendSpy, - }); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('throws if user is not supplied', () => { - expect(sendPushNotification).to.throw; - expect(fcmSendSpy).to.not.have.been.called; - expect(apnSendSpy).to.not.have.been.called; - }); - - it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => { - user.preferences.pushNotifications.unsubscribeFromAll = true; - expect(() => sendPushNotification(user)).to.throw; - expect(fcmSendSpy).to.not.have.been.called; - expect(apnSendSpy).to.not.have.been.called; - }); - - it('throws if details.identifier is not supplied', () => { - expect(() => sendPushNotification(user, { - title, - message, - })).to.throw; - expect(fcmSendSpy).to.not.have.been.called; - expect(apnSendSpy).to.not.have.been.called; - }); - - it('throws if details.title is not supplied', () => { - expect(() => sendPushNotification(user, { - identifier, - message, - })).to.throw; - expect(fcmSendSpy).to.not.have.been.called; - expect(apnSendSpy).to.not.have.been.called; - }); - - it('throws if details.message is not supplied', () => { - expect(() => sendPushNotification(user, { - identifier, - title, - })).to.throw; - expect(fcmSendSpy).to.not.have.been.called; - expect(apnSendSpy).to.not.have.been.called; - }); - - it('returns if no device is registered', () => { - sendPushNotification(user, { - identifier, - title, - message, - }); - expect(fcmSendSpy).to.not.have.been.called; - expect(apnSendSpy).to.not.have.been.called; - }); - - it('cuts the message to 300 chars', () => { - const longMessage = `12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`; - - expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true); - - const details = { - identifier, - title, - message: longMessage, - payload: { - message: longMessage, - }, - }; - - sendPushNotification(user, details); - - expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH })); - expect(details.payload.message) - .to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH })); - - expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH); - expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH); - }); - - it('cuts the message to 300 chars (no payload)', () => { - const longMessage = `12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 - 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`; - - expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true); - - const details = { - identifier, - title, - message: longMessage, - }; - - sendPushNotification(user, details); - - expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH })); - expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH); - }); - - // TODO disabled because APN relies on a Promise - xit('uses APN for iOS devices', () => { - user.pushDevices.push({ - type: 'ios', - regId: '123', - }); - - const details = { - identifier, - title, - message, - category: 'fun', - payload: { - a: true, - b: true, - }, - }; - - const expectedNotification = new apn.Notification({ - alert: message, - sound: 'default', - category: 'fun', - payload: { - identifier, - a: true, - b: true, - }, - }); - - sendPushNotification(user, details); - expect(apnSendSpy).to.have.been.calledOnce; - expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123'); - expect(fcmSendSpy).to.not.have.been.called; - }); -}); diff --git a/test/api/unit/libs/pushNotifications.test.js b/test/api/unit/libs/pushNotifications.test.js new file mode 100644 index 0000000000..96a6197526 --- /dev/null +++ b/test/api/unit/libs/pushNotifications.test.js @@ -0,0 +1,354 @@ +import apn from '@parse/node-apn'; +import _ from 'lodash'; +import nconf from 'nconf'; +import admin from 'firebase-admin'; +import { model as User } from '../../../../website/server/models/user'; +import { + MAX_MESSAGE_LENGTH, +} from '../../../../website/server/libs/pushNotifications'; + +let sendPushNotification; + +describe('pushNotifications', () => { + let user; + let fcmSendSpy; + let apnSendSpy; + let updateStub; + let classStubbedInstance; + + const identifier = 'identifier'; + const title = 'title'; + const message = 'message'; + + beforeEach(() => { + user = new User(); + fcmSendSpy = sinon.stub().returns(Promise.resolve('success')); + apnSendSpy = sinon.stub().returns(Promise.resolve()); + + nconf.set('PUSH_CONFIGS_APN_ENABLED', 'true'); + + classStubbedInstance = sandbox.createStubInstance(apn.Provider, { + send: apnSendSpy, + }); + sandbox.stub(apn, 'Provider').returns(classStubbedInstance); + + delete require.cache[require.resolve('../../../../website/server/libs/pushNotifications')]; + // eslint-disable-next-line global-require + sendPushNotification = require('../../../../website/server/libs/pushNotifications').sendNotification; + + updateStub = sandbox.stub(User, 'updateOne').resolves(); + sandbox.stub(admin, 'messaging').get(() => () => ({ send: fcmSendSpy })); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('validates supplied data', () => { + it('throws if user is not supplied', () => { + expect(sendPushNotification).to.throw; + expect(fcmSendSpy).to.not.have.been.called; + expect(apnSendSpy).to.not.have.been.called; + }); + + it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => { + user.preferences.pushNotifications.unsubscribeFromAll = true; + expect(() => sendPushNotification(user)).to.throw; + expect(fcmSendSpy).to.not.have.been.called; + expect(apnSendSpy).to.not.have.been.called; + }); + + it('throws if details.identifier is not supplied', () => { + expect(() => sendPushNotification(user, { + title, + message, + })).to.throw; + expect(fcmSendSpy).to.not.have.been.called; + expect(apnSendSpy).to.not.have.been.called; + }); + + it('throws if details.title is not supplied', () => { + expect(() => sendPushNotification(user, { + identifier, + message, + })).to.throw; + expect(fcmSendSpy).to.not.have.been.called; + expect(apnSendSpy).to.not.have.been.called; + }); + + it('throws if details.message is not supplied', () => { + expect(() => sendPushNotification(user, { + identifier, + title, + })).to.throw; + expect(fcmSendSpy).to.not.have.been.called; + expect(apnSendSpy).to.not.have.been.called; + }); + + it('returns if no device is registered', () => { + sendPushNotification(user, { + identifier, + title, + message, + }); + expect(fcmSendSpy).to.not.have.been.called; + expect(apnSendSpy).to.not.have.been.called; + }); + }); + + it('cuts the message to 300 chars', () => { + const longMessage = `12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`; + + expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true); + + const details = { + identifier, + title, + message: longMessage, + payload: { + message: longMessage, + }, + }; + + sendPushNotification(user, details); + + expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH })); + expect(details.payload.message) + .to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH })); + + expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH); + expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH); + }); + + it('cuts the message to 300 chars (no payload)', () => { + const longMessage = `12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345 + 12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`; + + expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true); + + const details = { + identifier, + title, + message: longMessage, + }; + + sendPushNotification(user, details); + + expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH })); + expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH); + }); + + describe('sends notifications', () => { + let details; + + beforeEach(() => { + details = { + identifier, + title, + message, + category: 'fun', + payload: { + a: true, + b: true, + }, + }; + }); + + it('uses APN for iOS devices', async () => { + user.pushDevices.push({ + type: 'ios', + regId: '123', + }); + + const expectedNotification = new apn.Notification({ + alert: { + title, + body: message, + }, + sound: 'default', + category: 'fun', + payload: { + identifier, + a: true, + b: true, + }, + }); + + await sendPushNotification(user, details); + expect(apnSendSpy).to.have.been.calledOnce; + expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123'); + expect(fcmSendSpy).to.not.have.been.called; + }); + + it('uses FCM for Android devices', async () => { + user.pushDevices.push({ + type: 'android', + regId: '123', + }); + + const expectedMessage = { + notification: { + title, + body: message, + }, + data: { + identifier, + notificationIdentifier: identifier, + }, + token: '123', + }; + + await sendPushNotification(user, details); + expect(fcmSendSpy).to.have.been.calledOnce; + expect(fcmSendSpy).to.have.been.calledWithMatch(expectedMessage); + expect(apnSendSpy).to.not.have.been.called; + }); + + it('handles multiple devices', async () => { + user.pushDevices.push({ + type: 'android', + regId: '123', + }); + user.pushDevices.push({ + type: 'ios', + regId: '456', + }); + user.pushDevices.push({ + type: 'android', + regId: '789', + }); + + await sendPushNotification(user, details); + expect(fcmSendSpy).to.have.been.calledTwice; + expect(apnSendSpy).to.have.been.calledOnce; + }); + }); + + describe('handles sending errors', () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('removes unregistered fcm devices', async () => { + user.pushDevices.push({ + type: 'android', + regId: '123', + }); + + const error = new Error(); + error.code = 'messaging/registration-token-not-registered'; + fcmSendSpy.rejects(error); + + await sendPushNotification(user, { + identifier, + title, + message, + }); + + expect(fcmSendSpy).to.have.been.calledOnce; + expect(apnSendSpy).to.not.have.been.called; + await clock.tick(10); + expect(updateStub).to.have.been.calledOnce; + }); + + it('removes invalid fcm devices', async () => { + user.pushDevices.push({ + type: 'android', + regId: '123', + }); + + const error = new Error(); + error.code = 'messaging/registration-token-not-registered'; + fcmSendSpy.rejects(error); + + await sendPushNotification(user, { + identifier, + title, + message, + }); + + expect(fcmSendSpy).to.have.been.calledOnce; + expect(apnSendSpy).to.not.have.been.called; + expect(updateStub).to.have.been.calledOnce; + }); + + it('removes unregistered apn devices', async () => { + user.pushDevices.push({ + type: 'ios', + regId: '123', + }); + + const error = { + failed: [ + { + device: '123', + response: { reason: 'Unregistered' }, + }, + ], + }; + apnSendSpy.resolves(error); + + await sendPushNotification(user, { + identifier, + title, + message, + }); + + expect(fcmSendSpy).to.not.have.been.called; + expect(apnSendSpy).to.have.been.calledOnce; + expect(updateStub).to.have.been.calledOnce; + }); + + it('removes invalid apn devices', async () => { + user.pushDevices.push({ + type: 'ios', + regId: '123', + }); + + const error = { + failed: [ + { + device: '123', + response: { reason: 'BadDeviceToken' }, + }, + ], + }; + apnSendSpy.resolves(error); + + await sendPushNotification(user, { + identifier, + title, + message, + }); + + expect(fcmSendSpy).to.not.have.been.called; + expect(apnSendSpy).to.have.been.calledOnce; + expect(updateStub).to.have.been.calledOnce; + }); + }); +}); diff --git a/test/api/unit/models/group.test.js b/test/api/unit/models/group.test.js index 9a2f7d6c9b..e2d54d8575 100644 --- a/test/api/unit/models/group.test.js +++ b/test/api/unit/models/group.test.js @@ -1362,8 +1362,8 @@ describe('Group Model', () => { sandbox.spy(User, 'updateMany'); }); - it('formats message', () => { - const chatMessage = party.sendChat({ + it('formats message', async () => { + const chatMessage = await party.sendChat({ message: 'a _new_ message with *markdown*', user: { _id: 'user-id', @@ -1396,8 +1396,8 @@ describe('Group Model', () => { expect(chat.user).to.eql('user name'); }); - it('formats message as system if no user is passed in', () => { - const chat = party.sendChat({ message: 'a system message' }); + it('formats message as system if no user is passed in', async () => { + const chat = await party.sendChat({ message: 'a system message' }); expect(chat.text).to.eql('a system message'); expect(validator.isUUID(chat.id)).to.eql(true); @@ -1411,8 +1411,8 @@ describe('Group Model', () => { expect(chat.user).to.not.exist; }); - it('updates users about new messages in party', () => { - party.sendChat({ message: 'message' }); + it('updates users about new messages in party', async () => { + await party.sendChat({ message: 'message' }); expect(User.updateMany).to.be.calledOnce; expect(User.updateMany).to.be.calledWithMatch({ @@ -1421,12 +1421,12 @@ describe('Group Model', () => { }); }); - it('updates users about new messages in group', () => { + it('updates users about new messages in group', async () => { const group = new Group({ type: 'guild', }); - group.sendChat({ message: 'message' }); + await group.sendChat({ message: 'message' }); expect(User.updateMany).to.be.calledOnce; expect(User.updateMany).to.be.calledWithMatch({ @@ -1435,8 +1435,8 @@ describe('Group Model', () => { }); }); - it('does not send update to user that sent the message', () => { - party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } }); + it('does not send update to user that sent the message', async () => { + await party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } }); expect(User.updateMany).to.be.calledOnce; expect(User.updateMany).to.be.calledWithMatch({ @@ -1445,18 +1445,18 @@ describe('Group Model', () => { }); }); - it('skips sending new message notification for guilds with > 5000 members', () => { + it('skips sending new message notification for guilds with > 5000 members', async () => { party.memberCount = 5001; - party.sendChat({ message: 'message' }); + await party.sendChat({ message: 'message' }); expect(User.updateMany).to.not.be.called; }); - it('skips sending messages to the tavern', () => { + it('skips sending messages to the tavern', async () => { party._id = TAVERN_ID; - party.sendChat({ message: 'message' }); + await party.sendChat({ message: 'message' }); expect(User.updateMany).to.not.be.called; }); @@ -2326,7 +2326,7 @@ describe('Group Model', () => { await guild.save(); - const groupMessage = guild.sendChat({ message: 'Test message.' }); + const groupMessage = await guild.sendChat({ message: 'Test message.' }); await groupMessage.save(); await sleep(); diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js index 4d3ca353c0..cef8f8c185 100644 --- a/website/server/controllers/api-v3/chat.js +++ b/website/server/controllers/api-v3/chat.js @@ -214,7 +214,7 @@ api.postChat = { }); } - const newChatMessage = group.sendChat({ + const newChatMessage = await group.sendChat({ message, user, flagCount, diff --git a/website/server/controllers/api-v3/members.js b/website/server/controllers/api-v3/members.js index e48cc99423..997ac57b2f 100644 --- a/website/server/controllers/api-v3/members.js +++ b/website/server/controllers/api-v3/members.js @@ -756,7 +756,7 @@ api.transferGems = { ]); } if (receiver.preferences.pushNotifications.giftedGems !== false) { - sendPushNotification( + await sendPushNotification( receiver, { title: res.t('giftedGems', receiverLang), diff --git a/website/server/controllers/api-v3/quests.js b/website/server/controllers/api-v3/quests.js index 42940d9666..1c2e94b457 100644 --- a/website/server/controllers/api-v3/quests.js +++ b/website/server/controllers/api-v3/quests.js @@ -120,10 +120,10 @@ api.inviteToQuest = { // send out invites const inviterVars = getUserInfo(user, ['name', 'email']); - const membersToEmail = members.filter(member => { + const membersToEmail = members.filter(async member => { // send push notifications while filtering members before sending emails if (member.preferences.pushNotifications.invitedQuest !== false) { - sendPushNotification( + await sendPushNotification( member, { title: quest.text(member.preferences.language), @@ -394,7 +394,7 @@ api.cancelQuest = { if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest')); const questName = questScrolls[group.quest.key].text('en'); - const newChatMessage = group.sendChat({ + const newChatMessage = await group.sendChat({ message: `\`${user.profile.name} cancelled the party quest ${questName}.\``, info: { type: 'quest_cancel', @@ -456,7 +456,7 @@ api.abortQuest = { if (user._id !== group.leader && user._id !== group.quest.leader) throw new NotAuthorized(res.t('onlyLeaderAbortQuest')); const questName = questScrolls[group.quest.key].text('en'); - const newChatMessage = group.sendChat({ + const newChatMessage = await group.sendChat({ message: `\`${common.i18n.t('chatQuestAborted', { username: user.profile.name, questName }, 'en')}\``, info: { type: 'quest_abort', diff --git a/website/server/libs/chat.js b/website/server/libs/chat.js index fa7b9c8bbf..f7c8d9cd38 100644 --- a/website/server/libs/chat.js +++ b/website/server/libs/chat.js @@ -25,7 +25,7 @@ export async function sendChatPushNotifications (user, group, message, mentions, .select('preferences.pushNotifications preferences.language profile.name pushDevices auth.local.username') .exec(); - members.forEach(member => { + members.forEach(async member => { if (member.preferences.pushNotifications.partyActivity !== false) { if (mentions && mentions.includes(`@${member.auth.local.username}`) && member.preferences.pushNotifications.mentionParty !== false) { return; @@ -33,7 +33,7 @@ export async function sendChatPushNotifications (user, group, message, mentions, if (!message.unformattedText) return; - sendPushNotification( + await sendPushNotification( member, { title: translate('groupActivityNotificationTitle', { user: message.user, group: group.name }, member.preferences.language), diff --git a/website/server/libs/inbox/index.js b/website/server/libs/inbox/index.js index 583ae0275c..2fe525b360 100644 --- a/website/server/libs/inbox/index.js +++ b/website/server/libs/inbox/index.js @@ -13,7 +13,7 @@ export async function sentMessage (sender, receiver, message, translate) { } if (receiver.preferences.pushNotifications.newPM !== false && messageSent.unformattedText) { - sendPushNotification( + await sendPushNotification( receiver, { title: translate( diff --git a/website/server/libs/invites/index.js b/website/server/libs/invites/index.js index 2a6a6d790d..ba231ff002 100644 --- a/website/server/libs/invites/index.js +++ b/website/server/libs/invites/index.js @@ -16,12 +16,12 @@ import { model as Group, } from '../../models/group'; -function sendInvitePushNotification (userToInvite, groupLabel, group, publicGuild, res) { +async function sendInvitePushNotification (userToInvite, groupLabel, group, publicGuild, res) { if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] === false) return; const identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty'; - sendPushNotification( + await sendPushNotification( userToInvite, { title: group.name, @@ -110,7 +110,7 @@ async function addInvitationToUser (userToInvite, group, inviter, res) { const groupLabel = group.type === 'guild' ? 'Guild' : 'Party'; sendInviteEmail(userToInvite, groupLabel, group, inviter); - sendInvitePushNotification(userToInvite, groupLabel, group, publicGuild, res); + await sendInvitePushNotification(userToInvite, groupLabel, group, publicGuild, res); const userInvited = await userToInvite.save(); if (group.type === 'guild') { diff --git a/website/server/libs/payments/gems.js b/website/server/libs/payments/gems.js index 8e2081c3e7..22c45c9d58 100644 --- a/website/server/libs/payments/gems.js +++ b/website/server/libs/payments/gems.js @@ -50,7 +50,7 @@ async function buyGemGift (data) { data.gift.member._id !== data.user._id && data.gift.member.preferences.pushNotifications.giftedGems !== false ) { - sendPushNotification( + await sendPushNotification( data.gift.member, { title: shared.i18n.t('giftedGems', languages[1]), diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index 694a738e10..33dd3421e7 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -367,7 +367,7 @@ async function createSubscription (data) { } if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) { - sendPushNotification( + await sendPushNotification( data.gift.member, { title: shared.i18n.t('giftedSubscription', languages[1]), diff --git a/website/server/libs/pushNotifications.js b/website/server/libs/pushNotifications.js index 1ef9e7f387..a38162bf00 100644 --- a/website/server/libs/pushNotifications.js +++ b/website/server/libs/pushNotifications.js @@ -1,15 +1,12 @@ import _ from 'lodash'; import nconf from 'nconf'; import apn from '@parse/node-apn'; -import gcmLib from 'node-gcm'; // works with FCM notifications too +import admin from 'firebase-admin'; import logger from './logger'; import { // eslint-disable-line import/no-cycle model as User, } from '../models/user'; -const FCM_API_KEY = nconf.get('PUSH_CONFIGS_FCM_SERVER_API_KEY'); -const fcmSender = FCM_API_KEY ? new gcmLib.Sender(FCM_API_KEY) : undefined; - const APN_ENABLED = nconf.get('PUSH_CONFIGS_APN_ENABLED') === 'true'; const apnProvider = APN_ENABLED ? new apn.Provider({ token: { @@ -30,7 +27,91 @@ function removePushDevice (user, pushDevice) { export const MAX_MESSAGE_LENGTH = 300; -export function sendNotification (user, details = {}) { +async function sendFCMNotification (user, pushDevice, payload) { + const messaging = admin.messaging(); + if (messaging === undefined) { + return; + } + const message = { + notification: { + title: payload.title, + body: payload.body, + }, + data: { + identifier: payload.identifier, + notificationIdentifier: payload.identifier, + }, + token: pushDevice.regId, + }; + + try { + await messaging.send(message); + } catch (error) { + if (error.code === 'messaging/registration-token-not-registered') { + removePushDevice(user, pushDevice); + logger.error(new Error('FCM error, unregistered pushDevice'), { + regId: pushDevice.regId, userId: user._id, + }); + } else if (error.code === 'messaging/invalid-registration-token') { + removePushDevice(user, pushDevice); + logger.error(new Error('FCM error, invalid pushDevice'), { + regId: pushDevice.regId, userId: user._id, + }); + } else { + logger.error(error, 'Unhandled FCM error.'); + } + } +} + +async function sendAPNNotification (user, pushDevice, details, payload) { + if (apnProvider) { + const notification = new apn.Notification({ + alert: { + title: details.title, + body: details.message, + }, + sound: 'default', + category: details.category, + topic: 'com.habitrpg.ios.Habitica', + payload, + }); + try { + const response = await apnProvider.send(notification, pushDevice.regId); + // Handle failed push notifications deliveries + response.failed.forEach(failure => { + if (failure.error) { // generic error + logger.error(new Error('Unhandled APN error'), { + response, regId: pushDevice.regId, userId: user._id, + }); + } else { // rejected + // see https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW17 + // for a list of rejection reasons + const { reason } = failure.response; + if (reason === 'Unregistered') { + removePushDevice(user, pushDevice); + logger.error(new Error('APN error, unregistered pushDevice'), { + regId: pushDevice.regId, userId: user._id, + }); + } else { + if (reason === 'BadDeviceToken') { + // An invalid token was registered by mistake + // Remove it but log the error differently so that it can be distinguished + // from when reason === Unregistered + removePushDevice(user, pushDevice); + } + logger.error(new Error('APN error'), { + response, regId: pushDevice.regId, userId: user._id, + }); + } + } + }); + } catch (err) { + logger.error(err, 'Unhandled APN error.'); + } + } +} + +export async function sendNotification (user, details = {}) { if (!user) throw new Error('User is required.'); if (user.preferences.pushNotifications.unsubscribeFromAll === true) return; const pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices; @@ -51,110 +132,16 @@ export function sendNotification (user, details = {}) { payload.message = _.truncate(payload.message, { length: MAX_MESSAGE_LENGTH }); } - _.each(pushDevices, pushDevice => { + await _.each(pushDevices, async pushDevice => { switch (pushDevice.type) { // eslint-disable-line default-case case 'android': // Required for fcm to be received in background payload.title = details.title; payload.body = details.message; - - if (fcmSender) { - const message = new gcmLib.Message({ - data: payload, - }); - - fcmSender.send(message, { - registrationTokens: [pushDevice.regId], - }, 5, (err, response) => { - if (err) { - logger.error(err, 'Unhandled FCM error.'); - return; - } - - // Handle failed push notifications deliveries - // Note that we're always sending to one device, not multiple - const failed = response - && response.results && response.results[0] && response.results[0].error; - - if (failed) { - // See https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9 - // for the list of errors - - // The regId is not valid anymore, remove it - if (failed === 'NotRegistered') { - removePushDevice(user, pushDevice); - logger.error(new Error('FCM error, unregistered pushDevice'), { - regId: pushDevice.regId, userId: user._id, - }); - } else { - // An invalid token was registered by mistake - // Remove it but log the error differently so that it can be distinguished - // from when failed === NotRegistered - // Blacklisted can happen in some rare cases, - // see https://stackoverflow.com/questions/42136122/why-does-firebase-push-token-return-blacklisted - // MismatchSenderId could be due to old tokens, - // see https://stackoverflow.com/questions/11313342/why-do-i-get-mismatchsenderid-from-gcm-server-side - if ( - failed === 'InvalidRegistration' - || failed === 'MismatchSenderId' - || pushDevice.regId === 'BLACKLISTED' - ) { - removePushDevice(user, pushDevice); - } - logger.error(new Error('FCM error'), { - response, regId: pushDevice.regId, userId: user._id, - }); - } - } - }); - } + await sendFCMNotification(user, pushDevice, payload); break; - case 'ios': - if (apnProvider) { - const notification = new apn.Notification({ - alert: { - title: details.title, - body: details.message, - }, - sound: 'default', - category: details.category, - topic: 'com.habitrpg.ios.Habitica', - payload, - }); - apnProvider.send(notification, pushDevice.regId) - .then(response => { - // Handle failed push notifications deliveries - response.failed.forEach(failure => { - if (failure.error) { // generic error - logger.error(new Error('Unhandled APN error'), { - response, regId: pushDevice.regId, userId: user._id, - }); - } else { // rejected - // see https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW17 - // for a list of rejection reasons - const { reason } = failure.response; - if (reason === 'Unregistered') { - removePushDevice(user, pushDevice); - logger.error(new Error('APN error, unregistered pushDevice'), { - regId: pushDevice.regId, userId: user._id, - }); - } else { - if (reason === 'BadDeviceToken') { - // An invalid token was registered by mistake - // Remove it but log the error differently so that it can be distinguished - // from when reason === Unregistered - removePushDevice(user, pushDevice); - } - logger.error(new Error('APN error'), { - response, regId: pushDevice.regId, userId: user._id, - }); - } - } - }); - }) - .catch(err => logger.error(err, 'Unhandled APN error.')); - } + sendAPNNotification(user, pushDevice, details, payload); break; } }); diff --git a/website/server/libs/setupFirebase.js b/website/server/libs/setupFirebase.js new file mode 100644 index 0000000000..5916eda2af --- /dev/null +++ b/website/server/libs/setupFirebase.js @@ -0,0 +1,14 @@ +import admin from 'firebase-admin'; +import nconf from 'nconf'; + +if (nconf.get('FIREBASE_PROJECT_ID') !== undefined && nconf.get('FIREBASE_PROJECT_ID') !== '') { + if (!global.firebaseApp) { + global.firebaseApp = admin.initializeApp({ + credential: admin.credential.cert({ + projectId: nconf.get('FIREBASE_PROJECT_ID'), + clientEmail: nconf.get('FIREBASE_CLIENT_EMAIL'), + privateKey: nconf.get('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'), + }), + }); + } +} diff --git a/website/server/libs/spells.js b/website/server/libs/spells.js index e744a2e987..8d7158c25c 100644 --- a/website/server/libs/spells.js +++ b/website/server/libs/spells.js @@ -254,7 +254,7 @@ async function castSpell (req, res, { isV3 = false }) { if (lastMessage && lastMessage.info.spell === spellId && lastMessage.info.user === user.profile.name && lastMessage.info.target === partyMembers.profile.name) { - const newChatMessage = party.sendChat({ + const newChatMessage = await party.sendChat({ message: `\`${common.i18n.t('chatCastSpellUserTimes', { username: user.profile.name, spell: spell.text(), @@ -273,7 +273,7 @@ async function castSpell (req, res, { isV3 = false }) { await newChatMessage.save(); await lastMessage.deleteOne(); } else { // Single target spell, not repeated - const newChatMessage = party.sendChat({ + const newChatMessage = await party.sendChat({ message: `\`${common.i18n.t('chatCastSpellUser', { username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name }, 'en')}\``, info: { type: 'spell_cast_user', @@ -288,7 +288,7 @@ async function castSpell (req, res, { isV3 = false }) { } } else if (lastMessage && lastMessage.info.spell === spellId // Party spell, check for repeat && lastMessage.info.user === user.profile.name) { - const newChatMessage = party.sendChat({ + const newChatMessage = await party.sendChat({ message: `\`${common.i18n.t('chatCastSpellPartyTimes', { username: user.profile.name, spell: spell.text(), @@ -305,7 +305,7 @@ async function castSpell (req, res, { isV3 = false }) { await newChatMessage.save(); await lastMessage.deleteOne(); } else { - const newChatMessage = party.sendChat({ // Non-repetitive partywide spell + const newChatMessage = await party.sendChat({ // Non-repetitive partywide spell message: `\`${common.i18n.t('chatCastSpellParty', { username: user.profile.name, spell: spell.text() }, 'en')}\``, info: { type: 'spell_cast_party', diff --git a/website/server/models/challenge.js b/website/server/models/challenge.js index cf02373a8e..5edce9027f 100644 --- a/website/server/models/challenge.js +++ b/website/server/models/challenge.js @@ -403,7 +403,7 @@ schema.methods.closeChal = async function closeChal (broken = {}) { ]); } if (savedWinner.preferences.pushNotifications.wonChallenge !== false) { - sendPushNotification( + await sendPushNotification( savedWinner, { title: challenge.name, diff --git a/website/server/models/group.js b/website/server/models/group.js index 920e93e260..6db5ff3e05 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -526,7 +526,7 @@ schema.methods.getMemberCount = async function getMemberCount () { return User.countDocuments(query).exec(); }; -schema.methods.sendChat = function sendChat (options = {}) { +schema.methods.sendChat = async function sendChat (options = {}) { const { message, user, metaData, client, flagCount = 0, info = {}, @@ -596,7 +596,7 @@ schema.methods.sendChat = function sendChat (options = {}) { sendChatPushNotifications(user, this, newChatMessage, mentions, translate); } if (mentionedMembers) { - mentionedMembers.forEach(member => { + await mentionedMembers.forEach(async member => { if (member._id === user._id) return; const pushNotifPrefs = member.preferences.pushNotifications; if (this.type === 'party') { @@ -617,7 +617,7 @@ schema.methods.sendChat = function sendChat (options = {}) { } if (newChatMessage.unformattedText) { - sendPushNotification(member, { + await sendPushNotification(member, { identifier: 'chatMention', title: `${user.profile.name} mentioned you in ${this.name}`, message: newChatMessage.unformattedText, @@ -751,7 +751,7 @@ schema.methods.startQuest = async function startQuest (user) { _id: { $in: nonMembers }, }, _cleanQuestParty()).exec(); - const newMessage = this.sendChat({ + const newMessage = await this.sendChat({ message: `\`${shared.i18n.t('chatQuestStarted', { questName: quest.text('en') }, 'en')}\``, metaData: { participatingMembers: this.getParticipatingQuestMembers().join(', '), @@ -766,7 +766,7 @@ schema.methods.startQuest = async function startQuest (user) { const membersToEmail = []; // send notifications and webhooks in the background without blocking - members.forEach(member => { + await members.forEach(async member => { if (member._id !== user._id) { // send push notifications and filter users that disabled emails if (member.preferences.emailNotifications.questStarted !== false) { @@ -776,7 +776,7 @@ schema.methods.startQuest = async function startQuest (user) { // send push notifications and filter users that disabled emails if (member.preferences.pushNotifications.questStarted !== false) { const memberLang = member.preferences.language; - sendPushNotification(member, { + await sendPushNotification(member, { title: quest.text(memberLang), message: shared.i18n.t('questStarted', memberLang), identifier: 'questStarted', @@ -1021,7 +1021,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) { group.quest.progress.hp -= progress.up; if (CRON_SAFE_MODE || CRON_SEMI_SAFE_MODE) { - const groupMessage = group.sendChat({ + const groupMessage = await group.sendChat({ message: `\`${shared.i18n.t('chatBossDontAttack', { bossName: quest.boss.name('en') }, 'en')}\``, info: { type: 'boss_dont_attack', @@ -1032,7 +1032,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) { }); promises.push(groupMessage.save()); } else { - const groupMessage = group.sendChat({ + const groupMessage = await group.sendChat({ message: `\`${shared.i18n.t('chatBossDamage', { username: user.profile.name, bossName: quest.boss.name('en'), userDamage: progress.up.toFixed(1), bossDamage: Math.abs(down).toFixed(1), }, user.preferences.language)}\``, @@ -1051,7 +1051,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) { if (quest.boss.rage) { group.quest.progress.rage += Math.abs(down); if (group.quest.progress.rage >= quest.boss.rage.value) { - const rageMessage = group.sendChat({ + const rageMessage = await group.sendChat({ message: quest.boss.rage.effect('en'), info: { type: 'boss_rage', @@ -1094,7 +1094,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) { // Boss slain, finish quest if (group.quest.progress.hp <= 0) { - const questFinishChat = group.sendChat({ + const questFinishChat = await group.sendChat({ message: `\`${shared.i18n.t('chatBossDefeated', { bossName: quest.boss.name('en') }, 'en')}\``, info: { type: 'boss_defeated', @@ -1148,7 +1148,7 @@ schema.methods._processCollectionQuest = async function processCollectionQuest ( }, []); foundText = foundText.join(', '); - const foundChat = group.sendChat({ + const foundChat = await group.sendChat({ message: `\`${shared.i18n.t('chatFindItems', { username: user.profile.name, items: foundText }, 'en')}\``, info: { type: 'user_found_items', @@ -1164,7 +1164,7 @@ schema.methods._processCollectionQuest = async function processCollectionQuest ( const questFinished = collectedItems.length === remainingItems.length; if (questFinished) { await group.finishQuest(quest); - const allItemsFoundChat = group.sendChat({ + const allItemsFoundChat = await group.sendChat({ message: `\`${shared.i18n.t('chatItemQuestFinish', 'en')}\``, info: { type: 'all_items_found', @@ -1236,7 +1236,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) { const chatPromises = []; if (tavern.quest.progress.hp <= 0) { - const completeChat = tavern.sendChat({ + const completeChat = await tavern.sendChat({ message: quest.completionChat('en'), info: { type: 'tavern_quest_completed', @@ -1273,7 +1273,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) { } if (!scene) { - const tiredChat = tavern.sendChat({ + const tiredChat = await tavern.sendChat({ message: `\`${shared.i18n.t('tavernBossTired', { rageName: quest.boss.rage.title('en'), bossName: quest.boss.name('en') }, 'en')}\``, info: { type: 'tavern_boss_rage_tired', @@ -1283,7 +1283,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) { chatPromises.push(tiredChat.save()); tavern.quest.progress.rage = 0; // quest.boss.rage.value; } else { - const rageChat = tavern.sendChat({ + const rageChat = await tavern.sendChat({ message: quest.boss.rage[scene]('en'), info: { type: 'tavern_boss_rage', @@ -1306,7 +1306,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) { && tavern.quest.progress.hp < quest.boss.desperation.threshold && !tavern.quest.extra.desperate ) { - const progressChat = tavern.sendChat({ + const progressChat = await tavern.sendChat({ message: quest.boss.desperation.text('en'), info: { type: 'tavern_boss_desperation', diff --git a/website/server/server.js b/website/server/server.js index c952da4b04..59f3125ea1 100644 --- a/website/server/server.js +++ b/website/server/server.js @@ -12,6 +12,7 @@ import attachMiddlewares from './middlewares/index'; // Load config files import './libs/setupMongoose'; import './libs/setupPassport'; +import './libs/setupFirebase'; // Load some schemas & models import './models/challenge';