Refactored backend to classes and IoC container
This commit is contained in:
parent
781e5e38c5
commit
dc8d00fdc8
@ -19,7 +19,9 @@ module.exports = {
|
||||
description: 'Pushover Example',
|
||||
user: '<log in to your Pushover dashboard to find your user key>',
|
||||
token: '<follow the instructions in the Pushover API documentation to create a token>',
|
||||
urlTitle: 'Manage notifications'
|
||||
urlTitle: 'Manage notifications',
|
||||
maxAttempts: 5,
|
||||
retryIntervalSeconds: 5
|
||||
}
|
||||
]
|
||||
}
|
71
index.js
71
index.js
@ -1,32 +1,75 @@
|
||||
const fshelpers = require('./lib/fshelpers');
|
||||
const Container = require('./src/container');
|
||||
const winston = require('winston');
|
||||
const express = require('express');
|
||||
const repository = require('./lib/repository');
|
||||
const facade = require('./lib/facade');
|
||||
|
||||
const AsyncFs = require('./src/asyncfs');
|
||||
const DateTimeProvider = require('./src/datetimeprovider');
|
||||
const ApiRoutes = require('./src/routes/api');
|
||||
const NotificationRepository = require('./src/notification/repository');
|
||||
const NotificationFacade = require('./src/notification/facade');
|
||||
const TransportProvider = require('./src/transport/provider');
|
||||
const SubjectParser = require('./src/subjectparser');
|
||||
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'verbose',
|
||||
});
|
||||
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.simple()
|
||||
}))
|
||||
|
||||
|
||||
const container = new Container();
|
||||
container.register('Logger', c => logger);
|
||||
container.register('AsyncFs', c => new AsyncFs());
|
||||
container.register('DateTimeProvider', c => new DateTimeProvider());
|
||||
container.register('ApiRoutes', c => new ApiRoutes(c.Logger, c.NotificationFacade, c.Config));
|
||||
container.register('NotificationRepository', c => new NotificationRepository(c.Logger, c.DateTimeProvider, c.AsyncFs, c.Config));
|
||||
container.register('NotificationFacade', c => new NotificationFacade(c.Logger, c.DateTimeProvider, c.TransportProvider, c.SubjectParser, c.NotificationRepository, c.Config));
|
||||
container.register('TransportProvider', c => new TransportProvider());
|
||||
container.register('SubjectParser', c => new SubjectParser());
|
||||
|
||||
|
||||
async function asyncMain()
|
||||
{
|
||||
if (await fshelpers.exists('config.js'))
|
||||
global.config = require('./config.js');
|
||||
else
|
||||
global.config = require('./config.default.js');
|
||||
|
||||
let config;
|
||||
|
||||
facade.init(global.config.contacts);
|
||||
await repository.init(global.config.dataFilename, global.config.salt);
|
||||
if (await container.AsyncFs.exists('config.js'))
|
||||
{
|
||||
logger.verbose('Using config.js');
|
||||
config = require('./config.js');
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.verbose('No config.js found, using config.default.js');
|
||||
config = require('./config.default.js');
|
||||
}
|
||||
|
||||
|
||||
container.register('Config', c => config);
|
||||
|
||||
|
||||
await container.NotificationRepository.init();
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
app.use('/api', require('./routes/api'));
|
||||
app.use('/api', container.ApiRoutes.create(express));
|
||||
app.use('/', express.static('frontend/dist'));
|
||||
|
||||
app.listen(global.config.port, () =>
|
||||
app.listen(config.port, () =>
|
||||
{
|
||||
console.log(`NotificationLatch listening at http://localhost:${global.config.port}`);
|
||||
logger.info(`NotificationLatch listening at http://localhost:${config.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
asyncMain();
|
||||
|
||||
asyncMain()
|
||||
.catch(err =>
|
||||
{
|
||||
logger.error(`Unhandled exception: ${err}`);
|
||||
logger.verbose(err.stack);
|
||||
});
|
117
lib/facade.js
117
lib/facade.js
@ -1,117 +0,0 @@
|
||||
const repository = require('./repository');
|
||||
const subjectParser = require('./subjectparser');
|
||||
const { DateTime } = require('luxon');
|
||||
|
||||
|
||||
function init(contacts, maxAttempts, retryInterval)
|
||||
{
|
||||
if (global.facadeData)
|
||||
throw new Error("Facade is already initialized");
|
||||
|
||||
|
||||
const transportMap = {
|
||||
"pushover": require('./transport/pushover')
|
||||
}
|
||||
|
||||
|
||||
global.facadeData = {
|
||||
maxAttempts: maxAttempts,
|
||||
retryInterval: retryInterval,
|
||||
|
||||
contacts: contacts.filter(c =>
|
||||
{
|
||||
if (!transportMap.hasOwnProperty(c.type))
|
||||
{
|
||||
console.error(`Unknown transport type '${c.type}', skipping contact`)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
||||
transportMap: transportMap
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function postNotification(subject, message, priority)
|
||||
{
|
||||
var parsedSubject = subjectParser.parse(subject);
|
||||
console.log(parsedSubject);
|
||||
|
||||
const token = await repository.storeNotification(parsedSubject.id, parsedSubject.title);
|
||||
if (token === null)
|
||||
return;
|
||||
|
||||
sendNotification({
|
||||
id: parsedSubject.id,
|
||||
token: token,
|
||||
title: parsedSubject.title,
|
||||
message: message,
|
||||
priority: priority,
|
||||
sound: parsedSubject.sound,
|
||||
timestamp: Math.floor(DateTime.now().toSeconds()),
|
||||
url: 'https://www.hierhaduwurlkunnenstaan.nl/'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function resetNotification(token)
|
||||
{
|
||||
await repository.resetNotification(token);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function sendNotification(notification)
|
||||
{
|
||||
if (!global.facadeData)
|
||||
throw new Error('Facade not initialized, call init() first');
|
||||
|
||||
global.facadeData.contacts.forEach(contact =>
|
||||
{
|
||||
const transport = global.facadeData.transportMap[contact.type];
|
||||
retryableSend(transport, contact, notification);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
|
||||
async function retryableSend(transport, contact, notification)
|
||||
{
|
||||
let attempt = 1;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
console.info(`Sending notification '${notification.id}' with token '${notification.token}' to '${contact.description}' (attempt ${attempt})`);
|
||||
await transport.send(contact, notification);
|
||||
|
||||
console.info(`Notification '${notification.id}' succesfully sent to '${contact.description}'`);
|
||||
return;
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
if (attempt >= global.facadeData.maxAttempts)
|
||||
{
|
||||
console.info(`Error while sending notification '${notification.id}' to '${contact.description}', max attempts reached: ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(`Error while sending notification '${notification.id}' to '${contact.description}', retrying in ${global.facadeData.retryInterval} seconds: ${err}`);
|
||||
await delay(global.facadeData.retryInterval * 1000);
|
||||
|
||||
attempt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
postNotification,
|
||||
resetNotification
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
const fs = require('fs').promises;
|
||||
|
||||
async function exists(path)
|
||||
{
|
||||
try
|
||||
{
|
||||
await fs.access(path);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports =
|
||||
{
|
||||
exists
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
const fs = require('fs').promises;
|
||||
const fshelpers = require('./fshelpers');
|
||||
const crypto = require('crypto');
|
||||
const { DateTime } = require('luxon');
|
||||
|
||||
function checkInitialized()
|
||||
{
|
||||
if (!global.repositoryData)
|
||||
throw new Error('Repository not initialized, call init() first');
|
||||
}
|
||||
|
||||
|
||||
async function init(filename, salt)
|
||||
{
|
||||
if (global.repositoryData)
|
||||
throw new Error('Repository is already initialized');
|
||||
|
||||
global.repositoryData = {
|
||||
filename: filename,
|
||||
salt: salt,
|
||||
notifications: {}
|
||||
};
|
||||
|
||||
if (!(await fshelpers.exists(filename)))
|
||||
return;
|
||||
|
||||
const contents = await fs.readFile(filename, 'utf8');
|
||||
global.repositoryData.notifications = JSON.parse(contents.toString());
|
||||
}
|
||||
|
||||
|
||||
async function flush()
|
||||
{
|
||||
const contents = JSON.stringify(global.repositoryData.notifications, null, 2);
|
||||
await fs.writeFile(global.repositoryData.filename, contents, 'utf8');
|
||||
}
|
||||
|
||||
|
||||
function getNotificationToken(id)
|
||||
{
|
||||
const hasher = crypto.createHmac("sha256", global.repositoryData.salt);
|
||||
return hasher.update(id).digest("hex");
|
||||
}
|
||||
|
||||
|
||||
function getUnixTimestamp()
|
||||
{
|
||||
return Math.floor(DateTime.now().toSeconds());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Returns the notification token if it should be sent out or null if the notification is latched
|
||||
async function storeNotification(id, title)
|
||||
{
|
||||
checkInitialized();
|
||||
|
||||
const now = getUnixTimestamp();
|
||||
const token = getNotificationToken(id);
|
||||
|
||||
if (!global.repositoryData.notifications.hasOwnProperty(token))
|
||||
{
|
||||
console.info(`New notification with id '${id}' and token '${token}', send permitted`);
|
||||
|
||||
global.repositoryData.notifications[token] = {
|
||||
id: id,
|
||||
title: title,
|
||||
latched: true,
|
||||
latchTime: now,
|
||||
resetTime: null,
|
||||
reminders: true,
|
||||
remindTime: null
|
||||
};
|
||||
|
||||
await flush();
|
||||
return token;
|
||||
}
|
||||
|
||||
const notification = global.repositoryData.notifications[token];
|
||||
|
||||
if (notification.latched)
|
||||
{
|
||||
console.info(`Notification with id ${id} and token '${token}' is already latched, send blocked`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.info(`Latching notification with id ${id} and token '${token}', send permitted`);
|
||||
notification.latched = true;
|
||||
notification.latchTime = now;
|
||||
|
||||
await flush();
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
async function resetNotification(token)
|
||||
{
|
||||
checkInitialized();
|
||||
|
||||
const now = getUnixTimestamp();
|
||||
|
||||
if (!global.repositoryData.notifications.hasOwnProperty(token))
|
||||
{
|
||||
console.info(`Notification token '${token}' does not exist, reset unneccesary`);
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = global.repositoryData.notifications[token];
|
||||
|
||||
if (!notification.latched)
|
||||
{
|
||||
console.info(`Notification with id '${notification.id}' and token '${token}' is not latched, reset unneccesary`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(`Resetting notification with id '${notification.id}' and token '${token}'`);
|
||||
notification.latched = false;
|
||||
notification.resetTime = now;
|
||||
notification.reminders = true;
|
||||
notification.remindTime = null;
|
||||
|
||||
await flush();
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
storeNotification,
|
||||
resetNotification
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
function parse(subject)
|
||||
{
|
||||
// Possible formats:
|
||||
// Title
|
||||
// |id| Title
|
||||
// |id,sound| Title
|
||||
// |,sound| Title
|
||||
const match = subject.match(/^(?:\|([^,]*?)(?:,(.+?)){0,1}\|){0,1}\s*(.+?)$/m);
|
||||
|
||||
if (match == null)
|
||||
return {
|
||||
id: subject,
|
||||
sound: null,
|
||||
title: subject
|
||||
};
|
||||
|
||||
return {
|
||||
id: match[1] || match[3],
|
||||
sound: match[2] || null,
|
||||
title: match[3]
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
parse
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
|
||||
async function send(contact, notification)
|
||||
{
|
||||
try
|
||||
{
|
||||
const data = {
|
||||
token: contact.token,
|
||||
user: contact.user,
|
||||
message: notification.message,
|
||||
priority: notification.priority,
|
||||
timestamp: notification.timestamp,
|
||||
url: notification.url,
|
||||
url_title: contact.urlTitle || 'Manage notifications'
|
||||
};
|
||||
|
||||
if (notification.sound)
|
||||
data.sound = notification.sound;
|
||||
|
||||
const response = await axios.post("https://api.pushover.net/1/messages.json", data);
|
||||
return response.statu;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
send
|
||||
}
|
244
package-lock.json
generated
244
package-lock.json
generated
@ -4,6 +4,16 @@
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@dabh/diagnostics": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
|
||||
"integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==",
|
||||
"requires": {
|
||||
"colorspace": "1.1.x",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@sindresorhus/is": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||
@ -86,6 +96,11 @@
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||
},
|
||||
"async": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz",
|
||||
"integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
@ -265,6 +280,30 @@
|
||||
"mimic-response": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
|
||||
"integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==",
|
||||
"requires": {
|
||||
"color-convert": "^1.9.1",
|
||||
"color-string": "^1.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
}
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@ -277,8 +316,30 @@
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"color-string": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz",
|
||||
"integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==",
|
||||
"requires": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
|
||||
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
|
||||
},
|
||||
"colorspace": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz",
|
||||
"integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==",
|
||||
"requires": {
|
||||
"color": "3.0.x",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
@ -323,6 +384,11 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"crypto-random-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||
@ -394,6 +460,11 @@
|
||||
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
|
||||
"dev": true
|
||||
},
|
||||
"enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
@ -461,6 +532,16 @@
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"fast-safe-stringify": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz",
|
||||
"integrity": "sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag=="
|
||||
},
|
||||
"fecha": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz",
|
||||
"integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q=="
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@ -484,6 +565,11 @@
|
||||
"unpipe": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
|
||||
@ -630,6 +716,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
@ -703,6 +794,11 @@
|
||||
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
@ -715,6 +811,11 @@
|
||||
"integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
|
||||
"dev": true
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"json-buffer": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
|
||||
@ -730,6 +831,11 @@
|
||||
"json-buffer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
|
||||
},
|
||||
"latest-version": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
|
||||
@ -739,6 +845,25 @@
|
||||
"package-json": "^6.3.0"
|
||||
}
|
||||
},
|
||||
"logform": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz",
|
||||
"integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==",
|
||||
"requires": {
|
||||
"colors": "^1.2.1",
|
||||
"fast-safe-stringify": "^2.0.4",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
|
||||
@ -904,6 +1029,14 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"requires": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"p-cancelable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
|
||||
@ -952,6 +1085,11 @@
|
||||
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
|
||||
"dev": true
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -1019,6 +1157,16 @@
|
||||
"strip-json-comments": "~2.0.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@ -1137,6 +1285,19 @@
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
||||
"dev": true
|
||||
},
|
||||
"simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
|
||||
"requires": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
@ -1182,6 +1343,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||
@ -1212,6 +1388,11 @@
|
||||
"integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==",
|
||||
"dev": true
|
||||
},
|
||||
"text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
|
||||
},
|
||||
"to-readable-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
|
||||
@ -1241,6 +1422,11 @@
|
||||
"nopt": "~1.0.10"
|
||||
}
|
||||
},
|
||||
"triple-beam": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
|
||||
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
|
||||
@ -1318,6 +1504,11 @@
|
||||
"prepend-http": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@ -1337,6 +1528,55 @@
|
||||
"string-width": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"winston": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz",
|
||||
"integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==",
|
||||
"requires": {
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
"async": "^3.1.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.2.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"winston-transport": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz",
|
||||
"integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==",
|
||||
"requires": {
|
||||
"readable-stream": "^2.3.7",
|
||||
"triple-beam": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
@ -12,7 +12,8 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"express": "^4.17.1",
|
||||
"luxon": "^2.0.2"
|
||||
"luxon": "^2.0.2",
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.12"
|
||||
|
@ -1,46 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const facade = require('../lib/facade');
|
||||
|
||||
function logRequestWarning(req, message)
|
||||
{
|
||||
console.warn(`[${req.ip}] ${message}`);
|
||||
}
|
||||
|
||||
|
||||
router.post('/notification', async (req, res) =>
|
||||
{
|
||||
if (req.headers.authorization !== 'Bearer ' + global.config.authToken)
|
||||
{
|
||||
logRequestWarning(req, 'Missing or invalid authorization header');
|
||||
res.sendStatus(401);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.body || !req.body.subject || !req.body.message)
|
||||
{
|
||||
logRequestWarning(req, 'Missing body, subject and/or message parameters');
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
await facade.postNotification(req.body.subject, req.body.message, req.body.priority || 0);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
|
||||
router.post('/notification/reset', async (req, res) =>
|
||||
{
|
||||
if (!req.body || !req.body.token)
|
||||
{
|
||||
logRequestWarning(req, 'Missing token');
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
await facade.resetNotification(req.body.token);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
module.exports = router;
|
21
src/asyncfs.js
Normal file
21
src/asyncfs.js
Normal file
@ -0,0 +1,21 @@
|
||||
const fs = require('fs').promises;
|
||||
|
||||
|
||||
class AsyncFs
|
||||
{
|
||||
async exists(path)
|
||||
{
|
||||
try
|
||||
{
|
||||
await fs.access(path);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = AsyncFs;
|
31
src/container.js
Normal file
31
src/container.js
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Original concept and code by Magnus Tovslid:
|
||||
* https://medium.com/@magnusjt/ioc-container-in-nodejs-e7aea8a89600
|
||||
*/
|
||||
class Container
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this.services = {};
|
||||
}
|
||||
|
||||
register(name, factory)
|
||||
{
|
||||
Object.defineProperty(this, name, {
|
||||
get: () =>
|
||||
{
|
||||
if(!this.services.hasOwnProperty(name))
|
||||
this.services[name] = factory(this);
|
||||
|
||||
return this.services[name];
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = Container;
|
13
src/datetimeprovider.js
Normal file
13
src/datetimeprovider.js
Normal file
@ -0,0 +1,13 @@
|
||||
const { DateTime } = require('luxon');
|
||||
|
||||
|
||||
class DateTimeProvider
|
||||
{
|
||||
unixTimestamp()
|
||||
{
|
||||
return Math.floor(DateTime.now().toSeconds());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = DateTimeProvider;
|
102
src/notification/facade.js
Normal file
102
src/notification/facade.js
Normal file
@ -0,0 +1,102 @@
|
||||
class NotificationFacade
|
||||
{
|
||||
constructor(logger, dateTimeProvider, transportProvider, subjectParser, notificationRepository, config)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.dateTimeProvider = dateTimeProvider;
|
||||
this.transportProvider = transportProvider;
|
||||
this.subjectParser = subjectParser;
|
||||
this.notificationRepository = notificationRepository;
|
||||
|
||||
this.contacts = config.contacts.filter(contact =>
|
||||
{
|
||||
if (transportProvider.byType(contact.type) === null)
|
||||
{
|
||||
logger.error(`Unknown transport type '${contact.type}' for contact '${contact.description}', skipping contact`)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async postNotification(subject, message, priority)
|
||||
{
|
||||
var parsedSubject = this.subjectParser.parse(subject);
|
||||
|
||||
const token = await this.notificationRepository.storeNotification(parsedSubject.id, parsedSubject.title);
|
||||
if (token === null)
|
||||
return;
|
||||
|
||||
this._sendNotification({
|
||||
id: parsedSubject.id,
|
||||
token: token,
|
||||
title: parsedSubject.title,
|
||||
message: message,
|
||||
priority: priority,
|
||||
sound: parsedSubject.sound,
|
||||
timestamp: this.dateTimeProvider.unixTimestamp(),
|
||||
url: 'https://www.hierhaduwurlkunnenstaan.nl/'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async resetNotification(token)
|
||||
{
|
||||
await this.notificationRepository.resetNotification(token);
|
||||
}
|
||||
|
||||
|
||||
|
||||
_sendNotification(notification)
|
||||
{
|
||||
this.contacts.forEach(contact =>
|
||||
{
|
||||
const transport = this.transportProvider.byType(contact.type);
|
||||
this._retryableSend(transport, contact, notification);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
_delay(ms)
|
||||
{
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
||||
async _retryableSend(transport, contact, notification)
|
||||
{
|
||||
let attempt = 1;
|
||||
const retryInterval = contact.retryIntervalSeconds || 0;
|
||||
const maxAttempts = contact.maxAttempts || 1;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.logger.info(`Sending notification '${notification.id}' with token '${notification.token}' to '${contact.description}' (attempt ${attempt})`);
|
||||
await transport.send(contact, notification);
|
||||
|
||||
this.logger.info(`Notification '${notification.id}' succesfully sent to '${contact.description}'`);
|
||||
return;
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
if (attempt >= maxAttempts)
|
||||
{
|
||||
this.logger.info(`Error while sending notification '${notification.id}' to '${contact.description}', max attempts reached: ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(`Error while sending notification '${notification.id}' to '${contact.description}', retrying in ${retryInterval} seconds: ${err}`);
|
||||
await this._delay(retryInterval * 1000);
|
||||
|
||||
attempt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = NotificationFacade;
|
130
src/notification/repository.js
Normal file
130
src/notification/repository.js
Normal file
@ -0,0 +1,130 @@
|
||||
const fs = require('fs').promises;
|
||||
const crypto = require('crypto');
|
||||
|
||||
|
||||
class NotificationRepository
|
||||
{
|
||||
constructor(logger, dateTimeProvider, asyncFs, config)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.dateTimeProvider = dateTimeProvider;
|
||||
this.asyncFs = asyncFs;
|
||||
|
||||
this.filename = config.dataFilename;
|
||||
this.salt = config.salt;
|
||||
this.notifications = null;
|
||||
}
|
||||
|
||||
|
||||
async init()
|
||||
{
|
||||
if (this.notifications !== null)
|
||||
throw new Error('Repository is already initialized');
|
||||
|
||||
this.notifications = {};
|
||||
|
||||
if (!(await this.asyncFs.exists(this.filename)))
|
||||
return;
|
||||
|
||||
const contents = await fs.readFile(this.filename, 'utf8');
|
||||
this.notifications = JSON.parse(contents.toString());
|
||||
}
|
||||
|
||||
|
||||
// Returns the notification token if it should be sent out or null if the notification is latched
|
||||
async storeNotification(id, title)
|
||||
{
|
||||
this._checkInitialized();
|
||||
|
||||
const now = this.dateTimeProvider.unixTimestamp();
|
||||
const token = this._getNotificationToken(id);
|
||||
|
||||
if (!this.notifications.hasOwnProperty(token))
|
||||
{
|
||||
this.logger.info(`New notification with id '${id}' and token '${token}', send permitted`);
|
||||
|
||||
this.notifications[token] = {
|
||||
id: id,
|
||||
title: title,
|
||||
latched: true,
|
||||
latchTime: now,
|
||||
resetTime: null,
|
||||
reminders: true,
|
||||
remindTime: null
|
||||
};
|
||||
|
||||
await this._flush();
|
||||
return token;
|
||||
}
|
||||
|
||||
const notification = this.notifications[token];
|
||||
|
||||
if (notification.latched)
|
||||
{
|
||||
this.logger.info(`Notification with id ${id} and token '${token}' is already latched, send blocked`);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.info(`Latching notification with id ${id} and token '${token}', send permitted`);
|
||||
notification.latched = true;
|
||||
notification.latchTime = now;
|
||||
|
||||
await this._flush();
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
async resetNotification(token)
|
||||
{
|
||||
this._checkInitialized();
|
||||
|
||||
const now = this.dateTimeProvider.unixTimestamp();
|
||||
|
||||
if (!this.notifications.hasOwnProperty(token))
|
||||
{
|
||||
this.logger.info(`Notification token '${token}' does not exist, reset unneccesary`);
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = this.notifications[token];
|
||||
|
||||
if (!notification.latched)
|
||||
{
|
||||
this.logger.info(`Notification with id '${notification.id}' and token '${token}' is not latched, reset unneccesary`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(`Resetting notification with id '${notification.id}' and token '${token}'`);
|
||||
notification.latched = false;
|
||||
notification.resetTime = now;
|
||||
notification.reminders = true;
|
||||
notification.remindTime = null;
|
||||
|
||||
await this._flush();
|
||||
}
|
||||
|
||||
|
||||
_getNotificationToken(id)
|
||||
{
|
||||
const hasher = crypto.createHmac("sha256", this.salt);
|
||||
return hasher.update(id).digest("hex");
|
||||
}
|
||||
|
||||
|
||||
async _flush()
|
||||
{
|
||||
const contents = JSON.stringify(this.notifications, null, 2);
|
||||
await fs.writeFile(this.filename, contents, 'utf8');
|
||||
}
|
||||
|
||||
|
||||
_checkInitialized()
|
||||
{
|
||||
if (this.notifications === null)
|
||||
throw new Error('Repository not initialized, call init() first');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
module.exports = NotificationRepository;
|
79
src/routes/api.js
Normal file
79
src/routes/api.js
Normal file
@ -0,0 +1,79 @@
|
||||
class ApiRoutes
|
||||
{
|
||||
constructor(logger, notificationFacade, config)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.notificationFacade = notificationFacade;
|
||||
this.authToken = config.authToken;
|
||||
}
|
||||
|
||||
|
||||
create(express)
|
||||
{
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/notification', (req, res) => this._wrapAsyncHandler(req, res, this._handlePostNotification.bind(this)));
|
||||
router.post('/notification/reset', (req, res) => this._wrapAsyncHandler(req, res, this._handleResetNotification.bind(this)));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
async _handlePostNotification(req, res)
|
||||
{
|
||||
if (req.headers.authorization !== 'Bearer ' + this.authToken)
|
||||
{
|
||||
this._logRequestWarning(req, 'Missing or invalid authorization header');
|
||||
res.sendStatus(401);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.body || !req.body.subject || !req.body.message)
|
||||
{
|
||||
this._logRequestWarning(req, 'Missing body, subject and/or message parameters');
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.notificationFacade.postNotification(req.body.subject, req.body.message, req.body.priority || 0);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
|
||||
async _handleResetNotification(req, res)
|
||||
{
|
||||
if (!req.body || !req.body.token)
|
||||
{
|
||||
this._logRequestWarning(req, 'Missing token');
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.notificationFacade.resetNotification(req.body.token);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
|
||||
async _wrapAsyncHandler(req, res, handler)
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler(req, res);
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
this.logger.error(`Unhandled exception in request handler: ${err}`);
|
||||
this.logger.verbose(err.stack);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_logRequestWarning(req, message)
|
||||
{
|
||||
this.logger.warn(`[${req.ip}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = ApiRoutes;
|
28
src/subjectparser.js
Normal file
28
src/subjectparser.js
Normal file
@ -0,0 +1,28 @@
|
||||
class SubjectParser
|
||||
{
|
||||
parse(subject)
|
||||
{
|
||||
// Possible formats:
|
||||
// Title
|
||||
// |id| Title
|
||||
// |id,sound| Title
|
||||
// |,sound| Title
|
||||
const match = subject.match(/^(?:\|([^,]*?)(?:,(.+?)){0,1}\|){0,1}\s*(.+?)$/m);
|
||||
|
||||
if (match == null)
|
||||
return {
|
||||
id: subject,
|
||||
sound: null,
|
||||
title: subject
|
||||
};
|
||||
|
||||
return {
|
||||
id: match[1] || match[3],
|
||||
sound: match[2] || null,
|
||||
title: match[3]
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
module.exports = SubjectParser;
|
17
src/transport/provider.js
Normal file
17
src/transport/provider.js
Normal file
@ -0,0 +1,17 @@
|
||||
const PushoverTransport = require('./pushover');
|
||||
|
||||
const transportMap = {
|
||||
'pushover': new PushoverTransport()
|
||||
}
|
||||
|
||||
|
||||
class TransportProvider
|
||||
{
|
||||
byType(type)
|
||||
{
|
||||
return transportMap.hasOwnProperty(type) ? transportMap[type] : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = TransportProvider;
|
34
src/transport/pushover.js
Normal file
34
src/transport/pushover.js
Normal file
@ -0,0 +1,34 @@
|
||||
const axios = require('axios');
|
||||
|
||||
|
||||
class PushoverTransport
|
||||
{
|
||||
async send(contact, notification)
|
||||
{
|
||||
try
|
||||
{
|
||||
const data = {
|
||||
token: contact.token,
|
||||
user: contact.user,
|
||||
message: notification.message,
|
||||
priority: notification.priority,
|
||||
timestamp: notification.timestamp,
|
||||
url: notification.url,
|
||||
url_title: contact.urlTitle || 'Manage notifications'
|
||||
};
|
||||
|
||||
if (notification.sound)
|
||||
data.sound = notification.sound;
|
||||
|
||||
const response = await axios.post("https://api.pushover.net/1/messages.json", data);
|
||||
return response.statu;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = PushoverTransport;
|
Loading…
Reference in New Issue
Block a user