Ghost/ghost/webmentions/lib/Mention.js
Fabien "egg" O'Carroll 3a0e0831ec Updated Mention entity validation of properties
Based on a discussion in slack we want to make all metadata properties optional,
with the exception of the title, which will default to the host of the source
URL if it's missing.

This is so that we can accept as many webmentions as possible and convert them
into Mentions. If we were to have strictly validation, we'd end up having to
drop webmentions that didn't match our criteria, and lose important data.

Giving the title a default allows us to provide a consistent UI experience too.
2023-01-19 19:24:07 +07:00

220 lines
5.7 KiB
JavaScript

const ObjectID = require('bson-objectid').default;
const {ValidationError} = require('@tryghost/errors');
module.exports = class Mention {
/** @type {ObjectID} */
#id;
get id() {
return this.#id;
}
/** @type {URL} */
#source;
get source() {
return this.#source;
}
/** @type {URL} */
#target;
get target() {
return this.#target;
}
/** @type {Date} */
#timestamp;
get timestamp() {
return this.#timestamp;
}
/** @type {Object<string, any> | null} */
#payload;
get payload() {
return this.#payload;
}
/** @type {ObjectID | null} */
#resourceId;
get resourceId() {
return this.#resourceId;
}
/** @type {string} */
#sourceTitle;
get sourceTitle() {
return this.#sourceTitle;
}
/** @type {string} */
#sourceSiteTitle;
get sourceSiteTitle() {
return this.#sourceSiteTitle;
}
/** @type {string} */
#sourceAuthor;
get sourceAuthor() {
return this.#sourceAuthor;
}
/** @type {string} */
#sourceExcerpt;
get sourceExcerpt() {
return this.#sourceExcerpt;
}
/** @type {URL | null} */
#sourceFavicon;
get sourceFavicon() {
return this.#sourceFavicon;
}
/** @type {URL | null} */
#sourceFeaturedImage;
get sourceFeaturedImage() {
return this.#sourceFeaturedImage;
}
toJSON() {
return {
id: this.id,
source: this.source,
target: this.target,
timestamp: this.timestamp,
payload: this.payload,
resourceId: this.resourceId,
sourceTitle: this.sourceTitle,
sourceSiteTitle: this.sourceSiteTitle,
sourceAuthor: this.sourceAuthor,
sourceExcerpt: this.sourceExcerpt,
sourceFavicon: this.sourceFavicon,
sourceFeaturedImage: this.sourceFeaturedImage
};
}
/** @private */
constructor(data) {
this.#id = data.id;
this.#source = data.source;
this.#target = data.target;
this.#timestamp = data.timestamp;
this.#payload = data.payload;
this.#resourceId = data.resourceId;
this.#sourceTitle = data.sourceTitle;
this.#sourceSiteTitle = data.sourceSiteTitle;
this.#sourceAuthor = data.sourceAuthor;
this.#sourceExcerpt = data.sourceExcerpt;
this.#sourceFavicon = data.sourceFavicon;
this.#sourceFeaturedImage = data.sourceFeaturedImage;
}
/**
* @param {any} data
* @returns {Promise<Mention>}
*/
static async create(data) {
let id;
if (!data.id) {
id = new ObjectID();
} else if (typeof data.id === 'string') {
id = ObjectID.createFromHexString(data.id);
} else if (data.id instanceof ObjectID) {
id = data.id;
} else {
throw new ValidationError({
message: 'Invalid ID provided for Mention'
});
}
let source;
if (data.source instanceof URL) {
source = data.source;
} else {
source = new URL(data.source);
}
let target;
if (data.target instanceof URL) {
target = data.target;
} else {
target = new URL(data.target);
}
let timestamp;
if (data.timestamp instanceof Date) {
timestamp = data.timestamp;
} else if (data.timestamp) {
timestamp = new Date(data.timestamp);
if (isNaN(timestamp.valueOf())) {
throw new ValidationError({
message: 'Invalid Date'
});
}
} else {
timestamp = new Date();
}
let payload;
payload = data.payload ? JSON.parse(JSON.stringify(data.payload)) : null;
let resourceId = null;
if (data.resourceId) {
if (data.resourceId instanceof ObjectID) {
resourceId = data.resourceId;
} else {
resourceId = ObjectID.createFromHexString(data.resourceId);
}
}
let sourceTitle = validateString(data.sourceTitle, 2000, 'sourceTitle');
if (sourceTitle === null) {
sourceTitle = source.host;
}
const sourceExcerpt = validateString(data.sourceExcerpt, 2000, 'sourceExcerpt');
const sourceSiteTitle = validateString(data.sourceSiteTitle, 2000, 'sourceSiteTitle');
const sourceAuthor = validateString(data.sourceAuthor, 2000, 'sourceAuthor');
let sourceFavicon = null;
if (data.sourceFavicon instanceof URL) {
sourceFavicon = data.sourceFavicon;
} else if (data.sourceFavicon) {
sourceFavicon = new URL(data.sourceFavicon);
}
let sourceFeaturedImage = null;
if (data.sourceFeaturedImage instanceof URL) {
sourceFeaturedImage = data.sourceFeaturedImage;
} else if (data.sourceFeaturedImage) {
sourceFeaturedImage = new URL(data.sourceFeaturedImage);
}
return new Mention({
id,
source,
target,
timestamp,
payload,
resourceId,
sourceTitle,
sourceSiteTitle,
sourceAuthor,
sourceExcerpt,
sourceFavicon,
sourceFeaturedImage
});
}
};
function validateString(value, maxlength, name) {
if (!value) {
return null;
}
if (typeof value !== 'string') {
throw new ValidationError({
message: `${name} must be a string`
});
}
return value.trim().slice(0, maxlength);
}