/*
* Author: Vlad Seryakov vseryakov@gmail.com
* backendjs 2018
*/
'use strict';
const { EventEmitter } = require("node:events");
const logger = require(__dirname + '/logger');
const lib = require(__dirname + '/lib');
/**
* Messaging and push notifications for mobile and other clients, supports Webpush push notifications.
*
* Emits a signal **uninstall(client, device_id, user_id)** on device invalidation or if a device token is invalid as
* reported by the server, user_id may not be available.
*
* @module push
*/
/**
* @property {ConfigOptions[]}
* @default
*/
const args = [
{ name: "config", obj: "_config", type: "json", merge: 1, descr: "An object with client configs by type", example: '-push-config {"webpush":{"type":"webpush",key":XXX","pubkey":"XXXX"}}' },
{ name: "([a-z0-9]+)", obj: "_config.$1", type: "map", merge: 1, descr: "A single client type parameters", example: "-push-webpush type:webpush,key:K,pubkey:PK,email:XXX" },
];
class Push extends EventEmitter {
constructor() {
super();
this.name = "push";
this.args = args;
/** @var {object} - push modules by type */
this.modules = {};
/** @var {object} - push live clients by type */
this.clients = {};
this._config = {};
}
/**
* Perform initial setup by calling static configure method for each module
* @param {object|object[]} options - whole config object for the type
* @memberof module:push
* @method configureModule
*/
configureModule(options, callback)
{
for (const type in this._config) {
try {
let Mod = this.modules[type];
if (!Mod) {
Mod = this.modules[type] = require(__dirname + "/push/" + type);
}
if (!Mod) continue;
lib.call(Mod.configure, this._config[type]);
} catch (e) {
logger.error("configureModule:", this.name, options, e.stack);
}
}
callback();
}
/**
* Initialize supported notification services, it supports jobs arguments convention so can be used in the jobs that
* need to send push notifications in the worker process. Can be called many times, only new modules will be initialized.
* @param {object} options
* @param {function} [callback]
* @memberof module:push
* @method init
*/
init(_options, callback)
{
if (typeof _options === "function") callback = _options;
logger.debug("init:", this.name);
for (const type in this._config) {
if (!type || this.clients[type]) continue;
const Mod = this.modules[type];
if (!Mod) continue;
this.clients[type] = new Mod(this._config[type]);
}
this.emit("init");
if (typeof callback === "function") callback();
}
/**
* Shutdown notification services, may wait till all pending messages are sent before calling the callback
* @param {object} options
* @param {function} [callback]
* @memberof module:push
* @method shutdown
*/
shutdown(_options, callback)
{
this.emit("shutdown");
logger.debug("shutdown:", this.name);
lib.forEach(Object.keys(this.clients), (type, next) => {
const client = this.clients[type];
delete this.clients[type];
if (!lib.isFunc(client.close)) return next();
client.close(next);
}, callback);
}
/**
* Deliver a notification for the given device token(s).
*
* @param {string} device - where to send the message to, can be multiple ids separated by , or |.
* @param {object} options - see each driver for specific properties,
* {@link module:push.WebPushClient}, {@link module:push.APNPushClient}, {@link module:push.FCMPushClient},
* {@link module:push.SNSPushClient},
* @param {string} options.msg - message body
* @param {string} [options.title] - message title
* @param {string} [options.app_id] - send to the devices for the given app only
* @param {function} callback
* @memberof module:push
* @method send
*/
send(device, options, callback)
{
// Collect all device tokens per app
var devs = lib.split(device, null, "string").map((x) => {
var dev = this.parseDevice(x);
if (!dev.token) return 0;
if (options?.app_id && dev.app !== options.app_id) return 0;
return dev;
}).filter(x => x);
logger.debug("send:", this.name, device, options, "DEV:", devs);
lib.forEach(devs, (dev, next) => {
var client = this.clients[dev.type];
if (!client) {
logger.error("send:", "unsupported:", dev);
return next();
}
client.send(dev, options, (err) => {
if (err) logger.error("send:", this.name, dev, err);
next();
});
}, callback);
}
/**
* Async version of {@link module:push.send}
* @param {string} device - where to send the message to, can be multiple ids separated by , or |.
* @param {object} options
* @returns {Promise} as object { err }
* @async
* @memberof module:push
* @method asend
*
*/
asend(device, options)
{
return new Promise((resolve, _reject) => {
this.send(device, options, (err) => {
resolve({ err });
})
});
}
/**
* Parse device URN and returns an object with all parts into separate properties. A device URN can be in the following format:
* @param {string} device - [type://]token[@app]
* - type - apn, fcm, webpush, sns, ...
* - token - device token
* - app - optional application name or bundle
* @memberof module:push
* @method parseDevice
*/
parseDevice(device)
{
var dev = { token: "", type: "", app: "" };
if (typeof device !== "string") return dev;
const d = device.match(/^([a-z]+:\/\/)([^@]+)?@?([a-z0-9._-]+)?/i);
if (d) {
if (d[2] && d[2] !== "undefined") dev.token = d[2];
if (d[1]) dev.type = d[1].slice(0, -3);
if (d[3]) dev.app = d[3];
}
return dev;
}
}
module.exports = new Push();