Ghost/core/server/adapters/scheduling/post-scheduling/index.js

174 lines
5.8 KiB
JavaScript
Raw Normal View History

const Promise = require('bluebird');
const moment = require('moment');
const jwt = require('jsonwebtoken');
const localUtils = require('../utils');
const common = require('../../../lib/common');
const models = require('../../../models');
const urlUtils = require('../../../lib/url-utils');
const _private = {};
const SCHEDULED_RESOURCES = ['post', 'page'];
/**
* @description Load the internal scheduler integration
*
* @return {Promise}
*/
_private.getSchedulerIntegration = function () {
return models.Integration.findOne({slug: 'ghost-scheduler'}, {withRelated: 'api_keys'})
.then((integration) => {
if (!integration) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.resource.resourceNotFound', {
resource: 'Integration'
})
});
}
return integration.toJSON();
});
};
/**
* @description Get signed admin token for making authenticated scheduling requests
*
* @return {Promise}
*/
_private.getSignedAdminToken = function (options) {
const {model, apiUrl, integration} = options;
let key = integration.api_keys[0];
const JWT_OPTIONS = {
keyid: key.id,
algorithm: 'HS256',
audience: apiUrl
};
// Default token expiry is till 6 hours after scheduled time
// or if published_at is in past then till 6 hours after blog start
// to allow for retries in case of network issues
// and never before 10 mins to publish time
let tokenExpiry = moment(model.get('published_at')).add(6, 'h');
if (tokenExpiry.isBefore(moment())) {
tokenExpiry = moment().add(6, 'h');
}
return jwt.sign(
{
exp: tokenExpiry.unix(),
nbf: moment(model.get('published_at')).subtract(10, 'm').unix()
},
Buffer.from(key.secret, 'hex'),
JWT_OPTIONS
);
};
/**
* @description Normalize model data into scheduler notation.
* @param {Object} options
* @return {Object}
*/
_private.normalize = function normalize(options) {
const {model, apiUrl, resourceType} = options;
const resource = `${resourceType}s`;
const signedAdminToken = _private.getSignedAdminToken(options);
let url = `${urlUtils.urlJoin(apiUrl, 'schedules', resource, model.get('id'))}/?token=${signedAdminToken}`;
return {
// NOTE: The scheduler expects a unix timestamp.
time: moment(model.get('published_at')).valueOf(),
url: url,
extra: {
httpMethod: 'PUT',
oldTime: model.previous('published_at') ? moment(model.previous('published_at')).valueOf() : null
}
};
};
/**
* @description Load all scheduled posts/pages from database.
* @return {Promise}
*/
_private.loadScheduledResources = function () {
const api = require('../../../api');
// Fetches all scheduled resources(posts/pages) with default API
return Promise.mapSeries(SCHEDULED_RESOURCES, (resourceType) => {
return api.schedules.getScheduled.query({
options: {
resource: resourceType
}
}).then((result) => {
return result[resourceType] || [];
});
}).then((results) => {
return SCHEDULED_RESOURCES.reduce(function (obj, entry, index) {
return Object.assign(obj, {
[entry]: results[index]
});
}, {});
});
};
/**
* @description Initialise post scheduling.
* @param {Object} options
* @return {*}
*/
exports.init = function init(options = {}) {
const {apiUrl} = options;
let adapter = null;
let integration = null;
if (!Object.keys(options).length) {
return Promise.reject(new common.errors.IncorrectUsageError({message: 'post-scheduling: no config was provided'}));
}
if (!apiUrl) {
return Promise.reject(new common.errors.IncorrectUsageError({message: 'post-scheduling: no apiUrl was provided'}));
}
return _private.getSchedulerIntegration()
.then((_integration) => {
integration = _integration;
return localUtils.createAdapter(options);
})
.then((_adapter) => {
adapter = _adapter;
if (!adapter.rescheduleOnBoot) {
return [];
}
return _private.loadScheduledResources();
})
.then((scheduledResources) => {
if (!Object.keys(scheduledResources).length) {
return;
}
// Reschedules all scheduled resources on boot
// NOTE: We are using reschedule, because custom scheduling adapter could use a database, which needs to be updated
// and not an in-process implementation!
Object.keys(scheduledResources).forEach((resourceType) => {
scheduledResources[resourceType].forEach((model) => {
adapter.reschedule(_private.normalize({model, apiUrl, integration, resourceType}), {bootstrap: true});
});
});
})
.then(() => {
adapter.run();
})
.then(() => {
SCHEDULED_RESOURCES.forEach((resource) => {
common.events.on(`${resource}.scheduled`, (model) => {
adapter.schedule(_private.normalize({model, apiUrl, integration, resourceType: resource}));
});
common.events.on(`${resource}.rescheduled`, (model) => {
adapter.reschedule(_private.normalize({model, apiUrl, integration, resourceType: resource}));
});
common.events.on(`${resource}.unscheduled`, (model) => {
adapter.unschedule(_private.normalize({model, apiUrl, integration, resourceType: resource}));
});
});
});
};