import PushNotifications from 'node-pushnotifications' import debug from 'debug' const log = debug('sender') import { deleteSubscribers } from './http' import { formatMessage, formatTitle, formatQuote } from './common/MessageUtils' import config from 'config' let cfg = /** @type { import('node-pushnotifications').Settings } */ (config) const apnConfig = (production = true) => { const apn = { ...cfg.apn, token: { key: cfg.apn?.token?.key || process.env.JUICK_APN_KEY, keyId: cfg.apn?.token?.keyId || process.env.JUICK_APN_KEY_ID, teamId: cfg.apn?.token?.teamId || process.env.JUICK_APN_TEAM_ID }, production: production } return apn } const fcmConfig = { ...cfg.fcm, serviceAccountKey: process.env.JUICK_FCM_SERVICE_ACCOUNT_FILE } log(`fcm config: ${JSON.stringify(fcmConfig)}`) const push = new PushNotifications({ ...config, apn: apnConfig(true), fcm: fcmConfig, }) const sandbox = new PushNotifications({ ...config, apn: apnConfig(false), fcm: fcmConfig }) /** @type {string} */ const application = config.get('service.application') || process.env.JUICK_APN_APPLICATION || '' /** * send notification * @param {PushNotifications.RegistrationId[]} productionIds * @param {PushNotifications.RegistrationId[]} sandboxIds * @param {PushNotifications.Data} data */ export function sendNotification(productionIds, sandboxIds, data) { [productionIds, sandboxIds].map((registrationIds, index) => { let sender = index == 0 ? push : sandbox if (registrationIds && registrationIds.length) { sender.send(registrationIds, data) .then((results) => { results.forEach(result => { log(`${result.method}: ${result.success} success, ${result.failure} failure`) if (result.failure) { console.error(`${result.method} failure: ${JSON.stringify(result)}`) console.error(`Failed data: ${JSON.stringify(data)}`) } }) results.filter(r => r.method === 'apn') .forEach(async r => { let badTokens = r.message.filter(m => m.errorMsg === 'BadDeviceToken' || m.errorMsg === 'Unregistered').map(m => { return { 'type': 'apns', 'token': m.regId } }) if (badTokens.length > 0) { await deleteSubscribers(badTokens) log(`${badTokens.length} APNS tokens deleted`) } }) results.filter(r => r.method === 'fcm') .forEach(async r => { let badTokens = r.message.filter(m => m.errorMsg === 'NotRegistered' || m.errorMsg === 'MismatchSenderId' || m.errorMsg === 'InvalidRegistration' || m.errorMsg === 'Requested entity was not found.').map(m => { return { 'type': 'fcm', 'token': m.regId } }) if (badTokens.length > 0) { await deleteSubscribers(badTokens) log(`${badTokens.length} GCM tokens deleted`) } }) results.filter(r => r.method === 'mpns') .forEach(async r => { let badTokens = r.message.filter(m => m.errorMsg === 'The channel expired.').map(m => { return { 'type': 'mpns', 'token': m.regId } }) if (badTokens.length > 0) { await deleteSubscribers(badTokens) log(`${badTokens.length} MPNS tokens deleted`) } }) results.filter(r => r.method === 'webPush') .forEach(async r => { let badTokens = r.message.filter(m => m.error && m.error['statusCode'] === 410).map(m => { return { 'type': 'web', 'token': JSON.stringify(m.regId) } }) if (badTokens.length > 0) { await deleteSubscribers(badTokens) log(`${badTokens.length} WebPush tokens deleted`) } }) }) .catch((err) => { console.error(JSON.stringify(err)) }) } }) } /** * builds notification object * @param {import('../client').SecureUser} user user * @param {import('../client').Message} msg message * @returns {PushNotifications.Data} notification template */ export function buildNotification(user, msg) { let template = { topic: application, custom: { message: msg }, timeToLive: 0 } const subscriber = { ...user } delete subscriber.tokens if (msg.service) { template.contentAvailable = true template.custom.service = true template.custom.user = subscriber } else { const avatar = `https://i.juick.com/a/${msg.user.uid}.png` const title = formatTitle(msg) const body = `${formatQuote(msg)}\n${formatMessage(msg)}` template.custom.mid = msg.mid template.custom.rid = msg.rid template.custom.uname = msg.user.uname template.custom.avatarUrl = avatar template.image1src = avatar template.text1 = title template.text2 = body template.title = title template.body = body //template.badge = user.unreadCount || 0 template.mutableContent = 1 const tag = msg.mid == 0 ? msg.user.uname : msg.mid template.tag = `${tag}` template.fcm_notification = { title: title, body: body, channel_id: 'default', click_action: 'com.juick.NEW_EVENT_ACTION', icon: 'ic_notification', color: '#3c77aa' } } return template }