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 gcmConfig = { ...cfg.gcm, id: cfg.gcm?.id || process.env.JUICK_GCM_ID }; const push = new PushNotifications({ ...config, apn: apnConfig(true), gcm: gcmConfig, }); const sandbox = new PushNotifications({ ...config, apn: apnConfig(false), gcm: gcmConfig }); /** @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 => { log(`Response message: ${JSON.stringify(r.message)}`); let badTokens = r.message.filter(m => m.errorMsg === 'BadDeviceToken').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 === 'gcm') .forEach(async r => { let badTokens = r.message.filter(m => m.errorMsg === 'NotRegistered' || m.errorMsg === 'MismatchSenderId' || m.errorMsg === 'InvalidRegistration').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 }; let { tokens, ...subscriber } = user; 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; template.color = '#3c77aa'; template.icon = 'ic_notification'; template.clickAction = 'com.juick.NEW_EVENT_ACTION'; const tag = msg.mid == 0 ? msg.user.uname : msg.mid; template.tag = `${tag}`; template.android_channel_id = 'default'; } return template; }