mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-18 16:01:40 +03:00
d820b961b0
no issue - Switches to used newly added config values throughout the services - Updated the `shouldSendEmail` fn to check if actual value is too far from achieved milestone as determined by the percentage setting (e. g. 998 members should not accidentally receive an email for achieving 100 members)
207 lines
5.8 KiB
JavaScript
207 lines
5.8 KiB
JavaScript
const got = require('got');
|
|
const validator = require('@tryghost/validator');
|
|
const errors = require('@tryghost/errors');
|
|
const ghostVersion = require('@tryghost/version');
|
|
const moment = require('moment');
|
|
|
|
/**
|
|
* @typedef {URL} webhookUrl
|
|
*/
|
|
|
|
/**
|
|
* @typedef {string} siteUrl
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('@tryghost/logging')} logging
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('./SlackNotificationsService').ISlackNotifications} ISlackNotifications
|
|
*/
|
|
|
|
/**
|
|
* @implements {ISlackNotifications}
|
|
*/
|
|
class SlackNotifications {
|
|
/** @type {URL} */
|
|
#webhookUrl;
|
|
|
|
/** @type {siteUrl} */
|
|
#siteUrl;
|
|
|
|
/** @type {logging} */
|
|
#logging;
|
|
|
|
/**
|
|
* @param {object} deps
|
|
* @param {URL} deps.webhookUrl
|
|
* @param {siteUrl} deps.siteUrl
|
|
* @param {logging} deps.logging
|
|
*/
|
|
constructor(deps) {
|
|
this.#siteUrl = deps.siteUrl;
|
|
this.#webhookUrl = deps.webhookUrl;
|
|
this.#logging = deps.logging;
|
|
}
|
|
|
|
/**
|
|
* @param {object} eventData
|
|
* @param {import('@tryghost/milestones/lib/InMemoryMilestoneRepository').Milestone} eventData.milestone
|
|
* @param {object} [eventData.meta]
|
|
* @param {'import'|'email'|'tooFar'} [eventData.meta.reason]
|
|
* @param {number} [eventData.meta.currentARR]
|
|
* @param {number} [eventData.meta.currentMembers]
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async notifyMilestoneReceived({milestone, meta}) {
|
|
const hasImportedMembers = meta?.reason === 'import' ? 'has imported members' : null;
|
|
const lastEmailTooSoon = meta?.reason === 'email' ? 'last email too recent' : null;
|
|
const tooFarFromMilestone = meta?.reason === 'tooFar' ? 'too far from milestone' : null;
|
|
const emailNotSentReason = hasImportedMembers || lastEmailTooSoon || tooFarFromMilestone;
|
|
const milestoneTypePretty = milestone.type === 'arr' ? 'ARR' : 'Members';
|
|
const valueFormatted = this.#getFormattedAmount({amount: milestone.value, currency: milestone?.currency});
|
|
const emailSentText = milestone?.emailSentAt ? this.#getFormattedDate(milestone?.emailSentAt) : `no / ${emailNotSentReason}`;
|
|
const title = `:tada: ${milestoneTypePretty} Milestone ${valueFormatted} reached!`;
|
|
|
|
let valueSection;
|
|
|
|
if (milestone.type === 'arr') {
|
|
valueSection = {
|
|
type: 'section',
|
|
fields: [
|
|
{
|
|
type: 'mrkdwn',
|
|
text: `*Milestone:*\n${valueFormatted}`
|
|
}
|
|
|
|
]
|
|
};
|
|
|
|
if (meta?.currentARR) {
|
|
valueSection.fields.push({
|
|
type: 'mrkdwn',
|
|
text: `*Current ARR:*\n${this.#getFormattedAmount({amount: meta.currentARR, currency: milestone?.currency})}`
|
|
});
|
|
}
|
|
} else {
|
|
valueSection = {
|
|
type: 'section',
|
|
fields: [
|
|
{
|
|
type: 'mrkdwn',
|
|
text: `*Milestone:*\n${valueFormatted}`
|
|
}
|
|
]
|
|
};
|
|
if (meta?.currentMembers) {
|
|
valueSection.fields.push({
|
|
type: 'mrkdwn',
|
|
text: `*Current Members:*\n${this.#getFormattedAmount({amount: meta.currentMembers})}`
|
|
});
|
|
}
|
|
}
|
|
|
|
const blocks = [
|
|
{
|
|
type: 'header',
|
|
text: {
|
|
type: 'plain_text',
|
|
text: title,
|
|
emoji: true
|
|
}
|
|
},
|
|
{
|
|
type: 'section',
|
|
text: {
|
|
type: 'mrkdwn',
|
|
text: `New *${milestoneTypePretty} Milestone* achieved for <${this.#siteUrl}|${this.#siteUrl}>`
|
|
}
|
|
},
|
|
{
|
|
type: 'divider'
|
|
},
|
|
valueSection,
|
|
{
|
|
type: 'section',
|
|
text: {
|
|
type: 'mrkdwn',
|
|
text: `*Email sent:*\n${emailSentText}`
|
|
}
|
|
}
|
|
];
|
|
|
|
const slackData = {
|
|
unfurl_links: false,
|
|
username: 'Ghost Milestone Service',
|
|
attachments: [
|
|
{
|
|
color: '#36a64f',
|
|
blocks
|
|
}
|
|
]
|
|
};
|
|
|
|
await this.send(slackData, this.#webhookUrl);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {object} slackData
|
|
* @param {URL} url
|
|
*
|
|
* @returns {Promise<any>}
|
|
*/
|
|
async send(slackData, url) {
|
|
if ((!url || typeof url !== 'string') || !validator.isURL(url)) {
|
|
const err = new errors.InternalServerError({
|
|
message: 'URL empty or invalid.',
|
|
code: 'URL_MISSING_INVALID',
|
|
context: url
|
|
});
|
|
|
|
return this.#logging.error(err);
|
|
}
|
|
|
|
const requestOptions = {
|
|
body: JSON.stringify(slackData),
|
|
headers: {
|
|
'user-agent': 'Ghost/' + ghostVersion.original + ' (https://github.com/TryGhost/Ghost)'
|
|
}
|
|
};
|
|
|
|
return await got.post(url, requestOptions);
|
|
}
|
|
|
|
/**
|
|
* @param {object} options
|
|
* @param {number} options.amount
|
|
* @param {string} [options.currency]
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
#getFormattedAmount({amount = 0, currency}) {
|
|
if (!currency) {
|
|
return Intl.NumberFormat().format(amount);
|
|
}
|
|
|
|
return Intl.NumberFormat('en', {
|
|
style: 'currency',
|
|
currency,
|
|
currencyDisplay: 'symbol'
|
|
}).format(amount);
|
|
}
|
|
|
|
/**
|
|
* @param {string|Date} date
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
#getFormattedDate(date) {
|
|
return moment(date).format('D MMM YYYY');
|
|
}
|
|
}
|
|
|
|
module.exports = SlackNotifications;
|