2022-09-19 18:12:54 +03:00
|
|
|
const DomainEvents = require('@tryghost/domain-events');
|
|
|
|
const {RedirectEvent} = require('@tryghost/link-redirects');
|
|
|
|
const LinkClick = require('./LinkClick');
|
|
|
|
const PostLink = require('./PostLink');
|
|
|
|
const ObjectID = require('bson-objectid').default;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {object} ILinkClickRepository
|
|
|
|
* @prop {(event: LinkClick) => Promise<void>} save
|
2022-09-22 14:39:52 +03:00
|
|
|
* @prop {({filter: string}) => Promise<LinkClick[]>} getAll
|
2022-09-19 18:12:54 +03:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {object} ILinkRedirect
|
|
|
|
* @prop {ObjectID} link_id
|
|
|
|
* @prop {URL} to
|
|
|
|
* @prop {URL} from
|
|
|
|
*/
|
|
|
|
|
2022-09-22 14:39:52 +03:00
|
|
|
/**
|
|
|
|
* @typedef {import('./FullPostLink')} FullPostLink
|
|
|
|
*/
|
|
|
|
|
2022-09-19 18:12:54 +03:00
|
|
|
/**
|
|
|
|
* @typedef {object} ILinkRedirectService
|
|
|
|
* @prop {(to: URL, slug: string) => Promise<ILinkRedirect>} addRedirect
|
|
|
|
* @prop {() => Promise<string>} getSlug
|
2022-09-22 14:39:52 +03:00
|
|
|
* @prop {({filter: string}) => Promise<ILinkRedirect[]>} getAll
|
2022-09-19 18:12:54 +03:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {object} IPostLinkRepository
|
|
|
|
* @prop {(postLink: PostLink) => Promise<void>} save
|
2022-09-22 14:39:52 +03:00
|
|
|
* @prop {({filter: string}) => Promise<FullPostLink[]>} getAll
|
2022-09-19 18:12:54 +03:00
|
|
|
*/
|
|
|
|
|
|
|
|
class LinkClickTrackingService {
|
|
|
|
#initialised = false;
|
|
|
|
|
|
|
|
/** @type ILinkClickRepository */
|
|
|
|
#linkClickRepository;
|
|
|
|
/** @type ILinkRedirectService */
|
|
|
|
#linkRedirectService;
|
|
|
|
/** @type IPostLinkRepository */
|
|
|
|
#postLinkRepository;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {object} deps
|
|
|
|
* @param {ILinkClickRepository} deps.linkClickRepository
|
|
|
|
* @param {ILinkRedirectService} deps.linkRedirectService
|
|
|
|
* @param {IPostLinkRepository} deps.postLinkRepository
|
|
|
|
*/
|
|
|
|
constructor(deps) {
|
|
|
|
this.#linkClickRepository = deps.linkClickRepository;
|
|
|
|
this.#linkRedirectService = deps.linkRedirectService;
|
|
|
|
this.#postLinkRepository = deps.postLinkRepository;
|
|
|
|
}
|
|
|
|
|
|
|
|
async init() {
|
|
|
|
if (this.#initialised) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.subscribe();
|
|
|
|
this.#initialised = true;
|
|
|
|
}
|
|
|
|
|
2022-09-22 14:39:52 +03:00
|
|
|
/**
|
|
|
|
* @param {object} options
|
|
|
|
* @param {string} options.filter
|
|
|
|
* @return {Promise<FullPostLink[]>}
|
|
|
|
*/
|
|
|
|
async getLinks(options) {
|
|
|
|
return await this.#postLinkRepository.getAll({
|
|
|
|
filter: options.filter
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-09-19 18:12:54 +03:00
|
|
|
/**
|
|
|
|
* Replace URL with a redirect that redirects to the original URL, and link that redirect with the given post
|
|
|
|
*/
|
|
|
|
async addRedirectToUrl(url, post) {
|
|
|
|
// Generate a unique redirect slug
|
2022-09-23 14:32:44 +03:00
|
|
|
const slugUrl = await this.#linkRedirectService.getSlugUrl();
|
2022-09-19 18:12:54 +03:00
|
|
|
|
|
|
|
// Add redirect for link click tracking
|
2022-09-23 14:32:44 +03:00
|
|
|
const redirect = await this.#linkRedirectService.addRedirect(slugUrl, url);
|
2022-09-19 18:12:54 +03:00
|
|
|
|
|
|
|
// Store a reference of the link against the post
|
|
|
|
const postLink = new PostLink({
|
|
|
|
link_id: redirect.link_id,
|
|
|
|
post_id: ObjectID.createFromHexString(post.id)
|
|
|
|
});
|
|
|
|
await this.#postLinkRepository.save(postLink);
|
|
|
|
|
|
|
|
return redirect.from;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add tracking to a URL and returns a new URL (if link click tracking is enabled)
|
|
|
|
* @param {URL} url
|
|
|
|
* @param {Post} post
|
|
|
|
* @param {string} memberUuid
|
|
|
|
* @return {Promise<URL>}
|
|
|
|
*/
|
|
|
|
async addTrackingToUrl(url, post, memberUuid) {
|
|
|
|
url = await this.addRedirectToUrl(url, post);
|
|
|
|
url.searchParams.set('m', memberUuid);
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
|
|
|
subscribe() {
|
|
|
|
DomainEvents.subscribe(RedirectEvent, async (event) => {
|
|
|
|
const uuid = event.data.url.searchParams.get('m');
|
|
|
|
if (!uuid) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const click = new LinkClick({
|
|
|
|
member_uuid: uuid,
|
|
|
|
link_id: event.data.link.link_id
|
|
|
|
});
|
|
|
|
await this.#linkClickRepository.save(click);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = LinkClickTrackingService;
|