Prepare for actionable notifications
This commit is contained in:
parent
e231037f7f
commit
41a538259c
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const worker = require('./worker');
|
||||||
|
|
||||||
const functions = require('firebase-functions');
|
const functions = require('firebase-functions');
|
||||||
const admin = require('firebase-admin');
|
const admin = require('firebase-admin');
|
||||||
|
|
||||||
@ -7,49 +9,34 @@ admin.initializeApp();
|
|||||||
|
|
||||||
var db = admin.firestore();
|
var db = admin.firestore();
|
||||||
|
|
||||||
const MAX_NOTIFICATIONS_PER_DAY = 50;
|
const MAX_NOTIFICATIONS_PER_DAY = 100;
|
||||||
|
|
||||||
exports.sendPushNotification = functions.https.onRequest(async (req, res) => {
|
exports.sendPushNotification = functions.https.onRequest(async (req, res) => {
|
||||||
|
return handleRequest(req, res, worker.createLegacyPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.pushNotifyV2 = functions.https.onRequest(async (req, res) => {
|
||||||
|
return handleRequest(req, res, worker.createPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleRequest(req, res, payloadHandler) {
|
||||||
if (debug()) console.log('Received payload', req.body);
|
if (debug()) console.log('Received payload', req.body);
|
||||||
var currentRateLimitDocName = getCurrentRateLimitsDocName();
|
var today = getToday();
|
||||||
var token = req.body.push_token;
|
var token = req.body.push_token;
|
||||||
var ref = db.collection('rateLimits').doc(currentRateLimitDocName).collection('tokens').doc(token);
|
if (!token) {
|
||||||
|
return res.status(403).send({ 'errorMessage': 'Missed token' });
|
||||||
var payload = {
|
|
||||||
notification: {
|
|
||||||
body: req.body.message
|
|
||||||
},
|
|
||||||
android: {
|
|
||||||
priority: 'HIGH',
|
|
||||||
notification: {
|
|
||||||
sound: 'default',
|
|
||||||
icon: 'mini_icon',
|
|
||||||
channel_id: 'ha_notify'
|
|
||||||
}
|
}
|
||||||
},
|
if (token.indexOf(':') === -1) {
|
||||||
token: token,
|
return res.status(403).send({'errorMessage': 'Invalid token'});
|
||||||
};
|
|
||||||
|
|
||||||
if(req.body.title) {
|
|
||||||
payload.notification.title = req.body.title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(req.body.data) {
|
var workerResult = payloadHandler(req);
|
||||||
if(req.body.data.android) {
|
var updateRateLimits = workerResult.updateRateLimits;
|
||||||
payload.android = req.body.data.android;
|
var payload = workerResult.payload;
|
||||||
}
|
|
||||||
if(req.body.data.apns) {
|
|
||||||
payload.apns = req.body.data.apns;
|
|
||||||
}
|
|
||||||
if(req.body.data.data) {
|
|
||||||
payload.data = req.body.data.data;
|
|
||||||
}
|
|
||||||
if(req.body.data.webpush) {
|
|
||||||
payload.webpush = req.body.data.webpush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug()) console.log('Notification payload', JSON.stringify(payload));
|
payload['token'] = token;
|
||||||
|
|
||||||
|
var ref = db.collection('rateLimitsV2').doc(today).collection('tokens').doc(token);
|
||||||
|
|
||||||
var docExists = false;
|
var docExists = false;
|
||||||
var docData = {
|
var docData = {
|
||||||
@ -59,20 +46,20 @@ exports.sendPushNotification = functions.https.onRequest(async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let currentDoc = await ref.get();
|
var currentDoc = await ref.get();
|
||||||
docExists = currentDoc.exists;
|
docExists = currentDoc.exists;
|
||||||
if(currentDoc.exists) {
|
if (currentDoc.exists) {
|
||||||
docData = currentDoc.data();
|
docData = currentDoc.data();
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error('Error getting document!', err);
|
console.error('Error getting document', err);
|
||||||
return handleError(res, 'getDoc', err);
|
return handleError(res, 'getDoc', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(docData.deliveredCount > MAX_NOTIFICATIONS_PER_DAY) {
|
if (updateRateLimits && docData.deliveredCount > MAX_NOTIFICATIONS_PER_DAY) {
|
||||||
return res.status(429).send({
|
return res.status(429).send({
|
||||||
errorType: 'RateLimited',
|
errorType: 'RateLimited',
|
||||||
message: 'The given target has reached the maximum number of notifications allowed per day. Please try again later.',
|
message: 'You have exited the maximum number of notifications allowed per day. Please try again later.',
|
||||||
target: token,
|
target: token,
|
||||||
rateLimits: getRateLimitsObject(docData),
|
rateLimits: getRateLimitsObject(docData),
|
||||||
});
|
});
|
||||||
@ -80,19 +67,25 @@ exports.sendPushNotification = functions.https.onRequest(async (req, res) => {
|
|||||||
|
|
||||||
docData.totalCount = docData.totalCount + 1;
|
docData.totalCount = docData.totalCount + 1;
|
||||||
|
|
||||||
|
if (debug) console.log('Sending payload', JSON.stringify(payload));
|
||||||
|
|
||||||
var messageId;
|
var messageId;
|
||||||
try {
|
try {
|
||||||
messageId = await admin.messaging().send(payload);
|
messageId = await admin.messaging().send(payload);
|
||||||
docData.deliveredCount = docData.deliveredCount + 1;
|
docData.deliveredCount = docData.deliveredCount + 1;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
docData.errorCount = docData.errorCount + 1;
|
docData.errorCount = docData.errorCount + 1;
|
||||||
await setRateLimitDoc(ref, docExists, docData, res);
|
await writeRateLimits(ref, docExists, docData, res);
|
||||||
return handleError(res, 'sendNotification', err);
|
return handleError(res, payload, 'sendNotification', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug()) console.log('Successfully sent message:', messageId);
|
if (debug()) console.log('Successfully sent message:', messageId);
|
||||||
|
|
||||||
await setRateLimitDoc(ref, docExists, docData, res);
|
if (updateRateLimits) {
|
||||||
|
await writeRateLimits(ref, docExists, docData, res);
|
||||||
|
} else {
|
||||||
|
if (debug) console.log('Not updating rate limits because notification is critical or command');
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(201).send({
|
return res.status(201).send({
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
@ -101,31 +94,35 @@ exports.sendPushNotification = functions.https.onRequest(async (req, res) => {
|
|||||||
rateLimits: getRateLimitsObject(docData),
|
rateLimits: getRateLimitsObject(docData),
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
};
|
||||||
|
|
||||||
async function setRateLimitDoc(ref, docExists, docData, res) {
|
async function writeRateLimits(ref, docExists, docData, res) {
|
||||||
try {
|
try {
|
||||||
if(docExists) {
|
if (docExists) {
|
||||||
if (debug()) console.log('Updating existing doc!');
|
if (debug()) console.log('Updating existing doc');
|
||||||
await ref.update(docData);
|
await ref.update(docData);
|
||||||
} else {
|
} else {
|
||||||
if (debug()) console.log('Creating new doc!');
|
if (debug()) console.log('Creating new doc');
|
||||||
await ref.set(docData);
|
await ref.set(docData);
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
if(docExists) {
|
if (docExists) {
|
||||||
console.error('Error updating document!', err);
|
console.error('Error updating document', err);
|
||||||
} else {
|
} else {
|
||||||
console.error('Error creating document!', err);
|
console.error('Error creating document', err);
|
||||||
}
|
}
|
||||||
return handleError(res, 'setDocument', err);
|
return handleError(res, 'setDocument', err);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleError(res, step, incomingError) {
|
function handleError(res, payload, step, incomingError) {
|
||||||
if (!incomingError) return null;
|
if (!incomingError) return null;
|
||||||
|
if (payload) {
|
||||||
|
console.error('InternalError during', step, 'with payload', JSON.stringify(payload), incomingError);
|
||||||
|
} else {
|
||||||
console.error('InternalError during', step, incomingError);
|
console.error('InternalError during', step, incomingError);
|
||||||
|
}
|
||||||
return res.status(500).send({
|
return res.status(500).send({
|
||||||
errorType: 'InternalError',
|
errorType: 'InternalError',
|
||||||
errorStep: step,
|
errorStep: step,
|
||||||
@ -133,7 +130,7 @@ function handleError(res, step, incomingError) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentRateLimitsDocName() {
|
function getToday() {
|
||||||
var today = new Date();
|
var today = new Date();
|
||||||
var dd = String(today.getDate()).padStart(2, '0');
|
var dd = String(today.getDate()).padStart(2, '0');
|
||||||
var mm = String(today.getMonth() + 1).padStart(2, '0');
|
var mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||||
@ -143,12 +140,14 @@ function getCurrentRateLimitsDocName() {
|
|||||||
|
|
||||||
function getRateLimitsObject(doc) {
|
function getRateLimitsObject(doc) {
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
|
var remainingCount = (MAX_NOTIFICATIONS_PER_DAY - doc.deliveredCount);
|
||||||
|
if (remainingCount === -1) remainingCount = 0;
|
||||||
return {
|
return {
|
||||||
successful: (doc.deliveredCount || 0),
|
successful: (doc.deliveredCount || 0),
|
||||||
errors: (doc.errorCount || 0),
|
errors: (doc.errorCount || 0),
|
||||||
total: (doc.totalCount || 0),
|
total: (doc.totalCount || 0),
|
||||||
maximum: MAX_NOTIFICATIONS_PER_DAY,
|
maximum: MAX_NOTIFICATIONS_PER_DAY,
|
||||||
remaining: (MAX_NOTIFICATIONS_PER_DAY - doc.deliveredCount),
|
remaining: remainingCount,
|
||||||
resetsAt: new Date(d.getFullYear(), d.getMonth(), d.getDate()+1)
|
resetsAt: new Date(d.getFullYear(), d.getMonth(), d.getDate()+1)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
createPayload: function createPayload(req) {
|
|
||||||
let payload = {
|
|
||||||
android: {},
|
|
||||||
data: {},
|
|
||||||
fcm_options: {
|
|
||||||
analytics_label: "androidV1Notification"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let updateRateLimits = true;
|
|
||||||
|
|
||||||
if(req.body.data){
|
|
||||||
|
|
||||||
// Handle the web actions by changing them into a format the app can handle
|
|
||||||
// https://www.home-assistant.io/integrations/html5/#actions
|
|
||||||
if(req.body.data.actions) {
|
|
||||||
for (let i = 0; i < req.body.data.actions.length; i++) {
|
|
||||||
const action = req.body.data.actions[i];
|
|
||||||
if(action.action){
|
|
||||||
payload.data["action_"+(i+1)+"_key"] = action.action
|
|
||||||
}
|
|
||||||
if(action.title) {
|
|
||||||
payload.data["action_"+(i+1)+"_title"] = action.title
|
|
||||||
}
|
|
||||||
if(action.uri){
|
|
||||||
payload.data["action_"+(i+1)+"_uri"] = action.uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow setting of ttl
|
|
||||||
// https://firebase.google.com/docs/reference/admin/node/admin.messaging.AndroidConfig.html#optional-ttl
|
|
||||||
if(req.body.data.ttl){
|
|
||||||
payload.android.ttl = req.body.data.ttl
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://firebase.google.com/docs/reference/admin/node/admin.messaging.AndroidConfig.html#optional-priority
|
|
||||||
if(req.body.data.priority){
|
|
||||||
payload.android.priority = req.body.data.priority
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://firebase.google.com/docs/reference/admin/node/admin.messaging.AndroidNotification.html
|
|
||||||
for (const key of [
|
|
||||||
'icon', 'color', 'sound', 'tag', 'clickAction',
|
|
||||||
'bodyLocKey', 'bodyLocArgs', 'titleLocKey', 'titleLocArgs', 'channelId',
|
|
||||||
'ticker', 'sticky', 'eventTime', 'localOnly', 'notificationPriority',
|
|
||||||
'defaultSound', 'defaultVibrateTimings', 'defaultLightSettings', 'vibrateTimings',
|
|
||||||
'visibility', 'notificationCount', 'lightSettings', 'image'
|
|
||||||
]) {
|
|
||||||
if(req.body.data[key]){
|
|
||||||
payload.data[key] = String(req.body.data[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always put message, title, and image in data so that the application can handle creating
|
|
||||||
// the notifications. This allows us to safely create actionable/imaged notifications.
|
|
||||||
if(req.body.message) {
|
|
||||||
payload.data.message = req.body.message
|
|
||||||
if(req.body.message in ['request_location_update', 'clear_notification']) {
|
|
||||||
updateRateLimits = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(req.body.title) {
|
|
||||||
payload.data.title = req.body.title
|
|
||||||
}
|
|
||||||
|
|
||||||
return { updateRateLimits: updateRateLimits, payload: payload };
|
|
||||||
}
|
|
||||||
}
|
|
111
functions/worker.js
Normal file
111
functions/worker.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
module.exports = {
|
||||||
|
|
||||||
|
createLegacyPayload: function createLegacyPayload(req) {
|
||||||
|
var payload = {
|
||||||
|
notification: {
|
||||||
|
body: req.body.message
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
priority: 'HIGH',
|
||||||
|
notification: {
|
||||||
|
sound: 'default',
|
||||||
|
icon: 'mini_icon',
|
||||||
|
channel_id: 'ha_notify'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
token: token,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(req.body.title) {
|
||||||
|
payload.notification.title = req.body.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(req.body.data) {
|
||||||
|
if(req.body.data.android) {
|
||||||
|
payload.android = req.body.data.android;
|
||||||
|
}
|
||||||
|
if(req.body.data.apns) {
|
||||||
|
payload.apns = req.body.data.apns;
|
||||||
|
}
|
||||||
|
if(req.body.data.data) {
|
||||||
|
payload.data = req.body.data.data;
|
||||||
|
}
|
||||||
|
if(req.body.data.webpush) {
|
||||||
|
payload.webpush = req.body.data.webpush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug()) console.log('Notification payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
return { updateRateLimits: true, payload: payload };
|
||||||
|
},
|
||||||
|
|
||||||
|
createPayload: function createPayload(req) {
|
||||||
|
let payload = {
|
||||||
|
android: {},
|
||||||
|
data: {},
|
||||||
|
fcm_options: {
|
||||||
|
analytics_label: "androidV1Notification"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let updateRateLimits = true;
|
||||||
|
|
||||||
|
if(req.body.data){
|
||||||
|
|
||||||
|
// Handle the web actions by changing them into a format the app can handle
|
||||||
|
// https://www.home-assistant.io/integrations/html5/#actions
|
||||||
|
if(req.body.data.actions) {
|
||||||
|
for (let i = 0; i < req.body.data.actions.length; i++) {
|
||||||
|
const action = req.body.data.actions[i];
|
||||||
|
if(action.action){
|
||||||
|
payload.data["action_"+(i+1)+"_key"] = action.action
|
||||||
|
}
|
||||||
|
if(action.title) {
|
||||||
|
payload.data["action_"+(i+1)+"_title"] = action.title
|
||||||
|
}
|
||||||
|
if(action.uri){
|
||||||
|
payload.data["action_"+(i+1)+"_uri"] = action.uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow setting of ttl
|
||||||
|
// https://firebase.google.com/docs/reference/admin/node/admin.messaging.AndroidConfig.html#optional-ttl
|
||||||
|
if(req.body.data.ttl){
|
||||||
|
payload.android.ttl = req.body.data.ttl
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://firebase.google.com/docs/reference/admin/node/admin.messaging.AndroidConfig.html#optional-priority
|
||||||
|
if(req.body.data.priority){
|
||||||
|
payload.android.priority = req.body.data.priority
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://firebase.google.com/docs/reference/admin/node/admin.messaging.AndroidNotification.html
|
||||||
|
for (const key of [
|
||||||
|
'icon', 'color', 'sound', 'tag', 'clickAction',
|
||||||
|
'bodyLocKey', 'bodyLocArgs', 'titleLocKey', 'titleLocArgs', 'channelId',
|
||||||
|
'ticker', 'sticky', 'eventTime', 'localOnly', 'notificationPriority',
|
||||||
|
'defaultSound', 'defaultVibrateTimings', 'defaultLightSettings', 'vibrateTimings',
|
||||||
|
'visibility', 'notificationCount', 'lightSettings', 'image'
|
||||||
|
]) {
|
||||||
|
if(req.body.data[key]){
|
||||||
|
payload.data[key] = String(req.body.data[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always put message, title, and image in data so that the application can handle creating
|
||||||
|
// the notifications. This allows us to safely create actionable/imaged notifications.
|
||||||
|
if(req.body.message) {
|
||||||
|
payload.data.message = req.body.message
|
||||||
|
if(req.body.message in ['request_location_update', 'clear_notification']) {
|
||||||
|
updateRateLimits = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(req.body.title) {
|
||||||
|
payload.data.title = req.body.title
|
||||||
|
}
|
||||||
|
|
||||||
|
return { updateRateLimits: updateRateLimits, payload: payload };
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user