2019-09-23 19:12:53 +03:00
|
|
|
const Promise = require('bluebird');
|
|
|
|
const moment = require('moment');
|
|
|
|
const jwt = require('jsonwebtoken');
|
|
|
|
const localUtils = require('../utils');
|
2021-04-27 16:18:04 +03:00
|
|
|
const events = require('../../../lib/common/events');
|
2021-05-03 19:29:44 +03:00
|
|
|
const i18n = require('../../../../shared/i18n');
|
2020-05-22 21:22:20 +03:00
|
|
|
const errors = require('@tryghost/errors');
|
2019-09-23 19:12:53 +03:00
|
|
|
const models = require('../../../models');
|
2020-05-28 13:57:02 +03:00
|
|
|
const urlUtils = require('../../../../shared/url-utils');
|
2019-09-23 19:12:53 +03:00
|
|
|
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) {
|
2020-05-22 21:22:20 +03:00
|
|
|
throw new errors.NotFoundError({
|
|
|
|
message: i18n.t('errors.api.resource.resourceNotFound', {
|
2019-09-23 19:12:53 +03:00
|
|
|
resource: 'Integration'
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return integration.toJSON();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @description Get signed admin token for making authenticated scheduling requests
|
|
|
|
*
|
|
|
|
* @return {Promise}
|
|
|
|
*/
|
2019-11-19 17:29:55 +03:00
|
|
|
_private.getSignedAdminToken = function ({publishedAt, apiUrl, integration}) {
|
2019-09-23 19:12:53 +03:00
|
|
|
let key = integration.api_keys[0];
|
|
|
|
|
|
|
|
const JWT_OPTIONS = {
|
|
|
|
keyid: key.id,
|
|
|
|
algorithm: 'HS256',
|
2019-11-19 17:29:55 +03:00
|
|
|
audience: apiUrl,
|
|
|
|
noTimestamp: true
|
2019-09-23 19:12:53 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
// 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
|
2019-11-19 17:29:55 +03:00
|
|
|
let tokenExpiry = moment(publishedAt).add(6, 'h');
|
2019-09-23 19:12:53 +03:00
|
|
|
if (tokenExpiry.isBefore(moment())) {
|
|
|
|
tokenExpiry = moment().add(6, 'h');
|
|
|
|
}
|
|
|
|
|
|
|
|
return jwt.sign(
|
|
|
|
{
|
|
|
|
exp: tokenExpiry.unix(),
|
2019-11-19 17:29:55 +03:00
|
|
|
nbf: moment(publishedAt).subtract(10, 'm').unix()
|
2019-09-23 19:12:53 +03:00
|
|
|
},
|
|
|
|
Buffer.from(key.secret, 'hex'),
|
|
|
|
JWT_OPTIONS
|
|
|
|
);
|
|
|
|
};
|
2016-05-19 14:49:22 +03:00
|
|
|
|
2019-05-01 23:05:42 +03:00
|
|
|
/**
|
|
|
|
* @description Normalize model data into scheduler notation.
|
|
|
|
* @param {Object} options
|
|
|
|
* @return {Object}
|
|
|
|
*/
|
2019-11-19 17:29:55 +03:00
|
|
|
_private.normalize = function normalize({model, apiUrl, resourceType, integration}, event = '') {
|
2019-09-23 19:12:53 +03:00
|
|
|
const resource = `${resourceType}s`;
|
2019-11-19 17:29:55 +03:00
|
|
|
let publishedAt = (event === 'unscheduled') ? model.previous('published_at') : model.get('published_at');
|
|
|
|
const signedAdminToken = _private.getSignedAdminToken({publishedAt, apiUrl, integration});
|
2019-09-23 19:12:53 +03:00
|
|
|
let url = `${urlUtils.urlJoin(apiUrl, 'schedules', resource, model.get('id'))}/?token=${signedAdminToken}`;
|
2016-05-19 14:49:22 +03:00
|
|
|
return {
|
2019-07-31 23:34:49 +03:00
|
|
|
// NOTE: The scheduler expects a unix timestamp.
|
2019-11-19 17:29:55 +03:00
|
|
|
time: moment(publishedAt).valueOf(),
|
2019-09-23 19:12:53 +03:00
|
|
|
url: url,
|
2016-05-19 14:49:22 +03:00
|
|
|
extra: {
|
|
|
|
httpMethod: 'PUT',
|
2019-05-01 23:05:42 +03:00
|
|
|
oldTime: model.previous('published_at') ? moment(model.previous('published_at')).valueOf() : null
|
2016-05-19 14:49:22 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2019-05-01 23:05:42 +03:00
|
|
|
/**
|
2019-09-23 19:12:53 +03:00
|
|
|
* @description Load all scheduled posts/pages from database.
|
2019-05-01 23:05:42 +03:00
|
|
|
* @return {Promise}
|
|
|
|
*/
|
2019-09-23 19:12:53 +03:00
|
|
|
_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]
|
|
|
|
});
|
|
|
|
}, {});
|
|
|
|
});
|
2016-05-19 14:49:22 +03:00
|
|
|
};
|
|
|
|
|
2019-05-01 23:05:42 +03:00
|
|
|
/**
|
|
|
|
* @description Initialise post scheduling.
|
|
|
|
* @param {Object} options
|
|
|
|
* @return {*}
|
|
|
|
*/
|
2018-06-26 01:46:31 +03:00
|
|
|
exports.init = function init(options = {}) {
|
|
|
|
const {apiUrl} = options;
|
2019-09-23 19:12:53 +03:00
|
|
|
let adapter = null;
|
|
|
|
let integration = null;
|
2016-05-19 14:49:22 +03:00
|
|
|
|
2018-06-26 01:46:31 +03:00
|
|
|
if (!Object.keys(options).length) {
|
2020-05-22 21:22:20 +03:00
|
|
|
return Promise.reject(new errors.IncorrectUsageError({message: 'post-scheduling: no config was provided'}));
|
2016-05-19 14:49:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!apiUrl) {
|
2020-05-22 21:22:20 +03:00
|
|
|
return Promise.reject(new errors.IncorrectUsageError({message: 'post-scheduling: no apiUrl was provided'}));
|
2016-05-19 14:49:22 +03:00
|
|
|
}
|
|
|
|
|
2019-09-23 19:12:53 +03:00
|
|
|
return _private.getSchedulerIntegration()
|
|
|
|
.then((_integration) => {
|
|
|
|
integration = _integration;
|
2020-04-05 19:52:48 +03:00
|
|
|
return localUtils.createAdapter();
|
2016-05-19 14:49:22 +03:00
|
|
|
})
|
2018-06-26 01:46:31 +03:00
|
|
|
.then((_adapter) => {
|
2016-05-19 14:49:22 +03:00
|
|
|
adapter = _adapter;
|
2019-05-01 23:05:42 +03:00
|
|
|
|
2017-11-08 02:24:34 +03:00
|
|
|
if (!adapter.rescheduleOnBoot) {
|
|
|
|
return [];
|
|
|
|
}
|
2019-05-01 23:05:42 +03:00
|
|
|
|
2019-09-23 19:12:53 +03:00
|
|
|
return _private.loadScheduledResources();
|
2016-05-19 14:49:22 +03:00
|
|
|
})
|
2019-09-23 19:12:53 +03:00
|
|
|
.then((scheduledResources) => {
|
|
|
|
if (!Object.keys(scheduledResources).length) {
|
2016-05-19 14:49:22 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-09-23 19:12:53 +03:00
|
|
|
// 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) => {
|
2019-11-20 09:46:26 +03:00
|
|
|
adapter.unschedule(_private.normalize({model, apiUrl, integration, resourceType}, 'unscheduled'), {bootstrap: true});
|
2019-11-19 17:29:55 +03:00
|
|
|
adapter.schedule(_private.normalize({model, apiUrl, integration, resourceType}));
|
2019-09-23 19:12:53 +03:00
|
|
|
});
|
2016-05-19 14:49:22 +03:00
|
|
|
});
|
|
|
|
})
|
2018-06-26 01:46:31 +03:00
|
|
|
.then(() => {
|
2016-05-19 14:49:22 +03:00
|
|
|
adapter.run();
|
|
|
|
})
|
2018-06-26 01:46:31 +03:00
|
|
|
.then(() => {
|
2019-09-23 19:12:53 +03:00
|
|
|
SCHEDULED_RESOURCES.forEach((resource) => {
|
2020-05-22 21:22:20 +03:00
|
|
|
events.on(`${resource}.scheduled`, (model) => {
|
2019-09-23 19:12:53 +03:00
|
|
|
adapter.schedule(_private.normalize({model, apiUrl, integration, resourceType: resource}));
|
|
|
|
});
|
2016-05-19 14:49:22 +03:00
|
|
|
|
2019-11-19 17:29:55 +03:00
|
|
|
/** We want to do reschedule as (unschedule + schedule) due to how token(+url) is generated
|
|
|
|
* We want to first remove existing schedule by generating a matching token(+url)
|
|
|
|
* followed by generating a new token(+url) for the new schedule
|
|
|
|
*/
|
2020-05-22 21:22:20 +03:00
|
|
|
events.on(`${resource}.rescheduled`, (model) => {
|
2019-11-19 17:29:55 +03:00
|
|
|
adapter.unschedule(_private.normalize({model, apiUrl, integration, resourceType: resource}, 'unscheduled'));
|
|
|
|
adapter.schedule(_private.normalize({model, apiUrl, integration, resourceType: resource}));
|
2019-09-23 19:12:53 +03:00
|
|
|
});
|
2016-05-19 14:49:22 +03:00
|
|
|
|
2020-05-22 21:22:20 +03:00
|
|
|
events.on(`${resource}.unscheduled`, (model) => {
|
2019-11-19 17:29:55 +03:00
|
|
|
adapter.unschedule(_private.normalize({model, apiUrl, integration, resourceType: resource}, 'unscheduled'));
|
2019-09-23 19:12:53 +03:00
|
|
|
});
|
2016-05-19 14:49:22 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|