diff --git a/.example.env b/.example.env index b73a7d54..60aa6c8d 100644 --- a/.example.env +++ b/.example.env @@ -127,7 +127,7 @@ RATE_LIMITER_MAX_OVERRIDE=50 PODPING_HIVE_ACCOUNT= PODPING_HIVE_POSTING_KEY= -FCM_GOOGLE_API_AUTH_TOKEN= +FCM_GOOGLE_API_PATH_TO_AUTH_JSON= MAINTENANCE_MODE_ENABLED= # lowercase true to enable MAINTENANCE_MODE_DOWNTIME_EXPECTED= # in minutes diff --git a/package.json b/package.json index 0d24041c..7767669b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "podverse-api", - "version": "4.16.21", + "version": "4.16.22", "description": "Data API, database migration scripts, and backend services for all Podverse models.", + "private": true, "contributors": [ "Mitch Downey" ], @@ -176,6 +177,7 @@ "eslint-plugin-react": "7.x", "eslint-plugin-react-hooks": "1.x", "follow-redirects": "^1.15.6", + "google-auth-library": "^9.14.1", "googleapis": "45.0.0", "http-errors": "1.7.3", "husky": "3.1.0", diff --git a/src/config/index.ts b/src/config/index.ts index 2de65c4b..f6383b2e 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -69,7 +69,7 @@ export interface Config { rateLimiterMaxOverride: any manticore: any twitterAPIBearerToken: string - fcmGoogleApiAuthToken: string + fcmGoogleApiPathToAuthJson: string minimumMobileVersion: string podping: { hiveAccount: string @@ -124,7 +124,7 @@ const podcastIndexBaseUrl = process.env.PODCAST_INDEX_BASE_URL || '' // default 1 hour (3600000 milliseconds) const podcastIndexRecentlyUpdatedSinceTime = process.env.PODCAST_INDEX_RECENTLY_UPDATED_SINCE_TIME || '3600000' // 1 hour const podcastIndexNewFeedsSinceTime = process.env.PODCAST_INDEX_NEW_FEEDS_SINCE_TIME || '43200000' // half a day -const fcmGoogleApiAuthToken = process.env.FCM_GOOGLE_API_AUTH_TOKEN || '' +const fcmGoogleApiPathToAuthJson = process.env.FCM_GOOGLE_PATH_TO_AUTH_JSON || '' const bitpayConfig = { apiKeyPath: process.env.BITPAY_API_KEY_PATH || '/', @@ -249,7 +249,7 @@ const config: Config = { }, twitterAPIBearerToken, minimumMobileVersion, - fcmGoogleApiAuthToken, + fcmGoogleApiPathToAuthJson, podping, maintenanceMode: { isEnabled: process.env.MAINTENANCE_MODE_ENABLED === 'true' || false, diff --git a/src/lib/notifications/fcmGoogleApi.ts b/src/lib/notifications/fcmGoogleApi.ts index e0ff642f..212c419e 100644 --- a/src/lib/notifications/fcmGoogleApi.ts +++ b/src/lib/notifications/fcmGoogleApi.ts @@ -1,11 +1,15 @@ /* eslint-disable @typescript-eslint/camelcase */ import { request } from '../request' -import { config } from '~/config' import { getFCMTokensForPodcastId } from '~/controllers/fcmDevice' +import { generateAccessToken } from './firebaseGenerateAccessToken' import { SendNotificationOptions } from './sendNotificationOptions' -const { fcmGoogleApiAuthToken } = config +import { config } from '~/config'; +const fs = require('fs'); -const fcmGoogleApiPath = 'https://fcm.googleapis.com/fcm/send' +const keyFilePath = config.fcmGoogleApiPathToAuthJson; +const key = JSON.parse(fs.readFileSync(keyFilePath, 'utf8')); + +const fcmGoogleApiPath = `https://fcm.googleapis.com/v1/projects/${key.project_id}/messages:send` export const sendFcmNewEpisodeDetectedNotification = async (options: SendNotificationOptions) => { const { podcastId, podcastShrunkImageUrl, podcastFullImageUrl, episodeFullImageUrl, episodeId } = options @@ -69,36 +73,25 @@ export const sendFCMGoogleApiNotification = async ( episodeImage?: string, episodeId?: string ) => { - if (!fcmTokens || fcmTokens.length === 0) return + const accessToken = await generateAccessToken() - const fcmTokenBatches: any[] = [] - const size = 1000 - for (let i = 0; i < fcmTokens.length; i += size) { - fcmTokenBatches.push(fcmTokens.slice(i, i + size)) - } - - for (const fcmTokenBatch of fcmTokenBatches) { - if (fcmTokenBatch?.length > 0) { - const imageUrl = episodeImage || podcastImage + if (!fcmTokens || fcmTokens.length === 0) return - try { - await request(fcmGoogleApiPath, { - method: 'POST', - headers: { - Authorization: `key=${fcmGoogleApiAuthToken}`, - 'Content-Type': 'application/json' - }, - body: { - registration_ids: fcmTokenBatch || [], + for (const fcmToken of fcmTokens) { + const imageUrl = episodeImage || podcastImage + try { + await request(fcmGoogleApiPath, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + }, + body: { + message: { + token: fcmToken, notification: { body, title, - podcastId, - episodeId, - podcastTitle: podcastTitle, - episodeTitle: episodeTitle, - notificationType, - timeSent: new Date(), image: imageUrl }, data: { @@ -131,12 +124,12 @@ export const sendFCMGoogleApiNotification = async ( image: imageUrl } } - }, - json: true - }) - } catch (error) { - console.log('sendFCMGoogleApiNotification error', error) - } + } + }, + json: true + }) + } catch (error) { + console.log('sendFCMGoogleApiNotification error', error) } } } diff --git a/src/lib/notifications/firebaseGenerateAccessToken.ts b/src/lib/notifications/firebaseGenerateAccessToken.ts new file mode 100644 index 00000000..0b736ffa --- /dev/null +++ b/src/lib/notifications/firebaseGenerateAccessToken.ts @@ -0,0 +1,18 @@ +import { config } from '~/config'; +import { JWT } from 'google-auth-library'; +const fs = require('fs'); + +const keyFilePath = config.fcmGoogleApiPathToAuthJson; +const key = JSON.parse(fs.readFileSync(keyFilePath, 'utf8')); + +const client = new JWT( + key.client_email, + '', + key.private_key, + ['https://www.googleapis.com/auth/firebase.messaging'], +); + +export async function generateAccessToken() { + const token = await client.authorize(); + return token.access_token; +} diff --git a/yarn.lock b/yarn.lock index 19460e8c..60e2d221 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4514,6 +4514,17 @@ gaxios@^2.0.1, gaxios@^2.1.0: is-stream "^2.0.0" node-fetch "^2.3.0" +gaxios@^6.0.0, gaxios@^6.1.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== + 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" + gcp-metadata@^3.4.0: version "3.5.0" resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-3.5.0.tgz#6d28343f65a6bbf8449886a0c0e4a71c77577055" @@ -4522,6 +4533,14 @@ gcp-metadata@^3.4.0: gaxios "^2.1.0" json-bigint "^0.3.0" +gcp-metadata@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.0.tgz#9b0dd2b2445258e7597f2024332d20611cbd6b8c" + integrity sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg== + dependencies: + gaxios "^6.0.0" + json-bigint "^1.0.0" + generic-pool@3.8.2: version "3.8.2" resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9" @@ -4732,6 +4751,18 @@ google-auth-library@^5.2.0, google-auth-library@^5.6.1: jws "^4.0.0" lru-cache "^5.0.0" +google-auth-library@^9.14.1: + version "9.14.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.14.1.tgz#4c6f535f474b01847ea1a60ef1d56dbd6a0aad2f" + integrity sha512-Rj+PMjoNFGFTmtItH7gHfbHpGVSb3vmnGK3nwNBqxQF9NoBpttSZI/rc0WiM63ma2uGDQtYEkMHkK9U6937NiA== + 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" + google-p12-pem@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-2.0.5.tgz#b1c44164d567ae894f7a19b4ff362a06be5b793b" @@ -4796,6 +4827,14 @@ gtoken@^4.1.0: jws "^4.0.0" mime "^2.2.0" +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== + dependencies: + gaxios "^6.0.0" + jws "^4.0.0" + handlebars@^4.4.3: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" @@ -5121,6 +5160,14 @@ https-proxy-agent@^7.0.0: agent-base "^7.0.2" debug "4" +https-proxy-agent@^7.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -6301,6 +6348,13 @@ json-bigint@^0.3.0: dependencies: bignumber.js "^9.0.0" +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -7412,6 +7466,13 @@ node-fetch@2.6.7, node-fetch@^2.3.0, node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.9: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"