Added @tryghost/mentions-email-report package
This package contains the business logic for the sending of mention report emails, it could eventually be included in the webmentions package I think, but has been kept separate for now in favour of smaller packages.
This commit is contained in:
parent
b8b0d1538e
commit
c56b819748
6
ghost/mentions-email-report/.eslintrc.js
Normal file
6
ghost/mentions-email-report/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ['ghost'],
|
||||||
|
extends: [
|
||||||
|
'plugin:ghost/node'
|
||||||
|
]
|
||||||
|
};
|
21
ghost/mentions-email-report/README.md
Normal file
21
ghost/mentions-email-report/README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Mentions Email Report
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
|
||||||
|
## Develop
|
||||||
|
|
||||||
|
This is a monorepo package.
|
||||||
|
|
||||||
|
Follow the instructions for the top-level repo.
|
||||||
|
1. `git clone` this repo & `cd` into it as usual
|
||||||
|
2. Run `yarn` to install top-level dependencies.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
- `yarn lint` run just eslint
|
||||||
|
- `yarn test` run lint and tests
|
||||||
|
|
1
ghost/mentions-email-report/index.js
Normal file
1
ghost/mentions-email-report/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./lib/MentionEmailReportJob');
|
117
ghost/mentions-email-report/lib/MentionEmailReportJob.js
Normal file
117
ghost/mentions-email-report/lib/MentionEmailReportJob.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
module.exports = class MentionEmailReportJob {
|
||||||
|
/** @type {IMentionReportGenerator} */
|
||||||
|
#mentionReportGenerator;
|
||||||
|
|
||||||
|
/** @type {IMentionReportRecipientRepository} */
|
||||||
|
#mentionReportRecipientRepository;
|
||||||
|
|
||||||
|
/** @type {IMentionReportEmailView} */
|
||||||
|
#mentionReportEmailView;
|
||||||
|
|
||||||
|
/** @type {IMentionReportHistoryService} */
|
||||||
|
#mentionReportHistoryService;
|
||||||
|
|
||||||
|
/** @type {IEmailService} */
|
||||||
|
#emailService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} deps
|
||||||
|
* @param {IMentionReportGenerator} deps.mentionReportGenerator
|
||||||
|
* @param {IMentionReportRecipientRepository} deps.mentionReportRecipientRepository
|
||||||
|
* @param {IMentionReportEmailView} deps.mentionReportEmailView
|
||||||
|
* @param {IMentionReportHistoryService} deps.mentionReportHistoryService
|
||||||
|
* @param {IEmailService} deps.emailService
|
||||||
|
*/
|
||||||
|
constructor(deps) {
|
||||||
|
this.#mentionReportGenerator = deps.mentionReportGenerator;
|
||||||
|
this.#mentionReportRecipientRepository = deps.mentionReportRecipientRepository;
|
||||||
|
this.#mentionReportEmailView = deps.mentionReportEmailView;
|
||||||
|
this.#mentionReportHistoryService = deps.mentionReportHistoryService;
|
||||||
|
this.#emailService = deps.emailService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for new mentions since the last report and sends an email to the recipients.
|
||||||
|
*
|
||||||
|
* @returns {Promise<number>} - A promise that resolves with the number of mentions found.
|
||||||
|
*/
|
||||||
|
async sendLatestReport() {
|
||||||
|
const lastReport = await this.#mentionReportHistoryService.getLatestReportDate();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (now.valueOf() - lastReport.valueOf() < 24 * 60 * 60 * 1000) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = await this.#mentionReportGenerator.getMentionReport(lastReport, now);
|
||||||
|
|
||||||
|
report.mentions = report.mentions.map((mention) => {
|
||||||
|
return {
|
||||||
|
targetUrl: mention.target,
|
||||||
|
sourceUrl: mention.source,
|
||||||
|
sourceTitle: mention.sourceTitle,
|
||||||
|
sourceExcerpt: mention.sourceExcerpt,
|
||||||
|
sourceSiteTitle: mention.sourceSiteTitle,
|
||||||
|
sourceFavicon: mention.sourceFavicon,
|
||||||
|
sourceAuthor: mention.sourceAuthor,
|
||||||
|
sourceFeaturedImage: mention.sourceFeaturedImage
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!report?.mentions?.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipients = await this.#mentionReportRecipientRepository.getMentionReportRecipients();
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
const subject = await this.#mentionReportEmailView.renderSubject(report, recipient);
|
||||||
|
const html = await this.#mentionReportEmailView.renderHTML(report, recipient);
|
||||||
|
const text = await this.#mentionReportEmailView.renderText(report, recipient);
|
||||||
|
|
||||||
|
await this.#emailService.send(recipient.email, subject, html, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#mentionReportHistoryService.setLatestReportDate(now);
|
||||||
|
|
||||||
|
return report.mentions.length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} MentionReportRecipient
|
||||||
|
* @prop {string} email
|
||||||
|
* @prop {string} slug
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} IMentionReportRecipientRepository
|
||||||
|
* @prop {() => Promise<MentionReportRecipient[]>} getMentionReportRecipients
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('@tryghost/webmentions/lib/MentionsAPI').MentionReport} MentionReport
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} IMentionReportGenerator
|
||||||
|
* @prop {(startDate: Date, endDate: Date) => Promise<MentionReport>} getMentionReport
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} IMentionReportEmailView
|
||||||
|
* @prop {(report: MentionReport, recipient: MentionReportRecipient) => Promise<string>} renderHTML
|
||||||
|
* @prop {(report: MentionReport, recipient: MentionReportRecipient) => Promise<string>} renderText
|
||||||
|
* @prop {(report: MentionReport, recipient: MentionReportRecipient) => Promise<string>} renderSubject
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} IEmailService
|
||||||
|
* @prop {(to: string, subject: string, html: string, text: string) => Promise<void>} send
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} IMentionReportHistoryService
|
||||||
|
* @prop {() => Promise<Date>} getLatestReportDate
|
||||||
|
* @prop {(date: Date) => Promise<void>} setLatestReportDate
|
||||||
|
*/
|
26
ghost/mentions-email-report/package.json
Normal file
26
ghost/mentions-email-report/package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@tryghost/mentions-email-report",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/mentions-email-report",
|
||||||
|
"author": "Ghost Foundation",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "echo \"Implement me!\"",
|
||||||
|
"test:unit": "NODE_ENV=testing c8 --all --check-coverage --100 --reporter text --reporter cobertura mocha './test/**/*.test.js'",
|
||||||
|
"test": "yarn test:unit",
|
||||||
|
"lint:code": "eslint *.js lib/ --ext .js --cache",
|
||||||
|
"lint": "yarn lint:code && yarn lint:test",
|
||||||
|
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.js",
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"c8": "7.13.0",
|
||||||
|
"mocha": "10.2.0",
|
||||||
|
"sinon": "15.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
6
ghost/mentions-email-report/test/.eslintrc.js
Normal file
6
ghost/mentions-email-report/test/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ['ghost'],
|
||||||
|
extends: [
|
||||||
|
'plugin:ghost/test'
|
||||||
|
]
|
||||||
|
};
|
164
ghost/mentions-email-report/test/hello.test.js
Normal file
164
ghost/mentions-email-report/test/hello.test.js
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
const sinon = require('sinon');
|
||||||
|
const MentionEmailReportJob = require('../');
|
||||||
|
|
||||||
|
class MockMentionReportRecipientRepository {
|
||||||
|
#recipients = [{
|
||||||
|
email: 'fake@email.address',
|
||||||
|
slug: 'user-slug'
|
||||||
|
}];
|
||||||
|
|
||||||
|
constructor(recipients) {
|
||||||
|
if (recipients) {
|
||||||
|
this.#recipients = recipients;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMentionReportRecipients() {
|
||||||
|
return this.#recipients;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockMentionReportEmailView {
|
||||||
|
async renderSubject() {
|
||||||
|
return 'Mention Report';
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderHTML() {
|
||||||
|
return '<h1>Mention Report</h1>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderText() {
|
||||||
|
return 'Mention Report';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockEmailService {
|
||||||
|
async send() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockMentionReportHistoryService {
|
||||||
|
#date = null;
|
||||||
|
|
||||||
|
constructor(date) {
|
||||||
|
if (!date) {
|
||||||
|
throw new Error('Missing date');
|
||||||
|
}
|
||||||
|
this.#date = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLatestReportDate() {
|
||||||
|
return this.#date;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLatestReportDate(date) {
|
||||||
|
this.#date = date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockMentionReportGenerator {
|
||||||
|
#mentions = null;
|
||||||
|
|
||||||
|
constructor(mentions) {
|
||||||
|
if (!mentions) {
|
||||||
|
throw new Error('Missing mentions');
|
||||||
|
}
|
||||||
|
this.#mentions = mentions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMentionReport(startDate, endDate) {
|
||||||
|
return {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
mentions: this.#mentions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MentionEmailReportJob', function () {
|
||||||
|
describe('sendLatestReport', function () {
|
||||||
|
it('Does not send an email if the report has no mentions', async function () {
|
||||||
|
const emailService = new MockEmailService();
|
||||||
|
|
||||||
|
const mock = sinon.mock(emailService);
|
||||||
|
|
||||||
|
mock.expects('send').never();
|
||||||
|
|
||||||
|
const job = new MentionEmailReportJob({
|
||||||
|
mentionReportGenerator: new MockMentionReportGenerator([]),
|
||||||
|
mentionReportRecipientRepository: new MockMentionReportRecipientRepository(),
|
||||||
|
mentionReportEmailView: new MockMentionReportEmailView(),
|
||||||
|
mentionReportHistoryService: new MockMentionReportHistoryService(new Date(0)),
|
||||||
|
emailService: emailService
|
||||||
|
});
|
||||||
|
|
||||||
|
await job.sendLatestReport();
|
||||||
|
|
||||||
|
mock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does not send an email if the last email was sent within 24 hours', async function () {
|
||||||
|
const emailService = new MockEmailService();
|
||||||
|
|
||||||
|
const mock = sinon.mock(emailService);
|
||||||
|
|
||||||
|
mock.expects('send').never();
|
||||||
|
|
||||||
|
const job = new MentionEmailReportJob({
|
||||||
|
mentionReportGenerator: new MockMentionReportGenerator([{
|
||||||
|
target: new URL('https://target.com'),
|
||||||
|
source: new URL('https://source.com'),
|
||||||
|
sourceTitle: 'Source Title',
|
||||||
|
sourceExcerpt: 'Source Excerpt',
|
||||||
|
sourceSiteTitle: 'Source Site Title',
|
||||||
|
sourceFavicon: new URL('https://source.com/favicon.ico'),
|
||||||
|
sourceAuthor: 'Source Author',
|
||||||
|
sourceFeaturedImage: new URL('https://source.com/featured-image.jpg')
|
||||||
|
}]),
|
||||||
|
mentionReportRecipientRepository: new MockMentionReportRecipientRepository(),
|
||||||
|
mentionReportEmailView: new MockMentionReportEmailView(),
|
||||||
|
mentionReportHistoryService: new MockMentionReportHistoryService(new Date()),
|
||||||
|
emailService: emailService
|
||||||
|
});
|
||||||
|
|
||||||
|
await job.sendLatestReport();
|
||||||
|
|
||||||
|
mock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Sends an email if the last email was sent more than 24 hours ago', async function () {
|
||||||
|
const emailService = new MockEmailService();
|
||||||
|
|
||||||
|
const mock = sinon.mock(emailService);
|
||||||
|
|
||||||
|
mock.expects('send').once().alwaysCalledWith(
|
||||||
|
'fake@email.address',
|
||||||
|
'Mention Report',
|
||||||
|
'<h1>Mention Report</h1>',
|
||||||
|
'Mention Report'
|
||||||
|
);
|
||||||
|
|
||||||
|
const job = new MentionEmailReportJob({
|
||||||
|
mentionReportGenerator: new MockMentionReportGenerator([{
|
||||||
|
target: new URL('https://target.com'),
|
||||||
|
source: new URL('https://source.com'),
|
||||||
|
sourceTitle: 'Source Title',
|
||||||
|
sourceExcerpt: 'Source Excerpt',
|
||||||
|
sourceSiteTitle: 'Source Site Title',
|
||||||
|
sourceFavicon: new URL('https://source.com/favicon.ico'),
|
||||||
|
sourceAuthor: 'Source Author',
|
||||||
|
sourceFeaturedImage: new URL('https://source.com/featured-image.jpg')
|
||||||
|
}]),
|
||||||
|
mentionReportRecipientRepository: new MockMentionReportRecipientRepository(),
|
||||||
|
mentionReportEmailView: new MockMentionReportEmailView(),
|
||||||
|
mentionReportHistoryService: new MockMentionReportHistoryService(new Date(0)),
|
||||||
|
emailService
|
||||||
|
});
|
||||||
|
|
||||||
|
await job.sendLatestReport();
|
||||||
|
|
||||||
|
mock.verify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user