Ghost/core/server/api/notifications.js

319 lines
11 KiB
JavaScript

// # Notifications API
// RESTful API for creating notifications
const Promise = require('bluebird'),
{merge, orderBy, remove} = require('lodash'),
semver = require('semver'),
moment = require('moment'),
ObjectId = require('bson-objectid'),
ghostVersion = require('../lib/ghost-version'),
pipeline = require('../lib/promise/pipeline'),
permissions = require('../services/permissions'),
localUtils = require('./utils'),
common = require('../lib/common'),
SettingsAPI = require('./settings'),
internalContext = {context: {internal: true}},
canThis = permissions.canThis;
let notifications,
_private = {};
_private.fetchAllNotifications = function fetchAllNotifications() {
let allNotifications;
return SettingsAPI.read(merge({key: 'notifications'}, internalContext))
.then((response) => {
allNotifications = JSON.parse(response.settings[0].value || []);
allNotifications.forEach((notification) => {
notification.addedAt = moment(notification.addedAt).toDate();
});
return allNotifications;
});
};
_private.publicResponse = function publicResponse(notificationsToReturn) {
notificationsToReturn.forEach((notification) => {
delete notification.seen;
delete notification.addedAt;
});
return {
notifications: notificationsToReturn
};
};
/**
* ## Notification API Methods
*
* **See:** [API Methods](constants.js.html#api%20methods)
*/
notifications = {
/**
* ### Browse
* Fetch all notifications
* @returns {Promise(Notifications)}
*/
browse: (options) => {
return canThis(options.context).browse.notification().then(() => {
return _private.fetchAllNotifications()
.then((allNotifications) => {
allNotifications = orderBy(allNotifications, 'addedAt', 'desc');
allNotifications = allNotifications.filter((notification) => {
// CASE: do not return old release notification
if (!notification.custom && notification.message) {
const notificationVersion = notification.message.match(/(\d+\.)(\d+\.)(\d+)/),
blogVersion = ghostVersion.full.match(/^(\d+\.)(\d+\.)(\d+)/);
if (notificationVersion && blogVersion && semver.gt(notificationVersion[0], blogVersion[0])) {
return true;
} else {
return false;
}
}
return notification.seen !== true;
});
return _private.publicResponse(allNotifications);
});
}, () => {
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.notifications.noPermissionToBrowseNotif')
}));
});
},
/**
* ### Add
*
*
* **takes:** a notification object of the form
*
* If notification message already exists, we return the existing notification object.
*
* ```
* msg = { notifications: [{
* status: 'alert', // A String. Can be 'alert' or 'notification'
* type: 'error', // A String. Can be 'error', 'success', 'warn' or 'info'
* message: 'This is an error', // A string. Should fit in one line.
* location: '', // A String. Should be unique key to the notification, usually takes the form of "noun.verb.message", eg: "user.invite.already-invited"
* dismissible: true // A Boolean. Whether the notification is dismissible or not.
* custom: true // A Boolean. Whether the notification is a custom message intended for particular Ghost versions.
* }] };
* ```
*/
add: (object, options) => {
let tasks;
/**
* ### Handle Permissions
* We need to be an authorised user to perform this action
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
if (permissions.parseContext(options.context).internal) {
return Promise.resolve(options);
}
return canThis(options.context).add.notification().then(() => {
return options;
}, () => {
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.notifications.noPermissionToAddNotif')
}));
});
}
/**
* ### Save Notifications
* Save the notifications
* @param {Object} options
* @returns {Object} options
*/
function saveNotifications(options) {
const defaults = {
dismissible: true,
location: 'bottom',
status: 'alert',
id: ObjectId.generate()
},
overrides = {
seen: false,
addedAt: moment().toDate()
},
notificationsToCheck = options.data.notifications;
let addedNotifications = [];
return _private.fetchAllNotifications()
.then((allNotifications) => {
notificationsToCheck.forEach((notification) => {
const isDuplicate = allNotifications.find((n) => {
return n.id === notification.id;
});
if (!isDuplicate) {
addedNotifications.push(merge({}, defaults, notification, overrides));
}
});
const hasReleaseNotification = notificationsToCheck.find((notification) => {
return !notification.custom;
});
// CASE: remove any existing release notifications if a new release notification comes in
if (hasReleaseNotification) {
remove(allNotifications, (el) => {
return !el.custom;
});
}
// CASE: nothing to add, skip
if (!addedNotifications.length) {
return Promise.resolve();
}
const addedReleaseNotifications = addedNotifications.filter((notification) => {
return !notification.custom;
});
// CASE: only latest release notification
if (addedReleaseNotifications.length > 1) {
addedNotifications = addedNotifications.filter((notification) => {
return notification.custom;
});
addedNotifications.push(orderBy(addedReleaseNotifications, 'created_at', 'desc')[0]);
}
return SettingsAPI.edit({
settings: [{
key: 'notifications',
value: allNotifications.concat(addedNotifications)
}]
}, internalContext);
})
.then(() => {
return _private.publicResponse(addedNotifications);
});
}
tasks = [
localUtils.validate('notifications'),
handlePermissions,
saveNotifications
];
return pipeline(tasks, object, options);
},
/**
* ### Destroy
* Remove a specific notification
*
* @param {{id (required), context}} options
* @returns {Promise}
*/
destroy: (options) => {
let tasks;
/**
* ### Handle Permissions
* We need to be an authorised user to perform this action
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
return canThis(options.context).destroy.notification().then(() => {
return options;
}, () => {
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.notifications.noPermissionToDestroyNotif')
}));
});
}
function destroyNotification(options) {
return _private.fetchAllNotifications()
.then((allNotifications) => {
const notificationToMarkAsSeen = allNotifications.find((notification) => {
return notification.id === options.id;
}),
notificationToMarkAsSeenIndex = allNotifications.findIndex((notification) => {
return notification.id === options.id;
});
if (notificationToMarkAsSeenIndex > -1 && !notificationToMarkAsSeen.dismissible) {
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.notifications.noPermissionToDismissNotif')
}));
}
if (notificationToMarkAsSeenIndex < 0) {
return Promise.reject(new common.errors.NotFoundError({
message: common.i18n.t('errors.api.notifications.notificationDoesNotExist')
}));
}
if (notificationToMarkAsSeen.seen) {
return Promise.resolve();
}
allNotifications[notificationToMarkAsSeenIndex].seen = true;
return SettingsAPI.edit({
settings: [{
key: 'notifications',
value: allNotifications
}]
}, internalContext);
})
.return();
}
tasks = [
handlePermissions,
destroyNotification
];
return pipeline(tasks, options);
},
/**
* ### DestroyAll
* Clear all notifications, used for tests
*
* @private Not exposed over HTTP
* @returns {Promise}
*/
destroyAll: (options) => {
return canThis(options.context).destroy.notification()
.then(() => {
return _private.fetchAllNotifications()
.then((allNotifications) => {
allNotifications.forEach((notification) => {
notification.seen = true;
});
return SettingsAPI.edit({
settings: [{
key: 'notifications',
value: allNotifications
}]
}, internalContext);
})
.return();
}, (err) => {
return Promise.reject(new common.errors.NoPermissionError({
err: err,
context: common.i18n.t('errors.api.notifications.noPermissionToDestroyNotif')
}));
});
}
};
module.exports = notifications;