mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
Extraced update check code into a separate service
refs https://github.com/TryGhost/Team/issues/728 - This is a first step before moving update check code into an outside codebase. - The aim is to have a self-contained module which could be unit tested and have a very clear API
This commit is contained in:
parent
0f42bbeac3
commit
d5e6dbb0fb
341
core/server/update-check-service.js
Normal file
341
core/server/update-check-service.js
Normal file
@ -0,0 +1,341 @@
|
||||
const _ = require('lodash');
|
||||
const url = require('url');
|
||||
const crypto = require('crypto');
|
||||
const moment = require('moment');
|
||||
const Promise = require('bluebird');
|
||||
const exec = require('child_process').exec;
|
||||
const errors = require('@tryghost/errors');
|
||||
const debug = require('ghost-ignition').debug('update-check');
|
||||
|
||||
const internal = {context: {internal: true}};
|
||||
|
||||
/**
|
||||
* Update Checker Class
|
||||
*
|
||||
* Makes a request to Ghost.org to request release & custom notifications.
|
||||
* The service is provided in return for users opting in to anonymous usage data collection.
|
||||
*
|
||||
* Blog owners can opt-out of update checks by setting `privacy: { useUpdateCheck: false }` in their config file.
|
||||
*/
|
||||
class UpdateCheckService {
|
||||
constructor({api, config, i18n, logging, urlUtils, request, ghostVersion, ghostMailer}) {
|
||||
this.api = api;
|
||||
this.config = config;
|
||||
this.i18n = i18n;
|
||||
this.logging = logging;
|
||||
this.urlUtils = urlUtils;
|
||||
this.request = request;
|
||||
this.ghostVersion = ghostVersion;
|
||||
this.ghostMailer = ghostMailer;
|
||||
}
|
||||
|
||||
nextCheckTimestamp() {
|
||||
const now = Math.round(new Date().getTime() / 1000);
|
||||
return now + (24 * 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Centralised error handler for the update check unit.
|
||||
*
|
||||
* CASES:
|
||||
* - the update check service returns an error
|
||||
* - error during collecting blog stats
|
||||
*
|
||||
* We still need to ensure that we set the "next_update_check" to a new value, otherwise no more
|
||||
* update checks will happen.
|
||||
*
|
||||
* @param err
|
||||
*/
|
||||
updateCheckError(err) {
|
||||
this.api.settings.edit({
|
||||
settings: [{
|
||||
key: 'next_update_check',
|
||||
value: this.nextCheckTimestamp()
|
||||
}]
|
||||
}, internal);
|
||||
|
||||
err.context = this.i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
|
||||
err.help = this.i18n.t('errors.updateCheck.checkingForUpdatesFailed.help', {url: 'https://ghost.org/docs/'});
|
||||
|
||||
this.logging.error(err);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Collect stats from your blog.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updateCheckData() {
|
||||
let data = {};
|
||||
let mailConfig = this.config.get('mail');
|
||||
|
||||
data.ghost_version = this.ghostVersion.original;
|
||||
data.node_version = process.versions.node;
|
||||
data.env = this.config.get('env');
|
||||
data.database_type = this.config.get('database').client;
|
||||
data.email_transport = mailConfig &&
|
||||
(mailConfig.options && mailConfig.options.service ?
|
||||
mailConfig.options.service :
|
||||
mailConfig.transport);
|
||||
|
||||
return Promise.props({
|
||||
hash: this.api.settings.read(_.extend({key: 'db_hash'}, internal)).reflect(),
|
||||
theme: this.api.settings.read(_.extend({key: 'active_theme'}, internal)).reflect(),
|
||||
posts: this.api.posts.browse().reflect(),
|
||||
users: this.api.users.browse(internal).reflect(),
|
||||
npm: Promise.promisify(exec)('npm -v').reflect()
|
||||
}).then(function (descriptors) {
|
||||
const hash = descriptors.hash.value().settings[0];
|
||||
const theme = descriptors.theme.value().settings[0];
|
||||
const posts = descriptors.posts.value();
|
||||
const users = descriptors.users.value();
|
||||
const npm = descriptors.npm.value();
|
||||
const blogUrl = this.urlUtils.urlFor('home', true);
|
||||
const parsedBlogUrl = url.parse(blogUrl);
|
||||
const blogId = parsedBlogUrl.hostname + parsedBlogUrl.pathname.replace(/\//, '') + hash.value;
|
||||
|
||||
data.url = blogUrl;
|
||||
data.blog_id = crypto.createHash('md5').update(blogId).digest('hex');
|
||||
data.theme = theme ? theme.value : '';
|
||||
data.post_count = posts && posts.meta && posts.meta.pagination ? posts.meta.pagination.total : 0;
|
||||
data.user_count = users && users.users && users.users.length ? users.users.length : 0;
|
||||
data.blog_created_at = users && users.users && users.users[0] && users.users[0].created_at ? moment(users.users[0].created_at).unix() : '';
|
||||
data.npm_version = npm.trim();
|
||||
|
||||
return data;
|
||||
}).catch(this.updateCheckError);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Perform request to update check service.
|
||||
*
|
||||
* With the privacy setting `useUpdateCheck` you can control if you want to expose data/stats from your blog to the
|
||||
* service. Enabled or disabled, you will receive the latest notification available from the service.
|
||||
*
|
||||
* @see https://ghost.org/docs/concepts/config/#privacy
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async updateCheckRequest() {
|
||||
return this.updateCheckData()
|
||||
.then(function then(reqData) {
|
||||
let reqObj = {
|
||||
timeout: 1000,
|
||||
headers: {}
|
||||
};
|
||||
|
||||
let checkEndpoint = this.config.get('updateCheck:url');
|
||||
let checkMethod = this.config.isPrivacyDisabled('useUpdateCheck') ? 'GET' : 'POST';
|
||||
|
||||
// CASE: Expose stats and do a check-in
|
||||
if (checkMethod === 'POST') {
|
||||
reqObj.json = true;
|
||||
reqObj.body = reqData;
|
||||
reqObj.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(reqData));
|
||||
reqObj.headers['Content-Type'] = 'application/json';
|
||||
} else {
|
||||
reqObj.json = true;
|
||||
reqObj.query = {
|
||||
ghost_version: reqData.ghost_version
|
||||
};
|
||||
}
|
||||
|
||||
debug('Request Update Check Service', checkEndpoint);
|
||||
|
||||
return this.request(checkEndpoint, reqObj)
|
||||
.then(function (response) {
|
||||
return response.body;
|
||||
})
|
||||
.catch(function (err) {
|
||||
// CASE: no notifications available, ignore
|
||||
if (err.statusCode === 404) {
|
||||
return {
|
||||
next_check: this.nextCheckTimestamp(),
|
||||
notifications: []
|
||||
};
|
||||
}
|
||||
|
||||
// CASE: service returns JSON error, deserialize into JS error
|
||||
if (err.response && err.response.body && typeof err.response.body === 'object') {
|
||||
err = errors.utils.deserialize(err.response.body);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This function handles the response from the update check service.
|
||||
*
|
||||
* The helper does three things:
|
||||
*
|
||||
* 1. Updates the time in the settings table to know when we can execute the next update check request.
|
||||
* 2. Iterates over the received notifications and filters them out based on your notification groups.
|
||||
* 3. Calls a custom helper to generate a Ghost notification for the database.
|
||||
*
|
||||
* The structure of the response is:
|
||||
*
|
||||
* {
|
||||
* id: 20,
|
||||
* version: 'all4',
|
||||
* messages:
|
||||
* [{
|
||||
* id: 'f8ff6c80-aa61-11e7-a126-6119te37e2b8',
|
||||
* version: '^2',
|
||||
* content: 'Hallouuuu custom',
|
||||
* top: true,
|
||||
* dismissible: true,
|
||||
* type: 'info'
|
||||
* }],
|
||||
* created_at: '2021-10-06T07:00:00.000Z',
|
||||
* custom: 1,
|
||||
* next_check: 1555608722
|
||||
* }
|
||||
*
|
||||
*
|
||||
* Example for grouped custom notifications in config:
|
||||
*
|
||||
* "notificationGroups": ["migration", "something"]
|
||||
*
|
||||
* The group 'all' is a reserved name for general custom notifications, which every self hosted blog can receive.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @return {Promise}
|
||||
*/
|
||||
async updateCheckResponse(response) {
|
||||
let notifications = [];
|
||||
let notificationGroups = (this.config.get('notificationGroups') || []).concat(['all']);
|
||||
|
||||
debug('Notification Groups', notificationGroups);
|
||||
debug('Response Update Check Service', response);
|
||||
|
||||
await this.api.settings.edit({
|
||||
settings: [{
|
||||
key: 'next_update_check',
|
||||
value: response.next_check
|
||||
}]
|
||||
}, internal);
|
||||
|
||||
/**
|
||||
* @NOTE:
|
||||
*
|
||||
* When we refactored notifications in Ghost 1.20, the service did not support returning multiple messages.
|
||||
* But we wanted to already add the support for future functionality.
|
||||
* That's why this helper supports two ways: returning an array of messages or returning an object with
|
||||
* a "notifications" key. The second one is probably the best, because we need to support "next_check"
|
||||
* on the root level of the response.
|
||||
*/
|
||||
if (_.isArray(response)) {
|
||||
notifications = response;
|
||||
} else if ((Object.prototype.hasOwnProperty.call(response, 'notifications') && _.isArray(response.notifications))) {
|
||||
notifications = response.notifications;
|
||||
} else {
|
||||
// CASE: default right now
|
||||
notifications = [response];
|
||||
}
|
||||
|
||||
// CASE: Hook into received notifications and decide whether you are allowed to receive custom group messages.
|
||||
if (notificationGroups.length) {
|
||||
notifications = notifications.filter(function (notification) {
|
||||
// CASE: release notification, keep
|
||||
if (!notification.custom) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// CASE: filter out messages based on your groups
|
||||
return _.includes(notificationGroups.map(function (groupIdentifier) {
|
||||
if (notification.version.match(new RegExp(groupIdentifier))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}), true) === true;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.each(notifications, this.createCustomNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Create a Ghost notification and call the API controller.
|
||||
*
|
||||
* @param {Object} notification
|
||||
* @return {Promise}
|
||||
*/
|
||||
async createCustomNotification(notification) {
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {users} = await this.api.users.browse(Object.assign({
|
||||
limit: 'all',
|
||||
include: ['roles']
|
||||
}, internal));
|
||||
|
||||
const adminEmails = users
|
||||
.filter(user => ['Owner', 'Administrator'].includes(user.roles[0].name))
|
||||
.map(user => user.email);
|
||||
|
||||
const siteUrl = this.config.get('url');
|
||||
|
||||
for (const message of notification.messages) {
|
||||
const toAdd = {
|
||||
// @NOTE: the update check service returns "0" or "1" (https://github.com/TryGhost/UpdateCheck/issues/43)
|
||||
custom: !!notification.custom,
|
||||
createdAt: moment(notification.created_at).toDate(),
|
||||
status: message.status || 'alert',
|
||||
type: message.type || 'info',
|
||||
id: message.id,
|
||||
dismissible: Object.prototype.hasOwnProperty.call(message, 'dismissible') ? message.dismissible : true,
|
||||
top: !!message.top,
|
||||
message: message.content
|
||||
};
|
||||
|
||||
if (toAdd.type === 'alert') {
|
||||
for (const email of adminEmails) {
|
||||
try {
|
||||
this.ghostMailer.send({
|
||||
to: email,
|
||||
subject: `Action required: Critical alert from Ghost instance ${siteUrl}`,
|
||||
html: toAdd.message,
|
||||
forceTextContent: true
|
||||
});
|
||||
} catch (err) {
|
||||
this.logging.err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug('Add Custom Notification', toAdd);
|
||||
await this.api.notifications.add({notifications: [toAdd]}, {context: {internal: true}});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @description Entry point to trigger the update check unit.
|
||||
*
|
||||
* Based on a settings value, we check if `next_update_check` is less than now to decide whether
|
||||
* we should request the update check service (http://updates.ghost.org) or not.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async check() {
|
||||
const result = await this.api.settings.read(_.extend({key: 'next_update_check'}, internal));
|
||||
|
||||
const nextUpdateCheck = result.settings[0];
|
||||
|
||||
// CASE: Next update check should happen now?
|
||||
// @NOTE: You can skip this check by adding a config value. This is helpful for developing.
|
||||
if (!this.config.get('updateCheck:forceUpdate') && nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.updateCheckRequest();
|
||||
|
||||
await this.updateCheckResponse(response);
|
||||
} catch (err) {
|
||||
this.updateCheckError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UpdateCheckService;
|
@ -1,346 +1,36 @@
|
||||
/**
|
||||
* Update Checking Unit
|
||||
*
|
||||
* Makes a request to Ghost.org to request release & custom notifications.
|
||||
* The service is provided in return for users opting in to anonymous usage data collection.
|
||||
*
|
||||
* Blog owners can opt-out of update checks by setting `privacy: { useUpdateCheck: false }` in their config file.
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const exec = require('child_process').exec;
|
||||
const moment = require('moment');
|
||||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const url = require('url');
|
||||
const debug = require('ghost-ignition').debug('update-check');
|
||||
|
||||
const api = require('./api').v2;
|
||||
const GhostMailer = require('./services/mail').GhostMailer;
|
||||
const config = require('../shared/config');
|
||||
const urlUtils = require('./../shared/url-utils');
|
||||
const errors = require('@tryghost/errors');
|
||||
|
||||
const i18n = require('../shared/i18n');
|
||||
const logging = require('../shared/logging');
|
||||
const request = require('./lib/request');
|
||||
const ghostVersion = require('./lib/ghost-version');
|
||||
|
||||
const internal = {context: {internal: true}};
|
||||
const allowedCheckEnvironments = ['development', 'production'];
|
||||
const UpdateCheckService = require('./update-check-service');
|
||||
|
||||
const ghostMailer = new GhostMailer();
|
||||
|
||||
function nextCheckTimestamp() {
|
||||
const now = Math.round(new Date().getTime() / 1000);
|
||||
return now + (24 * 3600);
|
||||
}
|
||||
const updateChecker = new UpdateCheckService({
|
||||
api,
|
||||
config,
|
||||
i18n,
|
||||
logging,
|
||||
urlUtils,
|
||||
request,
|
||||
ghostVersion,
|
||||
ghostMailer
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Centralised error handler for the update check unit.
|
||||
*
|
||||
* CASES:
|
||||
* - the update check service returns an error
|
||||
* - error during collecting blog stats
|
||||
*
|
||||
* We still need to ensure that we set the "next_update_check" to a new value, otherwise no more
|
||||
* update checks will happen.
|
||||
*
|
||||
* @param err
|
||||
*/
|
||||
function updateCheckError(err) {
|
||||
api.settings.edit({
|
||||
settings: [{
|
||||
key: 'next_update_check',
|
||||
value: nextCheckTimestamp()
|
||||
}]
|
||||
}, internal);
|
||||
module.exports = () => {
|
||||
const allowedCheckEnvironments = ['development', 'production'];
|
||||
|
||||
err.context = i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
|
||||
err.help = i18n.t('errors.updateCheck.checkingForUpdatesFailed.help', {url: 'https://ghost.org/docs/'});
|
||||
logging.error(err);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Create a Ghost notification and call the API controller.
|
||||
*
|
||||
* @param {Object} notification
|
||||
* @return {Promise}
|
||||
*/
|
||||
async function createCustomNotification(notification) {
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {users} = await api.users.browse(Object.assign({
|
||||
limit: 'all',
|
||||
include: ['roles']
|
||||
}, internal));
|
||||
|
||||
const adminEmails = users
|
||||
.filter(user => ['Owner', 'Administrator'].includes(user.roles[0].name))
|
||||
.map(user => user.email);
|
||||
|
||||
const siteUrl = config.get('url');
|
||||
|
||||
for (const message of notification.messages) {
|
||||
const toAdd = {
|
||||
// @NOTE: the update check service returns "0" or "1" (https://github.com/TryGhost/UpdateCheck/issues/43)
|
||||
custom: !!notification.custom,
|
||||
createdAt: moment(notification.created_at).toDate(),
|
||||
status: message.status || 'alert',
|
||||
type: message.type || 'info',
|
||||
id: message.id,
|
||||
dismissible: Object.prototype.hasOwnProperty.call(message, 'dismissible') ? message.dismissible : true,
|
||||
top: !!message.top,
|
||||
message: message.content
|
||||
};
|
||||
|
||||
if (toAdd.type === 'alert') {
|
||||
for (const email of adminEmails) {
|
||||
try {
|
||||
ghostMailer.send({
|
||||
to: email,
|
||||
subject: `Action required: Critical alert from Ghost instance ${siteUrl}`,
|
||||
html: toAdd.message,
|
||||
forceTextContent: true
|
||||
});
|
||||
} catch (err) {
|
||||
logging.err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug('Add Custom Notification', toAdd);
|
||||
await api.notifications.add({notifications: [toAdd]}, {context: {internal: true}});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Collect stats from your blog.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function updateCheckData() {
|
||||
let data = {};
|
||||
let mailConfig = config.get('mail');
|
||||
|
||||
data.ghost_version = ghostVersion.original;
|
||||
data.node_version = process.versions.node;
|
||||
data.env = config.get('env');
|
||||
data.database_type = config.get('database').client;
|
||||
data.email_transport = mailConfig &&
|
||||
(mailConfig.options && mailConfig.options.service ?
|
||||
mailConfig.options.service :
|
||||
mailConfig.transport);
|
||||
|
||||
return Promise.props({
|
||||
hash: api.settings.read(_.extend({key: 'db_hash'}, internal)).reflect(),
|
||||
theme: api.settings.read(_.extend({key: 'active_theme'}, internal)).reflect(),
|
||||
posts: api.posts.browse().reflect(),
|
||||
users: api.users.browse(internal).reflect(),
|
||||
npm: Promise.promisify(exec)('npm -v').reflect()
|
||||
}).then(function (descriptors) {
|
||||
const hash = descriptors.hash.value().settings[0];
|
||||
const theme = descriptors.theme.value().settings[0];
|
||||
const posts = descriptors.posts.value();
|
||||
const users = descriptors.users.value();
|
||||
const npm = descriptors.npm.value();
|
||||
const blogUrl = urlUtils.urlFor('home', true);
|
||||
const parsedBlogUrl = url.parse(blogUrl);
|
||||
const blogId = parsedBlogUrl.hostname + parsedBlogUrl.pathname.replace(/\//, '') + hash.value;
|
||||
|
||||
data.url = blogUrl;
|
||||
data.blog_id = crypto.createHash('md5').update(blogId).digest('hex');
|
||||
data.theme = theme ? theme.value : '';
|
||||
data.post_count = posts && posts.meta && posts.meta.pagination ? posts.meta.pagination.total : 0;
|
||||
data.user_count = users && users.users && users.users.length ? users.users.length : 0;
|
||||
data.blog_created_at = users && users.users && users.users[0] && users.users[0].created_at ? moment(users.users[0].created_at).unix() : '';
|
||||
data.npm_version = npm.trim();
|
||||
|
||||
return data;
|
||||
}).catch(updateCheckError);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Perform request to update check service.
|
||||
*
|
||||
* With the privacy setting `useUpdateCheck` you can control if you want to expose data/stats from your blog to the
|
||||
* service. Enabled or disabled, you will receive the latest notification available from the service.
|
||||
*
|
||||
* @see https://ghost.org/docs/concepts/config/#privacy
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function updateCheckRequest() {
|
||||
return updateCheckData()
|
||||
.then(function then(reqData) {
|
||||
let reqObj = {
|
||||
timeout: 1000,
|
||||
headers: {}
|
||||
};
|
||||
|
||||
let checkEndpoint = config.get('updateCheck:url');
|
||||
let checkMethod = config.isPrivacyDisabled('useUpdateCheck') ? 'GET' : 'POST';
|
||||
|
||||
// CASE: Expose stats and do a check-in
|
||||
if (checkMethod === 'POST') {
|
||||
reqObj.json = true;
|
||||
reqObj.body = reqData;
|
||||
reqObj.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(reqData));
|
||||
reqObj.headers['Content-Type'] = 'application/json';
|
||||
} else {
|
||||
reqObj.json = true;
|
||||
reqObj.query = {
|
||||
ghost_version: reqData.ghost_version
|
||||
};
|
||||
}
|
||||
|
||||
debug('Request Update Check Service', checkEndpoint);
|
||||
|
||||
return request(checkEndpoint, reqObj)
|
||||
.then(function (response) {
|
||||
return response.body;
|
||||
})
|
||||
.catch(function (err) {
|
||||
// CASE: no notifications available, ignore
|
||||
if (err.statusCode === 404) {
|
||||
return {
|
||||
next_check: nextCheckTimestamp(),
|
||||
notifications: []
|
||||
};
|
||||
}
|
||||
|
||||
// CASE: service returns JSON error, deserialize into JS error
|
||||
if (err.response && err.response.body && typeof err.response.body === 'object') {
|
||||
err = errors.utils.deserialize(err.response.body);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This function handles the response from the update check service.
|
||||
*
|
||||
* The helper does three things:
|
||||
*
|
||||
* 1. Updates the time in the settings table to know when we can execute the next update check request.
|
||||
* 2. Iterates over the received notifications and filters them out based on your notification groups.
|
||||
* 3. Calls a custom helper to generate a Ghost notification for the database.
|
||||
*
|
||||
* The structure of the response is:
|
||||
*
|
||||
* {
|
||||
* id: 20,
|
||||
* version: 'all4',
|
||||
* messages:
|
||||
* [{
|
||||
* id: 'f8ff6c80-aa61-11e7-a126-6119te37e2b8',
|
||||
* version: '^2',
|
||||
* content: 'Hallouuuu custom',
|
||||
* top: true,
|
||||
* dismissible: true,
|
||||
* type: 'info'
|
||||
* }],
|
||||
* created_at: '2021-10-06T07:00:00.000Z',
|
||||
* custom: 1,
|
||||
* next_check: 1555608722
|
||||
* }
|
||||
*
|
||||
*
|
||||
* Example for grouped custom notifications in config:
|
||||
*
|
||||
* "notificationGroups": ["migration", "something"]
|
||||
*
|
||||
* The group 'all' is a reserved name for general custom notifications, which every self hosted blog can receive.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @return {Promise}
|
||||
*/
|
||||
async function updateCheckResponse(response) {
|
||||
let notifications = [];
|
||||
let notificationGroups = (config.get('notificationGroups') || []).concat(['all']);
|
||||
|
||||
debug('Notification Groups', notificationGroups);
|
||||
debug('Response Update Check Service', response);
|
||||
|
||||
await api.settings.edit({
|
||||
settings: [{
|
||||
key: 'next_update_check',
|
||||
value: response.next_check
|
||||
}]
|
||||
}, internal);
|
||||
|
||||
/**
|
||||
* @NOTE:
|
||||
*
|
||||
* When we refactored notifications in Ghost 1.20, the service did not support returning multiple messages.
|
||||
* But we wanted to already add the support for future functionality.
|
||||
* That's why this helper supports two ways: returning an array of messages or returning an object with
|
||||
* a "notifications" key. The second one is probably the best, because we need to support "next_check"
|
||||
* on the root level of the response.
|
||||
*/
|
||||
if (_.isArray(response)) {
|
||||
notifications = response;
|
||||
} else if ((Object.prototype.hasOwnProperty.call(response, 'notifications') && _.isArray(response.notifications))) {
|
||||
notifications = response.notifications;
|
||||
} else {
|
||||
// CASE: default right now
|
||||
notifications = [response];
|
||||
}
|
||||
|
||||
// CASE: Hook into received notifications and decide whether you are allowed to receive custom group messages.
|
||||
if (notificationGroups.length) {
|
||||
notifications = notifications.filter(function (notification) {
|
||||
// CASE: release notification, keep
|
||||
if (!notification.custom) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// CASE: filter out messages based on your groups
|
||||
return _.includes(notificationGroups.map(function (groupIdentifier) {
|
||||
if (notification.version.match(new RegExp(groupIdentifier))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}), true) === true;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.each(notifications, createCustomNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Entry point to trigger the update check unit.
|
||||
*
|
||||
* Based on a settings value, we check if `next_update_check` is less than now to decide whether
|
||||
* we should request the update check service (http://updates.ghost.org) or not.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function updateCheck() {
|
||||
// CASE: The check will not happen if your NODE_ENV is not in the allowed defined environments.
|
||||
if (_.indexOf(allowedCheckEnvironments, process.env.NODE_ENV) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.settings.read(_.extend({key: 'next_update_check'}, internal));
|
||||
|
||||
const nextUpdateCheck = result.settings[0];
|
||||
|
||||
// CASE: Next update check should happen now?
|
||||
// @NOTE: You can skip this check by adding a config value. This is helpful for developing.
|
||||
if (!config.get('updateCheck:forceUpdate') && nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await updateCheckRequest();
|
||||
|
||||
await updateCheckResponse(response);
|
||||
} catch (err) {
|
||||
updateCheckError(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = updateCheck;
|
||||
updateChecker.check();
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user