ghost 4.13.0 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +1 -1
- package/content/themes/casper/default.hbs +2 -2
- package/content/themes/casper/package.json +1 -1
- package/content/themes/casper/page.hbs +28 -26
- package/content/themes/casper/partials/post-card.hbs +2 -2
- package/content/themes/casper/post.hbs +67 -65
- package/content/themes/casper/tag.hbs +2 -2
- package/core/built/assets/{chunk.3.f80c7fbb7573ce508a05.js → chunk.3.4b1d9e20e57164ac9c29.js} +31 -29
- package/core/built/assets/ghost-dark-bb2831fc27fcb02893ed0a761207dc63.css +1 -0
- package/core/built/assets/{ghost.min-ba7f03a78d7d98444af386b8ae9347a7.js → ghost.min-d1d99f3ed6e0f427874b2a11e7078475.js} +1777 -1685
- package/core/built/assets/ghost.min-e7612edfa72b0fe2c201b387923e6fc7.css +1 -0
- package/core/built/assets/icons/check-2.svg +1 -0
- package/core/built/assets/icons/discount-bubble.svg +1 -0
- package/core/built/assets/icons/no-data-line-chart.svg +1 -0
- package/core/built/assets/icons/no-data-list.svg +9 -8
- package/core/built/assets/icons/no-data-subscription.svg +1 -0
- package/core/built/assets/{vendor.min-29784d514390cb5abc74ae660cb2fbc7.js → vendor.min-3660ec7864887f1496fe7a27fd23ab76.js} +1570 -1289
- package/core/frontend/helpers/ghost_head.js +7 -1
- package/core/frontend/helpers/match.js +19 -4
- package/core/frontend/helpers/products.js +68 -0
- package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
- package/core/frontend/services/routing/controllers/email-post.js +3 -2
- package/core/frontend/services/settings/loader.js +2 -2
- package/core/frontend/services/sitemap/base-generator.js +12 -8
- package/core/frontend/services/sitemap/manager.js +1 -1
- package/core/frontend/services/theme-engine/handlebars/helpers.js +1 -0
- package/core/frontend/services/theme-engine/middleware.js +4 -1
- package/core/server/api/canary/email-preview.js +15 -33
- package/core/server/api/canary/integrations.js +7 -30
- package/core/server/api/canary/labels.js +8 -9
- package/core/server/api/canary/members.js +13 -9
- package/core/server/api/canary/schedules.js +9 -57
- package/core/server/api/canary/settings.js +20 -158
- package/core/server/api/canary/themes.js +5 -59
- package/core/server/api/canary/utils/serializers/output/members.js +2 -14
- package/core/server/api/canary/utils/validators/input/settings.js +23 -1
- package/core/server/api/canary/webhooks.js +6 -24
- package/core/server/api/v2/schedules.js +9 -57
- package/core/server/api/v3/email-preview.js +15 -28
- package/core/server/api/v3/integrations.js +7 -30
- package/core/server/api/v3/labels.js +8 -9
- package/core/server/api/v3/members.js +4 -1
- package/core/server/api/v3/schedules.js +9 -57
- package/core/server/api/v3/settings.js +13 -132
- package/core/server/api/v3/utils/validators/input/settings.js +23 -1
- package/core/server/api/v3/webhooks.js +6 -28
- package/core/server/data/exporter/table-lists.js +1 -0
- package/core/server/data/importer/import-manager.js +398 -0
- package/core/server/data/importer/importers/data/data-importer.js +162 -0
- package/core/server/data/importer/importers/data/index.js +1 -162
- package/core/server/data/importer/index.js +1 -379
- package/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js +70 -0
- package/core/server/data/migrations/versions/4.14/02-fix-free-members-status-events.js +60 -0
- package/core/server/data/migrations/versions/4.15/01-add-temp-members-analytic-events-table.js +12 -0
- package/core/server/data/migrations/versions/4.16/01-add-custom-theme-settings-table.js +9 -0
- package/core/server/data/schema/fixtures/utils.js +6 -1
- package/core/server/data/schema/schema.js +26 -0
- package/core/server/lib/request-external.js +3 -2
- package/core/server/models/action.js +1 -1
- package/core/server/models/api-key.js +1 -1
- package/core/server/models/base/bookshelf.js +0 -3
- package/core/server/models/base/index.js +2 -0
- package/core/server/models/base/plugins/events.js +2 -2
- package/core/server/models/base/plugins/raw-knex.js +10 -10
- package/core/server/models/custom-theme-setting.js +9 -0
- package/core/server/models/email.js +2 -2
- package/core/server/models/index.js +2 -0
- package/core/server/models/integration.js +1 -1
- package/core/server/models/label.js +2 -2
- package/core/server/models/member-analytic-event.js +9 -0
- package/core/server/models/member.js +2 -2
- package/core/server/models/post.js +2 -2
- package/core/server/models/settings.js +2 -2
- package/core/server/models/tag.js +2 -2
- package/core/server/models/user.js +2 -2
- package/core/server/models/webhook.js +2 -2
- package/core/server/services/bulk-email/bulk-email-processor.js +1 -4
- package/core/server/services/custom-theme-settings.js +8 -0
- package/core/server/services/integrations/integrations-service.js +61 -0
- package/core/server/services/mail/GhostMailer.js +29 -37
- package/core/server/services/mega/email-preview.js +41 -0
- package/core/server/services/mega/index.js +4 -0
- package/core/server/services/mega/mega.js +27 -23
- package/core/server/services/mega/post-email-serializer.js +28 -21
- package/core/server/services/mega/template.js +11 -0
- package/core/server/services/members/api.js +1 -0
- package/core/server/services/members/emails/signup.js +1 -1
- package/core/server/services/oembed.js +7 -2
- package/core/server/services/posts/post-scheduling-service.js +100 -0
- package/core/server/services/settings/index.js +13 -16
- package/core/server/services/settings/settings-bread-service.js +188 -0
- package/core/server/services/settings/settings-utils.js +32 -0
- package/core/server/services/themes/ThemeStorage.js +5 -4
- package/core/server/services/themes/activation-bridge.js +14 -0
- package/core/server/services/themes/index.js +2 -0
- package/core/server/services/themes/installer.js +72 -0
- package/core/server/services/themes/validate.js +5 -2
- package/core/server/services/webhooks/webhooks-service.js +55 -0
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/members/app.js +3 -0
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/custom-theme-settings-cache.js +3 -0
- package/core/shared/i18n/translations/en.json +1 -6
- package/core/shared/labs.js +5 -7
- package/package.json +64 -62
- package/yarn.lock +1490 -1055
- package/core/built/assets/ghost-dark-98d56e4973a502750748090f9dbc8280.css +0 -1
- package/core/built/assets/ghost.min-6932a664a1cb92a8e4a15f540cae3ad8.css +0 -1
- package/core/server/services/mega/template-labs.js +0 -1024
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Usage: `{{ghost_head}}`
|
|
3
3
|
//
|
|
4
4
|
// Outputs scripts and other assets at the top of a Ghost theme
|
|
5
|
-
const {metaData, escapeExpression, SafeString, logging, settingsCache, config, blogIcon, urlUtils} = require('../services/proxy');
|
|
5
|
+
const {metaData, escapeExpression, SafeString, logging, settingsCache, config, blogIcon, urlUtils, labs} = require('../services/proxy');
|
|
6
6
|
const _ = require('lodash');
|
|
7
7
|
const debug = require('@tryghost/debug')('ghost_head');
|
|
8
8
|
const templateStyles = require('./tpl/styles');
|
|
@@ -176,6 +176,12 @@ module.exports = function ghost_head(options) { // eslint-disable-line camelcase
|
|
|
176
176
|
head.push('<meta name="generator" content="Ghost ' +
|
|
177
177
|
escapeExpression(safeVersion) + '" />');
|
|
178
178
|
|
|
179
|
+
// Ghost analytics tag
|
|
180
|
+
if (labs.isSet('membersActivity')) {
|
|
181
|
+
const postId = (dataRoot && dataRoot.post) ? dataRoot.post.id : '';
|
|
182
|
+
head.push(writeMetaTag('ghost-analytics-id', postId, 'name'));
|
|
183
|
+
}
|
|
184
|
+
|
|
179
185
|
head.push('<link rel="alternate" type="application/rss+xml" title="' +
|
|
180
186
|
escapeExpression(meta.site.title) + '" href="' +
|
|
181
187
|
escapeExpression(meta.rssUrl) + '" />');
|
|
@@ -2,20 +2,35 @@ const {logging, i18n, SafeString, labs} = require('../services/proxy');
|
|
|
2
2
|
const _ = require('lodash');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* This is identical to the built-in if helper
|
|
5
|
+
* This is identical to the built-in if helper, except inverse/fn calls are replaced with false/true
|
|
6
|
+
* https://github.com/handlebars-lang/handlebars.js/blob/19bdace85a8d0bc5ed3a4dec4071cb08c8d003f2/lib/handlebars/helpers/if.js#L9-L20
|
|
6
7
|
*/
|
|
8
|
+
function isEmptyValue(value) {
|
|
9
|
+
if (!value && value !== 0) {
|
|
10
|
+
return true;
|
|
11
|
+
} else if (Array.isArray(value) && value.length === 0) {
|
|
12
|
+
return true;
|
|
13
|
+
} else {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
const handleConditional = (conditional, options) => {
|
|
8
19
|
if (_.isFunction(conditional)) {
|
|
9
20
|
conditional = conditional.call(this);
|
|
10
21
|
}
|
|
11
22
|
|
|
23
|
+
if (conditional instanceof SafeString) {
|
|
24
|
+
conditional = conditional.string;
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
// Default behavior is to render the positive path if the value is truthy and not empty.
|
|
13
28
|
// The `includeZero` option may be set to treat the condtional as purely not empty based on the
|
|
14
29
|
// behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative.
|
|
15
|
-
if ((!options.hash.includeZero && !conditional) ||
|
|
16
|
-
return true;
|
|
17
|
-
} else {
|
|
30
|
+
if ((!options.hash.includeZero && !conditional) || isEmptyValue(conditional)) {
|
|
18
31
|
return false;
|
|
32
|
+
} else {
|
|
33
|
+
return true;
|
|
19
34
|
}
|
|
20
35
|
};
|
|
21
36
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// # Products Helper
|
|
2
|
+
// Usage: `{{products}}`, `{{products separator=' - '}}`
|
|
3
|
+
//
|
|
4
|
+
// Returns a string of the products with access to the post.
|
|
5
|
+
// By default, products are separated by commas.
|
|
6
|
+
|
|
7
|
+
const nql = require('@nexes/nql');
|
|
8
|
+
const isString = require('lodash/isString');
|
|
9
|
+
const {SafeString, labs} = require('../services/proxy');
|
|
10
|
+
|
|
11
|
+
function products(options = {}) {
|
|
12
|
+
options = options || {};
|
|
13
|
+
options.hash = options.hash || {};
|
|
14
|
+
|
|
15
|
+
const separator = isString(options.hash.separator) ? options.hash.separator : '';
|
|
16
|
+
let output = '';
|
|
17
|
+
|
|
18
|
+
let productsList = [];
|
|
19
|
+
if (options.data.product) {
|
|
20
|
+
productsList = [options.data.product];
|
|
21
|
+
}
|
|
22
|
+
if (options.data.products) {
|
|
23
|
+
productsList = options.data.products;
|
|
24
|
+
}
|
|
25
|
+
let accessProductsList = [];
|
|
26
|
+
|
|
27
|
+
if (['members', 'paid', 'public'].includes(this.visibility)) {
|
|
28
|
+
accessProductsList = productsList;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (this.visibility === 'filter') {
|
|
32
|
+
const nqlFilter = nql(this.visibility_filter);
|
|
33
|
+
accessProductsList = productsList.filter((product) => {
|
|
34
|
+
return nqlFilter.queryJSON({product: product.slug});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (accessProductsList.length > 0) {
|
|
39
|
+
const productNames = accessProductsList.map(product => product.name);
|
|
40
|
+
if (accessProductsList.length === 1) {
|
|
41
|
+
output = productNames[0] + ' tier';
|
|
42
|
+
} else {
|
|
43
|
+
if (separator) {
|
|
44
|
+
output = productNames.join(separator) + ' tiers';
|
|
45
|
+
} else {
|
|
46
|
+
const firsts = productNames.slice(0, productNames.length - 1);
|
|
47
|
+
const last = productNames[productNames.length - 1];
|
|
48
|
+
output = firsts.join(', ') + ' and ' + last + ' tiers';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return new SafeString(output);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = function productsLabsWrapper() {
|
|
57
|
+
let self = this;
|
|
58
|
+
let args = arguments;
|
|
59
|
+
|
|
60
|
+
return labs.enabledHelper({
|
|
61
|
+
flagKey: 'multipleProducts',
|
|
62
|
+
flagName: 'Tiers',
|
|
63
|
+
helperName: 'products',
|
|
64
|
+
helpUrl: 'https://ghost.org/docs/themes/'
|
|
65
|
+
}, () => {
|
|
66
|
+
return products.apply(self, args);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<h2>This post is for subscribers only</h2>
|
|
9
9
|
{{/has}}
|
|
10
10
|
{{#has visibility="filter"}}
|
|
11
|
-
<h2>This post is for subscribers on
|
|
11
|
+
<h2>This post is for subscribers on the {{products}} only </h2>
|
|
12
12
|
{{/has}}
|
|
13
13
|
{{#if @member}}
|
|
14
14
|
<a class="gh-btn" data-portal="account/plans" style="color:{{accentColor}}">Upgrade your account</a>
|
|
@@ -3,6 +3,7 @@ const config = require('../../../../shared/config');
|
|
|
3
3
|
const urlService = require('../../url');
|
|
4
4
|
const urlUtils = require('../../../../shared/url-utils');
|
|
5
5
|
const helpers = require('../helpers');
|
|
6
|
+
const labs = require('../../../../shared/labs');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @description Email Post Controller.
|
|
@@ -11,7 +12,7 @@ const helpers = require('../helpers');
|
|
|
11
12
|
* @param {Function} next
|
|
12
13
|
* @returns {Promise}
|
|
13
14
|
*/
|
|
14
|
-
module.exports = function emailPostController(req, res, next) {
|
|
15
|
+
module.exports = [labs.enabledMiddleware('emailOnlyPosts'), function emailPostController(req, res, next) {
|
|
15
16
|
debug('emailPostController');
|
|
16
17
|
|
|
17
18
|
const api = require('../../proxy').api[res.locals.apiVersion];
|
|
@@ -62,4 +63,4 @@ module.exports = function emailPostController(req, res, next) {
|
|
|
62
63
|
return renderer(post);
|
|
63
64
|
})
|
|
64
65
|
.catch(helpers.handleError(next));
|
|
65
|
-
};
|
|
66
|
+
}];
|
|
@@ -22,7 +22,7 @@ const getSettingFilePath = (setting) => {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Functionally same as loadSettingsSync with exception of loading
|
|
25
|
-
*
|
|
25
|
+
* settings asynchronously. This method is used at new places to read settings
|
|
26
26
|
* to prevent blocking the eventloop
|
|
27
27
|
*
|
|
28
28
|
* @param {String} setting the requested settings as defined in setting knownSettings
|
|
@@ -56,7 +56,7 @@ const loadSettings = async (setting) => {
|
|
|
56
56
|
/**
|
|
57
57
|
* Reads the desired settings YAML file and passes the
|
|
58
58
|
* file to the YAML parser which then returns a JSON object.
|
|
59
|
-
* NOTE: loading happens
|
|
59
|
+
* NOTE: loading happens synchronously
|
|
60
60
|
*
|
|
61
61
|
* @param {String} setting the requested settings as defined in setting knownSettings
|
|
62
62
|
* @returns {Object} settingsFile
|
|
@@ -19,30 +19,34 @@ class BaseSiteMapGenerator {
|
|
|
19
19
|
this.nodeTimeLookup = {};
|
|
20
20
|
this.siteMapContent = null;
|
|
21
21
|
this.lastModified = 0;
|
|
22
|
+
this.maxNodes = 50000;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
generateXmlFromNodes() {
|
|
25
|
-
const self = this;
|
|
26
|
-
|
|
27
26
|
// Get a mapping of node to timestamp
|
|
28
|
-
|
|
27
|
+
let nodesToProcess = _.map(this.nodeLookup, (node, id) => {
|
|
29
28
|
return {
|
|
30
29
|
id: id,
|
|
31
30
|
// Using negative here to sort newest to oldest
|
|
32
|
-
ts: -(
|
|
31
|
+
ts: -(this.nodeTimeLookup[id] || 0),
|
|
33
32
|
node: node
|
|
34
33
|
};
|
|
35
|
-
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Limit to 50k nodes - this is a quick fix to prevent errors in google console
|
|
37
|
+
if (this.maxNodes) {
|
|
38
|
+
nodesToProcess = nodesToProcess.slice(0, this.maxNodes);
|
|
39
|
+
}
|
|
36
40
|
|
|
37
41
|
// Sort nodes by timestamp
|
|
38
|
-
|
|
42
|
+
nodesToProcess = _.sortBy(nodesToProcess, 'ts');
|
|
39
43
|
|
|
40
44
|
// Grab just the nodes
|
|
41
|
-
|
|
45
|
+
nodesToProcess = _.map(nodesToProcess, 'node');
|
|
42
46
|
|
|
43
47
|
const data = {
|
|
44
48
|
// Concat the elements to the _attr declaration
|
|
45
|
-
urlset: [XMLNS_DECLS].concat(
|
|
49
|
+
urlset: [XMLNS_DECLS].concat(nodesToProcess)
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
// Generate full xml
|
|
@@ -15,7 +15,7 @@ class SiteMapManager {
|
|
|
15
15
|
this.posts = options.posts || this.createPostsGenerator(options);
|
|
16
16
|
this.users = this.authors = options.authors || this.createUsersGenerator(options);
|
|
17
17
|
this.tags = options.tags || this.createTagsGenerator(options);
|
|
18
|
-
this.index = options.index || this.createIndexGenerator(
|
|
18
|
+
this.index = options.index || this.createIndexGenerator();
|
|
19
19
|
|
|
20
20
|
events.on('router.created', (router) => {
|
|
21
21
|
if (router.name === 'StaticRoutesRouter') {
|
|
@@ -12,6 +12,7 @@ const registerAllCoreHelpers = function registerAllCoreHelpers() {
|
|
|
12
12
|
registerThemeHelper('cancel_link', coreHelpers.cancel_link);
|
|
13
13
|
registerThemeHelper('concat', coreHelpers.concat);
|
|
14
14
|
registerThemeHelper('content', coreHelpers.content);
|
|
15
|
+
registerThemeHelper('products', coreHelpers.products);
|
|
15
16
|
registerThemeHelper('date', coreHelpers.date);
|
|
16
17
|
registerThemeHelper('encode', coreHelpers.encode);
|
|
17
18
|
registerThemeHelper('excerpt', coreHelpers.excerpt);
|
|
@@ -5,6 +5,7 @@ const {api} = require('../proxy');
|
|
|
5
5
|
const errors = require('@tryghost/errors');
|
|
6
6
|
const tpl = require('@tryghost/tpl');
|
|
7
7
|
const settingsCache = require('../../../shared/settings-cache');
|
|
8
|
+
const customThemeSettingsCache = require('../../../shared/custom-theme-settings-cache');
|
|
8
9
|
const labs = require('../../../shared/labs');
|
|
9
10
|
const activeTheme = require('./active');
|
|
10
11
|
const preview = require('./preview');
|
|
@@ -114,6 +115,7 @@ async function updateGlobalTemplateOptions(req, res, next) {
|
|
|
114
115
|
posts_per_page: activeTheme.get().config('posts_per_page'),
|
|
115
116
|
image_sizes: activeTheme.get().config('image_sizes')
|
|
116
117
|
};
|
|
118
|
+
const themeSettingsData = customThemeSettingsCache.getAll();
|
|
117
119
|
const productData = await getProductAndPricesData();
|
|
118
120
|
const priceData = calculateLegacyPriceData(productData);
|
|
119
121
|
|
|
@@ -136,7 +138,8 @@ async function updateGlobalTemplateOptions(req, res, next) {
|
|
|
136
138
|
config: themeData,
|
|
137
139
|
price: priceData,
|
|
138
140
|
product,
|
|
139
|
-
products
|
|
141
|
+
products,
|
|
142
|
+
custom: themeSettingsData
|
|
140
143
|
}
|
|
141
144
|
});
|
|
142
145
|
}
|
|
@@ -2,7 +2,10 @@ const models = require('../../models');
|
|
|
2
2
|
const i18n = require('../../../shared/i18n');
|
|
3
3
|
const errors = require('@tryghost/errors');
|
|
4
4
|
const mega = require('../../services/mega');
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
const emailPreview = new mega.EmailPreview({
|
|
7
|
+
apiVersion: 'canary'
|
|
8
|
+
});
|
|
6
9
|
|
|
7
10
|
module.exports = {
|
|
8
11
|
docName: 'email_preview',
|
|
@@ -22,34 +25,19 @@ module.exports = {
|
|
|
22
25
|
'status'
|
|
23
26
|
],
|
|
24
27
|
permissions: true,
|
|
25
|
-
query(frame) {
|
|
28
|
+
async query(frame) {
|
|
26
29
|
const options = Object.assign(frame.options, {formats: 'html,plaintext', withRelated: ['authors', 'posts_meta']});
|
|
27
30
|
const data = Object.assign(frame.data, {status: 'all'});
|
|
28
|
-
return models.Post.findOne(data, options)
|
|
29
|
-
.then((model) => {
|
|
30
|
-
if (!model) {
|
|
31
|
-
throw new errors.NotFoundError({
|
|
32
|
-
message: i18n.t('errors.api.posts.postNotFound')
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return mega.postEmailSerializer.serialize(model, {isBrowserPreview: true, apiVersion: 'canary'}).then((emailContent) => {
|
|
37
|
-
if (labs.isSet('emailCardSegments') && frame.options.memberSegment) {
|
|
38
|
-
emailContent = mega.postEmailSerializer.renderEmailForSegment(emailContent, frame.options.memberSegment);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const replacements = mega.postEmailSerializer.parseReplacements(emailContent);
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
emailContent[replacement.format] = emailContent[replacement.format].replace(
|
|
45
|
-
replacement.match,
|
|
46
|
-
replacement.fallback || ''
|
|
47
|
-
);
|
|
48
|
-
});
|
|
32
|
+
const model = await models.Post.findOne(data, options);
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
if (!model) {
|
|
35
|
+
throw new errors.NotFoundError({
|
|
36
|
+
message: i18n.t('errors.api.posts.postNotFound')
|
|
52
37
|
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return emailPreview.generateEmailContent(model, frame.options.memberSegment);
|
|
53
41
|
}
|
|
54
42
|
},
|
|
55
43
|
sendTestEmail: {
|
|
@@ -69,21 +57,15 @@ module.exports = {
|
|
|
69
57
|
async query(frame) {
|
|
70
58
|
const options = Object.assign(frame.options, {status: 'all'});
|
|
71
59
|
let model = await models.Post.findOne(options, {withRelated: ['authors']});
|
|
60
|
+
|
|
72
61
|
if (!model) {
|
|
73
62
|
throw new errors.NotFoundError({
|
|
74
63
|
message: i18n.t('errors.api.posts.postNotFound')
|
|
75
64
|
});
|
|
76
65
|
}
|
|
66
|
+
|
|
77
67
|
const {emails = [], memberSegment} = frame.data;
|
|
78
|
-
|
|
79
|
-
if (response && response[0] && response[0].error) {
|
|
80
|
-
throw new errors.EmailError({
|
|
81
|
-
statusCode: response[0].error.statusCode,
|
|
82
|
-
message: response[0].error.message,
|
|
83
|
-
context: response[0].error.originalMessage
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
return response;
|
|
68
|
+
return await mega.mega.sendTestEmail(model, emails, 'canary', memberSegment);
|
|
87
69
|
}
|
|
88
70
|
}
|
|
89
71
|
};
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
const i18n = require('../../../shared/i18n');
|
|
2
2
|
const errors = require('@tryghost/errors');
|
|
3
3
|
const models = require('../../models');
|
|
4
|
+
const getIntegrationsServiceInstance = require('../../services/integrations/integrations-service');
|
|
5
|
+
|
|
6
|
+
const integrationsService = getIntegrationsServiceInstance({
|
|
7
|
+
IntegrationModel: models.Integration,
|
|
8
|
+
ApiKeyModel: models.ApiKey
|
|
9
|
+
});
|
|
4
10
|
|
|
5
11
|
module.exports = {
|
|
6
12
|
docName: 'integrations',
|
|
@@ -76,36 +82,7 @@ module.exports = {
|
|
|
76
82
|
}
|
|
77
83
|
},
|
|
78
84
|
query({data, options}) {
|
|
79
|
-
|
|
80
|
-
return models.ApiKey.findOne({id: options.keyid})
|
|
81
|
-
.then(async (model) => {
|
|
82
|
-
if (!model) {
|
|
83
|
-
throw new errors.NotFoundError({
|
|
84
|
-
message: i18n.t('errors.api.resource.resourceNotFound', {
|
|
85
|
-
resource: 'ApiKey'
|
|
86
|
-
})
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
await models.ApiKey.refreshSecret(model.toJSON(), Object.assign({}, options, {id: options.keyid}));
|
|
91
|
-
return models.Integration.findOne({id: options.id}, {
|
|
92
|
-
withRelated: ['api_keys', 'webhooks']
|
|
93
|
-
});
|
|
94
|
-
} catch (err) {
|
|
95
|
-
throw new errors.GhostError({
|
|
96
|
-
err: err
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return models.Integration.edit(data, Object.assign(options, {require: true}))
|
|
102
|
-
.catch(models.Integration.NotFoundError, () => {
|
|
103
|
-
throw new errors.NotFoundError({
|
|
104
|
-
message: i18n.t('errors.api.resource.resourceNotFound', {
|
|
105
|
-
resource: 'Integration'
|
|
106
|
-
})
|
|
107
|
-
});
|
|
108
|
-
});
|
|
85
|
+
return integrationsService.edit(data, options);
|
|
109
86
|
}
|
|
110
87
|
},
|
|
111
88
|
add: {
|
|
@@ -76,16 +76,15 @@ module.exports = {
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
permissions: true,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
79
|
+
query(frame) {
|
|
80
|
+
return models.Label.add(frame.data.labels[0], frame.options)
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
|
83
|
+
throw new errors.ValidationError({message: i18n.t('errors.api.labels.labelAlreadyExists')});
|
|
84
|
+
}
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
throw error;
|
|
87
|
+
});
|
|
89
88
|
}
|
|
90
89
|
},
|
|
91
90
|
|
|
@@ -124,8 +124,10 @@ module.exports = {
|
|
|
124
124
|
}, frame.options);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
if (
|
|
128
|
-
|
|
127
|
+
if (!labsService.isSet('multipleProducts')) {
|
|
128
|
+
if (frame.data.members[0].comped) {
|
|
129
|
+
await membersService.api.members.setComplimentarySubscription(member);
|
|
130
|
+
}
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
if (frame.options.send_email) {
|
|
@@ -188,14 +190,16 @@ module.exports = {
|
|
|
188
190
|
|
|
189
191
|
const hasCompedSubscription = !!member.related('stripeSubscriptions').find(sub => sub.get('plan_nickname') === 'Complimentary' && sub.get('status') === 'active');
|
|
190
192
|
|
|
191
|
-
if (
|
|
192
|
-
if (frame.data.members[0].comped
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
if (!labsService.isSet('multipleProducts')) {
|
|
194
|
+
if (typeof frame.data.members[0].comped === 'boolean') {
|
|
195
|
+
if (frame.data.members[0].comped && !hasCompedSubscription) {
|
|
196
|
+
await membersService.api.members.setComplimentarySubscription(member);
|
|
197
|
+
} else if (!(frame.data.members[0].comped) && hasCompedSubscription) {
|
|
198
|
+
await membersService.api.members.cancelComplimentarySubscription(member);
|
|
199
|
+
}
|
|
197
200
|
|
|
198
|
-
|
|
201
|
+
await member.load(['stripeSubscriptions', 'products', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']);
|
|
202
|
+
}
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
await member.load(['stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']);
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
const _ = require('lodash');
|
|
2
|
-
const moment = require('moment');
|
|
3
|
-
const config = require('../../../shared/config');
|
|
4
1
|
const models = require('../../models');
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const errors = require('@tryghost/errors');
|
|
8
|
-
const api = require('./index');
|
|
2
|
+
|
|
3
|
+
const postSchedulingService = require('../../services/posts/post-scheduling-service')('canary');
|
|
9
4
|
|
|
10
5
|
module.exports = {
|
|
11
6
|
docName: 'schedules',
|
|
@@ -32,11 +27,8 @@ module.exports = {
|
|
|
32
27
|
permissions: {
|
|
33
28
|
docName: 'posts'
|
|
34
29
|
},
|
|
35
|
-
query(frame) {
|
|
36
|
-
let resource;
|
|
30
|
+
async query(frame) {
|
|
37
31
|
const resourceType = frame.options.resource;
|
|
38
|
-
const publishAPostBySchedulerToleranceInMinutes = config.get('times').publishAPostBySchedulerToleranceInMinutes;
|
|
39
|
-
|
|
40
32
|
const options = {
|
|
41
33
|
status: 'scheduled',
|
|
42
34
|
id: frame.options.id,
|
|
@@ -45,53 +37,13 @@ module.exports = {
|
|
|
45
37
|
}
|
|
46
38
|
};
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const publishedAtMoment = moment(resource.published_at);
|
|
52
|
-
|
|
53
|
-
if (publishedAtMoment.diff(moment(), 'minutes') > publishAPostBySchedulerToleranceInMinutes) {
|
|
54
|
-
return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.job.notFound')}));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (publishedAtMoment.diff(moment(), 'minutes') < publishAPostBySchedulerToleranceInMinutes * -1 && frame.data.force !== true) {
|
|
58
|
-
return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.job.publishInThePast')}));
|
|
59
|
-
}
|
|
40
|
+
const {scheduledResource, preScheduledResource} = await postSchedulingService.publish(resourceType, frame.options.id, frame.data.force, options);
|
|
41
|
+
const cacheInvalidate = postSchedulingService.handleCacheInvalidation(scheduledResource, preScheduledResource);
|
|
42
|
+
this.headers.cacheInvalidate = cacheInvalidate;
|
|
60
43
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
updated_at: moment(resource.updated_at).toISOString(true)
|
|
65
|
-
}];
|
|
66
|
-
|
|
67
|
-
return api[resourceType].edit(
|
|
68
|
-
editedResource,
|
|
69
|
-
_.pick(options, ['context', 'id', 'transacting', 'forUpdate'])
|
|
70
|
-
);
|
|
71
|
-
})
|
|
72
|
-
.then((result) => {
|
|
73
|
-
const scheduledResource = result[resourceType][0];
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
(scheduledResource.status === 'published' && resource.status !== 'published') ||
|
|
77
|
-
(scheduledResource.status === 'draft' && resource.status === 'published')
|
|
78
|
-
) {
|
|
79
|
-
this.headers.cacheInvalidate = true;
|
|
80
|
-
} else if (
|
|
81
|
-
(scheduledResource.status === 'draft' && resource.status !== 'published') ||
|
|
82
|
-
(scheduledResource.status === 'scheduled' && resource.status !== 'scheduled')
|
|
83
|
-
) {
|
|
84
|
-
this.headers.cacheInvalidate = {
|
|
85
|
-
value: urlUtils.urlFor({
|
|
86
|
-
relativeUrl: urlUtils.urlJoin('/p', scheduledResource.uuid, '/')
|
|
87
|
-
})
|
|
88
|
-
};
|
|
89
|
-
} else {
|
|
90
|
-
this.headers.cacheInvalidate = false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return result;
|
|
94
|
-
});
|
|
44
|
+
const response = {};
|
|
45
|
+
response[resourceType] = [scheduledResource];
|
|
46
|
+
return response;
|
|
95
47
|
}
|
|
96
48
|
},
|
|
97
49
|
|