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