Skip to content
This repository was archived by the owner on Sep 20, 2023. It is now read-only.

Commit

Permalink
Changes for admin tool (#4418)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahthepro authored Apr 1, 2020
1 parent 68476e3 commit b8308f7
Show file tree
Hide file tree
Showing 6 changed files with 423 additions and 42 deletions.
1 change: 1 addition & 0 deletions infra/notifications/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
firebase-service-account.json
7 changes: 1 addition & 6 deletions infra/notifications/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,14 +464,9 @@ app.post('/events', async (req, res) => {
})

app.post('/send_pn', pushAppAuth, async (req, res) => {
const { title, body, payload, address } = req.body

const mPush = new MobilePush(config)

const data = await mPush.sendToAddress(address, {
message: { title, body },
payload
})
const data = await mPush.multicastMessage(req.body)

res.send(data)
})
Expand Down
4 changes: 4 additions & 0 deletions infra/notifications/src/dupeTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ function getMessageFingerprint(messageObject) {
* @returns {Promise<*>}
*/
async function isNotificationDupe(messageFingerprint, config) {
if (process.env.NODE_ENV === 'test') {
return 0
}

return NotificationLog.count({
where: {
messageFingerprint: messageFingerprint,
Expand Down
276 changes: 240 additions & 36 deletions infra/notifications/src/mobilePush.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const apn = require('apn')
const firebase = require('firebase-admin')
const Sequelize = require('sequelize')
const web3Utils = require('web3-utils')

Expand All @@ -9,12 +8,16 @@ const { messageTemplates } = require('../templates/messageTemplates')
const { getNotificationMessage } = require('./notification')
const logger = require('./logger')

const chunk = require('lodash/chunk')

const {
getMessageFingerprint,
isNotificationDupe,
logNotificationSent
} = require('./dupeTools')

const firebaseMessaging = require('./utils/firebaseMessaging')

// Configure the APN provider
let apnProvider, apnBundle
if (process.env.APNS_KEY_FILE) {
Expand All @@ -35,26 +38,6 @@ if (process.env.APNS_KEY_FILE) {
logger.warn('APN provider not configured.')
}

// Firebase Admin SDK
// ref: https://firebase.google.com/docs/reference/admin/node/admin.messaging
let firebaseMessaging
if (process.env.FIREBASE_SERVICE_JSON) {
try {
const firebaseServiceJson = require(process.env.FIREBASE_SERVICE_JSON)

firebase.initializeApp({
credential: firebase.credential.cert(firebaseServiceJson),
databaseURL: process.env.FIREBASE_DB_URL
})

firebaseMessaging = firebase.messaging()
} catch (error) {
logger.error(`Error trying to configure firebaseMessaging: ${error}`)
}
} else {
logger.warn('Firebase messaging not configured.')
}

class MobilePush {
/**
* Constructor
Expand All @@ -64,6 +47,143 @@ class MobilePush {
this.config = config
}

/**
* Sends a mobile push notification to an Android or iOS device.
*
* @param {[{deviceToken:string, ethAddress:string}]} registry
* @param {string} deviceType: 'APN' or 'FCM'
* @param {{message: {title:string, body:string}, payload: Object}} notificationObj
* @param {string} messageHash: Optional. Hash of the origin message.
* @returns {Promise<void>}
* @private
*/
async _rawSend(registry, deviceType, notificationObj, messageHash) {
if (!notificationObj) {
throw new Error('Missing notificationObj')
}

if (!['APN', 'FCM'].includes(deviceType)) {
throw new Error(`Invalid device type ${deviceType}`)
}

const notificationObjAndHash = { ...notificationObj, messageHash }

const channel = deviceType === 'APN' ? 'mobile-ios' : 'mobile-android'

const filteredRegistry = registry
.map(data => {
const messageFingerprint = getMessageFingerprint({
...notificationObjAndHash,
channel,
ethAddress: data.ethAddress
})

return {
...data,
messageFingerprint
}
})
.filter(async data => {
const { ethAddress, messageFingerprint } = data

const dupeCount = await isNotificationDupe(
messageFingerprint,
this.config
)

if (dupeCount > 0) {
logger.warn(
`Duplicate. Notification already recently sent. Skipping.`,
ethAddress,
channel,
messageFingerprint
)
return false
}

return true
})

if (filteredRegistry.length === 0) {
logger.warn('No device tokens to send notifications')
return
}

const filteredTokens = filteredRegistry.map(data => data.deviceToken)

// Do not send notification during unit tests.
const isTest = process.env.NODE_ENV === 'test'
let success = isTest

if (deviceType === 'APN' && !isTest) {
if (!apnProvider) {
throw new Error('APN provider not configured, notification failed')
}

const notification = new apn.Notification({
alert: notificationObj.message,
sound: 'default',
payload: notificationObj.payload,
topic: apnBundle
})

try {
const response = await apnProvider.send(notification, filteredTokens)
// response.sent: Array of device tokens to which the notification was sent successfully
// response.failed: Array of objects containing the device token (`device`) and either
if (response.sent.length) {
success = true
logger.debug('APN sent')
} else {
logger.error('APN send failure:', response)
}
} catch (error) {
logger.error('APN send error: ', error)
}
} else if (deviceType === 'FCM' && !isTest) {
if (!firebaseMessaging) {
throw new Error(
'Firebase messaging not configured, notification failed'
)
}
// FCM notifications
// Message: https://firebase.google.com/docs/reference/admin/node/admin.messaging.Message
const message = {
android: {
priority: 'high',
notification: {
channelId: 'Dapp'
}
},
notification: {
...notificationObj.message
},
data: notificationObj.payload,
tokens: filteredTokens
}

try {
const response = await firebaseMessaging.send(message)
logger.debug('FCM message sent:', response)
success = true
} catch (error) {
logger.error('FCM message failed to send: ', error)
}
}

const promises = filteredRegistry.map(data => {
return logNotificationSent(
data.messageFingerprint,
data.ethAddress,
channel
)
})

if (success) {
await Promise.all(promises)
}
}

/**
* Sends a mobile push notification to an Android or iOS device.
*
Expand Down Expand Up @@ -195,11 +315,10 @@ class MobilePush {
}

for (const mobileRegister of mobileRegisters) {
await this._send(
mobileRegister.deviceToken,
await this._rawSend(
[{ deviceType: mobileRegister.deviceToken, ethAddress }],
mobileRegister.deviceType,
notificationObj,
ethAddress,
messageHash
)
}
Expand Down Expand Up @@ -365,18 +484,103 @@ class MobilePush {
}
}

/**
* Wrapper for _sendToEthAddress
* @param {String} ethAddress Target address
* @param {{
* message: { title, body },
* payload
* }} Object Notification object
*
* @retuns An object with `ok: true` on success or an error message otherwise
*/
async sendToAddress(ethAddress, notificationObj) {
return await this._sendToEthAddress(ethAddress, notificationObj, null)
async _sendInChunks(data, deviceType, mobileRegisters) {
if (mobileRegisters.length === 0) {
const errorMsg = `No device registered with notification enabled`
logger.info(errorMsg)
return {
error: errorMsg
}
}
const { title, body, payload } = data

const notificationObj = {
message: {
title,
body
},
payload
}

const targets = mobileRegisters.map(register => {
return {
ethAddress: register.ethAddress,
deviceToken: register.deviceToken
}
})

// Send in chunks of 100
const chunks = chunk(targets, 100)

try {
for (const chunk of chunks) {
await this._rawSend(chunk, deviceType, notificationObj)
}
} catch (error) {
logger.error('Failed to send', error)
return {
error: error.message
}
}
}

async _sendToAllDevices(data, deviceType) {
const mobileRegisters = await MobileRegistry.findAll({
where: {
deviceToken: { [Sequelize.Op.ne]: null },
deviceType,
deleted: false,
'permissions.alert': true
},
order: [['updatedAt', 'DESC']] // Most recently updated records first.
})

await this._sendInChunks(data, deviceType, mobileRegisters)
}

async _sendToManyAddresses(data, deviceType, addresses) {
const mobileRegisters = await MobileRegistry.findAll({
where: {
deviceToken: { [Sequelize.Op.ne]: null },
deviceType,
ethAddress: {
[Sequelize.Op.in]: addresses.map(a => a.toLowerCase())
},
deleted: false,
'permissions.alert': true
},
order: [['updatedAt', 'DESC']] // Most recently updated records first.
})

await this._sendInChunks(data, deviceType, mobileRegisters)
}

async multicastMessage(data) {
const {
target, // One of `all` or `address`
targetAddress // A single address or an array of target addresses
} = data

try {
if (target === 'all') {
await this._sendToAllDevices(data, 'APN')
await this._sendToAllDevices(data, 'FCM')
} else if (target === 'address') {
const addresses =
targetAddress instanceof Array ? targetAddress : [targetAddress]
await this._sendToManyAddresses(data, 'APN', addresses)
await this._sendToManyAddresses(data, 'FCM', addresses)
}

return {
ok: true
}
} catch (error) {
logger.error(error)
return {
ok: false
}
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions infra/notifications/src/utils/firebaseMessaging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const firebase = require('firebase-admin')
const logger = require('../logger')

// Firebase Admin SDK
// ref: https://firebase.google.com/docs/reference/admin/node/admin.messaging
let firebaseMessaging
if (process.env.FIREBASE_SERVICE_JSON) {
try {
const firebaseServiceJson = require(process.env.FIREBASE_SERVICE_JSON)

firebase.initializeApp({
credential: firebase.credential.cert(firebaseServiceJson),
databaseURL: process.env.FIREBASE_DB_URL
})

firebaseMessaging = firebase.messaging()
} catch (error) {
logger.error(`Error trying to configure firebaseMessaging: ${error}`)
}
} else {
logger.warn('Firebase messaging not configured.')
}

module.exports = firebaseMessaging
Loading

0 comments on commit b8308f7

Please sign in to comment.