ghost
Version:
The professional publishing platform
229 lines (228 loc) • 9.81 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RecommendationService = void 0;
const errors_1 = __importDefault(require("@tryghost/errors"));
const logging_1 = __importDefault(require("@tryghost/logging"));
const tpl_1 = __importDefault(require("@tryghost/tpl"));
const ClickEvent_1 = require("./ClickEvent");
const Recommendation_1 = require("./Recommendation");
const SubscribeEvent_1 = require("./SubscribeEvent");
const messages = {
notFound: 'Recommendation with id {id} not found'
};
class RecommendationService {
repository;
clickEventRepository;
subscribeEventRepository;
wellknownService;
mentionSendingService;
recommendationEnablerService;
recommendationMetadataService;
constructor(deps) {
this.repository = deps.repository;
this.wellknownService = deps.wellknownService;
this.mentionSendingService = deps.mentionSendingService;
this.recommendationEnablerService = deps.recommendationEnablerService;
this.clickEventRepository = deps.clickEventRepository;
this.subscribeEventRepository = deps.subscribeEventRepository;
this.recommendationMetadataService = deps.recommendationMetadataService;
}
async init() {
const recommendations = await this.#listRecommendations();
await this.updateWellknown(recommendations);
// Do a slow update of all the recommendation metadata (keeping logo up to date, one-click-subscribe, etc.)
// We better move this to a job in the future
if (!process.env.NODE_ENV?.startsWith('test')) {
setTimeout(async () => {
try {
await this.updateAllRecommendationsMetadata();
}
catch (e) {
logging_1.default.error('[Recommendations] Failed to update all recommendations metadata on boot', e);
}
}, 2 * 60 * 1000 + Math.random() * 5 * 60 * 1000);
}
}
async updateAllRecommendationsMetadata() {
const recommendations = await this.#listRecommendations();
logging_1.default.info('[Recommendations] Updating recommendations metadata');
for (const recommendation of recommendations) {
try {
await this._updateRecommendationMetadata(recommendation);
await this.repository.save(recommendation);
}
catch (e) {
logging_1.default.error('[Recommendations] Failed to save updated metadata for recommendation ' + recommendation.url.toString(), e);
}
}
}
async updateWellknown(recommendations) {
await this.wellknownService.set(recommendations);
}
async updateRecommendationsEnabledSetting(recommendations) {
const expectedSetting = (recommendations.length > 0).toString();
const currentSetting = this.recommendationEnablerService.getSetting();
if (currentSetting && currentSetting === expectedSetting) {
return;
}
await this.recommendationEnablerService.setSetting(expectedSetting);
}
sendMentionToRecommendation(recommendation) {
this.mentionSendingService.sendAll({
url: this.wellknownService.getURL(),
links: [
recommendation.url
]
}).catch((err) => {
logging_1.default.error('Failed to send mention to recommendation', err);
});
}
async readRecommendation(id) {
const recommendation = await this.repository.getById(id);
if (!recommendation) {
throw new errors_1.default.NotFoundError({
message: (0, tpl_1.default)(messages.notFound, { id })
});
}
return recommendation.plain;
}
async addRecommendation(addRecommendation) {
const recommendation = Recommendation_1.Recommendation.create(addRecommendation);
// If a recommendation with this URL already exists, throw an error
const existing = await this.repository.getByUrl(recommendation.url);
if (existing) {
throw new errors_1.default.ValidationError({
message: 'A recommendation with this URL already exists.'
});
}
await this.repository.save(recommendation);
const recommendations = await this.#listRecommendations();
await this.updateWellknown(recommendations);
await this.updateRecommendationsEnabledSetting(recommendations);
// Only send an update for the mentioned URL
this.sendMentionToRecommendation(recommendation);
return recommendation.plain;
}
async checkRecommendation(url) {
// If a recommendation with this URL already exists, return it, but with updated metadata
const existing = await this.repository.getByUrl(url);
if (existing) {
this._updateRecommendationMetadata(existing);
await this.repository.save(existing);
return existing.plain;
}
let metadata;
try {
metadata = await this.recommendationMetadataService.fetch(url);
}
catch (e) {
logging_1.default.error('[Recommendations] Failed to fetch metadata for url ' + url, e);
return {
url: url,
title: undefined,
excerpt: undefined,
featuredImage: undefined,
favicon: undefined,
oneClickSubscribe: false
};
}
return {
url: url,
title: metadata.title ?? undefined,
excerpt: metadata.excerpt ?? undefined,
featuredImage: metadata.featuredImage ?? undefined,
favicon: metadata.favicon ?? undefined,
oneClickSubscribe: !!metadata.oneClickSubscribe
};
}
async _updateRecommendationMetadata(recommendation) {
// Fetch data
try {
const metadata = await this.recommendationMetadataService.fetch(recommendation.url);
// Set null values to undefined so we don't trigger an update
recommendation.edit({
// Don't set title if it's already set on the recommendation
title: recommendation.title ? undefined : (metadata.title ?? undefined),
excerpt: metadata.excerpt ?? undefined,
featuredImage: metadata.featuredImage ?? undefined,
favicon: metadata.favicon ?? undefined,
oneClickSubscribe: !!metadata.oneClickSubscribe
});
}
catch (e) {
logging_1.default.error('[Recommendations] Failed to update metadata for recommendation ' + recommendation.url.toString(), e);
}
}
async editRecommendation(id, recommendationEdit) {
// Check if it exists
const existing = await this.repository.getById(id);
if (!existing) {
throw new errors_1.default.NotFoundError({
message: (0, tpl_1.default)(messages.notFound, { id })
});
}
existing.edit(recommendationEdit);
await this._updateRecommendationMetadata(existing);
await this.repository.save(existing);
const recommendations = await this.#listRecommendations();
await this.updateWellknown(recommendations);
this.sendMentionToRecommendation(existing);
return existing.plain;
}
async deleteRecommendation(id) {
const existing = await this.repository.getById(id);
if (!existing) {
throw new errors_1.default.NotFoundError({
message: (0, tpl_1.default)(messages.notFound, { id })
});
}
existing.delete();
await this.repository.save(existing);
const recommendations = await this.#listRecommendations();
await this.updateWellknown(recommendations);
await this.updateRecommendationsEnabledSetting(recommendations);
// Send a mention (because it was deleted, according to the webmentions spec)
this.sendMentionToRecommendation(existing);
}
/**
* Sames as listRecommendations, but returns Entities instead of plain objects (Entities are only used internally)
*/
async #listRecommendations(options = { page: 1, limit: 'all' }) {
if (options.limit === 'all') {
return await this.repository.getAll({
...options
});
}
return await this.repository.getPage({
...options,
page: options.page || 1,
limit: options.limit || 15
});
}
async listRecommendations(options = { page: 1, limit: 'all', include: [] }) {
const list = await this.#listRecommendations(options);
return list.map(e => e.plain);
}
async countRecommendations({ filter }) {
return await this.repository.getCount({ filter });
}
async trackClicked({ id, memberId }) {
const clickEvent = ClickEvent_1.ClickEvent.create({ recommendationId: id, memberId });
await this.clickEventRepository.save(clickEvent);
}
async trackSubscribed({ id, memberId }) {
const subscribeEvent = SubscribeEvent_1.SubscribeEvent.create({ recommendationId: id, memberId });
await this.subscribeEventRepository.save(subscribeEvent);
}
async readRecommendationByUrl(url) {
const recommendation = await this.repository.getByUrl(url);
if (!recommendation) {
return null;
}
return recommendation.plain;
}
}
exports.RecommendationService = RecommendationService;