2023-01-17 10:55:53 +03:00
const ObjectID = require ( 'bson-objectid' ) . default ;
const { ValidationError } = require ( '@tryghost/errors' ) ;
2023-01-25 16:10:29 +03:00
const MentionCreatedEvent = require ( './MentionCreatedEvent' ) ;
2023-02-17 14:56:44 +03:00
const cheerio = require ( 'cheerio' ) ;
2023-01-17 10:55:53 +03:00
module . exports = class Mention {
2023-01-25 16:10:29 +03:00
/** @type {Array} */
events = [ ] ;
2023-01-17 10:55:53 +03:00
/** @type {ObjectID} */
# id ;
get id ( ) {
return this . # id ;
}
2023-02-13 15:18:17 +03:00
/** @type {boolean} */
# verified = false ;
get verified ( ) {
return this . # verified ;
}
2023-09-20 14:09:47 +03:00
/** @type {boolean} */
# deleted = false ;
get deleted ( ) {
return this . # deleted ;
}
delete ( ) {
this . # deleted = true ;
}
2023-09-26 18:29:17 +03:00
# undelete ( ) {
// When an earlier mention is deleted, but then it gets verified again, we need to undelete it
if ( this . # deleted ) {
this . # deleted = false ;
this . events . push ( MentionCreatedEvent . create ( { mention : this } ) ) ;
}
}
2023-02-13 15:18:17 +03:00
/ * *
* @ param { string } html
2023-08-31 17:57:18 +03:00
* @ param { string } contentType
2023-02-13 15:18:17 +03:00
* /
2023-08-31 17:57:18 +03:00
verify ( html , contentType ) {
const wasVerified = this . # verified ;
if ( contentType . includes ( 'text/html' ) ) {
try {
const $ = cheerio . load ( html ) ;
const hasTargetUrl = $ ( 'a[href*="' + this . target . href + '"], img[src*="' + this . target . href + '"], video[src*="' + this . target . href + '"]' ) . length > 0 ;
this . # verified = hasTargetUrl ;
if ( wasVerified && ! this . # verified ) {
2023-09-26 18:29:17 +03:00
// Delete the mention, but keep it verified (it was just deleted, because it was verified earlier, so now it is removed from the site according to the spec)
2023-08-31 17:57:18 +03:00
this . # deleted = true ;
this . # verified = true ;
2023-09-26 18:29:17 +03:00
} else {
this . # undelete ( ) ;
2023-08-31 17:57:18 +03:00
}
} catch ( e ) {
this . # verified = false ;
}
}
if ( contentType . includes ( 'application/json' ) ) {
try {
// Check valid JSON
JSON . parse ( html ) ;
// Check full text string is present in the json
this . # verified = ! ! html . includes ( JSON . stringify ( this . target . href ) ) ;
if ( wasVerified && ! this . # verified ) {
2023-09-26 18:29:17 +03:00
// Delete the mention, but keep it verified (it was just deleted, because it was verified earlier, so now it is removed from the site according to the spec)
2023-08-31 17:57:18 +03:00
this . # deleted = true ;
this . # verified = true ;
2023-09-26 18:29:17 +03:00
} else {
this . # undelete ( ) ;
2023-08-31 17:57:18 +03:00
}
} catch ( e ) {
this . # verified = false ;
}
}
2023-02-13 15:18:17 +03:00
}
2023-01-17 10:55:53 +03:00
/** @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 ;
}
2023-02-21 08:02:47 +03:00
/** @type {string | null} */
# resourceType ;
get resourceType ( ) {
return this . # resourceType ;
}
2023-01-17 10:55:53 +03:00
/** @type {string} */
# sourceTitle ;
get sourceTitle ( ) {
return this . # sourceTitle ;
}
2023-01-19 15:08:18 +03:00
/** @type {string | null} */
2023-01-18 08:32:37 +03:00
# sourceSiteTitle ;
get sourceSiteTitle ( ) {
return this . # sourceSiteTitle ;
}
2023-01-19 15:08:18 +03:00
/** @type {string | null} */
2023-01-18 08:32:37 +03:00
# sourceAuthor ;
get sourceAuthor ( ) {
return this . # sourceAuthor ;
}
2023-01-19 15:08:18 +03:00
/** @type {string | null} */
2023-01-17 10:55:53 +03:00
# sourceExcerpt ;
get sourceExcerpt ( ) {
return this . # sourceExcerpt ;
}
/** @type {URL | null} */
# sourceFavicon ;
get sourceFavicon ( ) {
return this . # sourceFavicon ;
}
/** @type {URL | null} */
# sourceFeaturedImage ;
get sourceFeaturedImage ( ) {
return this . # sourceFeaturedImage ;
}
2023-02-09 13:44:14 +03:00
/ * *
* @ param { object } metadata
* /
setSourceMetadata ( metadata ) {
/** @type {string} */
let sourceTitle = validateString ( metadata . sourceTitle , 2000 , 'sourceTitle' ) ;
if ( sourceTitle === null ) {
sourceTitle = this . # source . host ;
}
/** @type {string | null} */
const sourceExcerpt = validateString ( metadata . sourceExcerpt , 2000 , 'sourceExcerpt' ) ;
/** @type {string | null} */
2023-03-20 21:39:33 +03:00
let sourceSiteTitle = validateString ( metadata . sourceSiteTitle , 2000 , 'sourceSiteTitle' ) ;
if ( sourceSiteTitle === null ) {
sourceSiteTitle = this . # source . host ;
}
2023-02-09 13:44:14 +03:00
/** @type {string | null} */
const sourceAuthor = validateString ( metadata . sourceAuthor , 2000 , 'sourceAuthor' ) ;
/** @type {URL | null} */
let sourceFavicon = null ;
if ( metadata . sourceFavicon instanceof URL ) {
sourceFavicon = metadata . sourceFavicon ;
} else if ( metadata . sourceFavicon ) {
sourceFavicon = new URL ( metadata . sourceFavicon ) ;
}
/** @type {URL | null} */
let sourceFeaturedImage = null ;
if ( metadata . sourceFeaturedImage instanceof URL ) {
sourceFeaturedImage = metadata . sourceFeaturedImage ;
} else if ( metadata . sourceFeaturedImage ) {
sourceFeaturedImage = new URL ( metadata . sourceFeaturedImage ) ;
}
this . # sourceTitle = sourceTitle ;
this . # sourceExcerpt = sourceExcerpt ;
this . # sourceSiteTitle = sourceSiteTitle ;
this . # sourceAuthor = sourceAuthor ;
this . # sourceFavicon = sourceFavicon ;
this . # sourceFeaturedImage = sourceFeaturedImage ;
}
2023-01-17 10:55:53 +03:00
toJSON ( ) {
return {
id : this . id ,
source : this . source ,
target : this . target ,
timestamp : this . timestamp ,
payload : this . payload ,
resourceId : this . resourceId ,
2023-02-21 08:02:47 +03:00
resourceType : this . resourceType ,
2023-01-17 10:55:53 +03:00
sourceTitle : this . sourceTitle ,
2023-01-18 08:32:37 +03:00
sourceSiteTitle : this . sourceSiteTitle ,
sourceAuthor : this . sourceAuthor ,
2023-01-17 10:55:53 +03:00
sourceExcerpt : this . sourceExcerpt ,
sourceFavicon : this . sourceFavicon ,
2023-02-16 10:23:59 +03:00
sourceFeaturedImage : this . sourceFeaturedImage ,
verified : this . verified
2023-01-17 10:55:53 +03:00
} ;
}
/** @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 ;
2023-02-21 08:02:47 +03:00
this . # resourceType = data . resourceType ;
2023-02-17 14:38:28 +03:00
this . # verified = data . verified ;
2023-09-26 18:29:17 +03:00
this . # deleted = data . deleted || false ;
2023-01-17 10:55:53 +03:00
}
/ * *
* @ param { any } data
* @ returns { Promise < Mention > }
* /
static async create ( data ) {
2023-01-19 15:08:18 +03:00
/** @type ObjectID */
2023-01-17 10:55:53 +03:00
let id ;
2023-01-25 16:10:29 +03:00
let isNew = false ;
2023-01-17 10:55:53 +03:00
if ( ! data . id ) {
2023-01-25 16:10:29 +03:00
isNew = true ;
2023-01-17 10:55:53 +03:00
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'
} ) ;
}
2023-01-19 15:08:18 +03:00
/** @type URL */
2023-01-17 10:55:53 +03:00
let source ;
if ( data . source instanceof URL ) {
source = data . source ;
} else {
source = new URL ( data . source ) ;
}
2023-01-19 15:08:18 +03:00
/** @type URL */
2023-01-17 10:55:53 +03:00
let target ;
if ( data . target instanceof URL ) {
target = data . target ;
} else {
target = new URL ( data . target ) ;
}
2023-01-19 15:08:18 +03:00
/** @type Date */
2023-01-17 10:55:53 +03:00
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 ;
2023-02-17 14:38:28 +03:00
/** @type boolean */
let verified ;
verified = isNew ? false : ! ! data . verified ;
2023-01-19 15:08:18 +03:00
/** @type {ObjectID | null} */
2023-01-17 10:55:53 +03:00
let resourceId = null ;
if ( data . resourceId ) {
if ( data . resourceId instanceof ObjectID ) {
resourceId = data . resourceId ;
} else {
resourceId = ObjectID . createFromHexString ( data . resourceId ) ;
}
}
2023-02-21 08:02:47 +03:00
/** @type {string | null} */
let resourceType = null ;
if ( data . resourceType ) {
resourceType = data . resourceType ;
}
2023-01-25 16:10:29 +03:00
const mention = new Mention ( {
2023-01-17 10:55:53 +03:00
id ,
source ,
target ,
timestamp ,
payload ,
2023-02-17 14:38:28 +03:00
resourceId ,
2023-02-21 08:02:47 +03:00
resourceType ,
2023-09-26 18:29:17 +03:00
verified ,
deleted : isNew ? false : ! ! data . deleted
2023-01-17 10:55:53 +03:00
} ) ;
2023-01-25 16:10:29 +03:00
2023-02-09 13:44:14 +03:00
mention . setSourceMetadata ( data ) ;
2023-01-25 16:10:29 +03:00
if ( isNew ) {
mention . events . push ( MentionCreatedEvent . create ( { mention } ) ) ;
}
return mention ;
2023-01-17 10:55:53 +03:00
}
2023-02-09 13:29:13 +03:00
2023-08-31 17:57:18 +03:00
/ * *
* @ returns { boolean }
* /
isDeleted ( ) {
return this . # deleted ;
}
2023-02-09 13:29:13 +03:00
/ * *
* @ param { Mention } mention
* @ returns { boolean }
* /
static isDeleted ( mention ) {
2023-08-31 17:57:18 +03:00
return mention . isDeleted ( ) ;
2023-02-09 13:29:13 +03:00
}
2023-01-17 10:55:53 +03:00
} ;
2023-01-19 15:07:01 +03:00
function validateString ( value , maxlength , name ) {
2023-01-17 10:55:53 +03:00
if ( ! value ) {
2023-01-19 15:07:01 +03:00
return null ;
2023-01-17 10:55:53 +03:00
}
if ( typeof value !== 'string' ) {
throw new ValidationError ( {
message : ` ${ name } must be a string `
} ) ;
}
2023-01-19 15:07:01 +03:00
return value . trim ( ) . slice ( 0 , maxlength ) ;
2023-01-17 10:55:53 +03:00
}