notifx
Version:
Pluggable notification dispatcher to send notifications to various channels.
287 lines (273 loc) • 11.9 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var nodemailer = require('nodemailer');
var handlebars = require('handlebars');
var promises = require('fs/promises');
var axios = require('axios');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* NotifX notification manager.
*
* Responsible for:
* - Registering notification channels (e.g., email, SMS, Telegram)
* - Registering named notifications using those channels
* - Dispatching notifications dynamically with support for sync/async channels
*/
class Notifx {
constructor() {
/**
* Registered dispatch channels, indexed by name.
* @private
*/
this._channels = {};
/**
* Registered notifications, each mapped to a channel and its arguments.
* @private
*/
this._notifications = {};
}
/**
* Registers a named dispatch channel.
*
* The dispatcher function must have a fixed signature, meaning:
* - It must accept the same number and order of arguments as those provided when registering notifications for this channel.
* - It must be a function that is either synchronous or asynchronous.
*
* @param {string} name - Unique name for the channel.
* @param {Dispatcher} dispatcher - The dispatcher function to call when sending.
* @throws {TypeError} If the name is not a string or dispatcher is not a function.
*/
registerChannel(name, dispatcher) {
if (typeof name !== "string") {
throw new TypeError("Channel name must be a string");
}
if (typeof dispatcher !== "function") {
throw new TypeError("Channel dispatcher must be a function");
}
if (!this._channels.hasOwnProperty(name)) {
this._channels[name] = dispatcher;
}
}
/**
* Registers a named notification to be sent through a channel.
*
* The arguments must match the expected signature of the channel's dispatcher function.
*
* @param {string} notificationName - Unique name for this notification.
* @param {string} channelName - Name of the registered channel to use.
* @param {...any[]} args - Arguments that will be passed to the dispatcher when the notification is sent.
* @throws {TypeError} If names are not strings.
* @throws {Error} If the channel is not registered.
*/
registerNotification(notificationName, channelName, args) {
if (typeof notificationName !== "string" ||
typeof channelName !== "string") {
throw new TypeError("Notification or channel name must be a string");
}
if (!this._channels.hasOwnProperty(channelName)) {
throw new Error(`Channel "${channelName}" is not registered`);
}
if (!this._notifications.hasOwnProperty(notificationName)) {
this._notifications[notificationName] = {
channel: channelName,
args,
};
}
}
/**
* Sends a previously registered notification via its associated channel by invoking the channel's dispatcher function.
*
* The dispatcher will be called with the same arguments provided when registering the notification,
* unless additional arguments are provided at send-time.
*
* @param {string} notificationName - Name of the registered notification.
* @param {...any[]} args - Optional override of arguments to pass to the dispatcher.
* @returns {Promise<any>} The return value from the dispatcher function.
* @throws {Error} If notification or channel is missing, or argument count mismatches.
*/
send(notificationName, ...args) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._notifications.hasOwnProperty(notificationName)) {
throw new Error(`Notification "${notificationName}" is not registered`);
}
const notif = this._notifications[notificationName];
const channel = notif.channel;
const dispatch = this._channels[channel];
if (typeof dispatch !== "function") {
throw new TypeError(`Channel "${channel}" is not a function`);
}
// Get the arguments to pass to the dispatcher.
const funcArgs = args.length > 0 ? args : notif.args;
if (dispatch.length !== funcArgs.length) {
throw new Error(`Channel "${channel}" expects ${dispatch.length} arguments but got ${funcArgs.length}`);
}
try {
// Call the dispatcher with the provided arguments.
const result = dispatch(...funcArgs);
return result && typeof result.then === "function"
? yield result
: result;
}
catch (err) {
throw err;
}
});
}
}
const notifx = new Notifx();
function isFilePath(str) {
return __awaiter(this, void 0, void 0, function* () {
try {
const fileStat = yield promises.stat(str);
return fileStat.isFile();
}
catch (_a) {
return false;
}
});
}
function readFile(path) {
return __awaiter(this, void 0, void 0, function* () {
return promises.readFile(path, { encoding: "utf-8" });
});
}
/**
* Creates a nodemailer transporter.
* @param {object} options - Nodemailer transport configuration object.
* @returns {nodemailer.Transporter} A nodemailer transporter.
*/
const setTransporter = (options) => {
return nodemailer.createTransport(options);
};
/**
* Email plugin.
*/
const emailPlugin = {
/**
* Creates an email sender function that can be used as a notifx channel dispatcher.
*
* @param {object} mailerOptions - Nodemailer transport configuration (SMTP options).
* @returns {Function} An async function that sends an email using provided parameters.
*
* @example
* const send = emailPlugin.sendEmail(smtpConfig);
* await send("template.html", "from@example.com", "to@example.com", "Hello {{username}}", {
* body: { username: "jonhdoe" },
* title: { username: "jonhdoe" },
* });
*/
sendEmail: (mailerOptions) =>
/**
* Sends an email with HTML body and subject, using handlebars templates.
*
* @param {string} body - HTML body content or file path to HTML template.
* @param {string} from - Sender email address.
* @param {string} to - Recipient email address.
* @param {string} title - Subject line template (can contain handlebars variables).
* @param {{ body: Object, title: Object }} replacements - Data for handlebars templates.
* @param {Array} [attachments=[]] - Optional list of email attachments.
* @returns {Promise<any>} Resolves with the Nodemailer `info` object.
* @throws Will throw an error if email sending fails.
*/
(body_1, from_1, to_1, title_1, replacements_1, ...args_1) => __awaiter(void 0, [body_1, from_1, to_1, title_1, replacements_1, ...args_1], void 0, function* (body, from, to, title, replacements, attachments = []) {
const transporter = setTransporter(mailerOptions);
// Check if body is a file path and read it if necessary
body = (yield isFilePath(body)) ? yield readFile(body) : body;
// Compile the body and title templates using Handlebars
const bodyTemplate = handlebars.compile(body);
const titleTemplate = handlebars.compile(title);
const html = bodyTemplate(replacements.body);
const subject = titleTemplate(replacements.title);
const mailOptions = {
from,
to,
subject,
html,
attachments,
};
try {
// Send the email using the transporter
const info = yield transporter.sendMail(mailOptions);
console.log(`Email sent to ${to}: ${info.messageId}`);
return info;
}
catch (error) {
console.error("Email send error:", error);
throw error;
}
}),
};
/**
* Telegram plugin for sending messages using the Telegram Bot API.
*/
const telegramPlugin = {
/**
* Creates a Telegram message sender function that can be used as a notifx channel dispatcher.
*
* @param {string} botToken - The Telegram bot token.
* @returns {Function} An async function that sends a message via Telegram.
*
* @example
* const send = telegramPlugin.sendTelegramMessage(botToken);
* await send("123456789", "Hello <b>{{username}}</b>!", {
* message: { username: "jonhdoe" }
* });
*/
sendTelegramMessage: (botToken) =>
/**
* Sends a formatted message to a Telegram user or group.
*
* @param {string} chatId - Telegram `chat_id` (user, group, or channel).
* @param {string} message - Message template string using handlebars syntax.
* @param {{ message: Object }} replacements - Data for handlebars template.
* @returns {Promise<any>} Resolves with the Telegram API response data.
* @throws Will throw an error if the API call fails.
*/
(chatId, message, replacements) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const messageTemplate = handlebars.compile(message);
const parsedMessage = messageTemplate(replacements.message);
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
try {
const response = yield axios.post(url, {
chat_id: chatId,
text: parsedMessage,
parse_mode: "HTML",
});
console.log(`Telegram message sent to ${chatId}`);
return response.data;
}
catch (error) {
console.error("Telegram send error:", ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error.message);
throw error;
}
}),
};
exports.default = notifx;
exports.emailPlugin = emailPlugin;
exports.telegramPlugin = telegramPlugin;