X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Flib%2Factivitypub%2Fvideo-comments.ts;h=911c7cd3020b5f334215bd4950095273d9d0d547;hb=7e98a7df7d04e19ba67163a86c7b876d78d76839;hp=4ca8bf6595796431dd1396dbd46d712f740ddc3e;hpb=4157cdb13748cb6e8ce7081d062a8778554cc5a7;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index 4ca8bf659..911c7cd30 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts @@ -1,159 +1,171 @@ -import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' +import { map } from 'bluebird' import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' import { logger } from '../../helpers/logger' -import { doRequest } from '../../helpers/requests' -import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers' -import { ActorModel } from '../../models/activitypub/actor' -import { VideoModel } from '../../models/video/video' +import { doJSONRequest } from '../../helpers/requests' +import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' import { VideoCommentModel } from '../../models/video/video-comment' -import { getOrCreateActorAndServerAndModel } from './actor' -import { getOrCreateVideoAndAccountAndChannel } from './videos' -import * as Bluebird from 'bluebird' - -async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { - let originCommentId: number = null - let inReplyToCommentId: number = null - - // If this is not a reply to the video (thread), create or get the parent comment - if (video.url !== comment.inReplyTo) { - const { comment: parent } = await addVideoComment(video, comment.inReplyTo) - if (!parent) { - logger.warn('Cannot fetch or get parent comment %s of comment %s.', comment.inReplyTo, comment.id) - return undefined +import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' +import { getOrCreateAPActor } from './actors' +import { checkUrlsSameHost } from './url' +import { getOrCreateAPVideo } from './videos' + +type ResolveThreadParams = { + url: string + comments?: MCommentOwner[] + isVideo?: boolean + commentCreated?: boolean +} +type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> + +async function addVideoComments (commentUrls: string[]) { + return map(commentUrls, async commentUrl => { + try { + await resolveThread({ url: commentUrl, isVideo: false }) + } catch (err) { + logger.warn('Cannot resolve thread %s.', commentUrl, { err }) } + }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) +} + +async function resolveThread (params: ResolveThreadParams): ResolveThreadResult { + const { url, isVideo } = params + + if (params.commentCreated === undefined) params.commentCreated = false + if (params.comments === undefined) params.comments = [] - originCommentId = parent.originCommentId || parent.id - inReplyToCommentId = parent.id + // If it is not a video, or if we don't know if it's a video, try to get the thread from DB + if (isVideo === false || isVideo === undefined) { + const result = await resolveCommentFromDB(params) + if (result) return result } - return { - url: comment.url, - text: comment.content, - videoId: video.id, - accountId: actor.Account.id, - inReplyToCommentId, - originCommentId, - createdAt: new Date(comment.published), - updatedAt: new Date(comment.updated) + try { + // If it is a video, or if we don't know if it's a video + if (isVideo === true || isVideo === undefined) { + // Keep await so we catch the exception + return await tryToResolveThreadFromVideo(params) + } + } catch (err) { + logger.debug('Cannot resolve thread from video %s, maybe because it was not a video', url, { err }) } + + return resolveRemoteParentComment(params) } -async function addVideoComments (commentUrls: string[], instance: VideoModel) { - return Bluebird.map(commentUrls, commentUrl => { - return addVideoComment(instance, commentUrl) - }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) +export { + addVideoComments, + resolveThread } -async function addVideoComment (videoInstance: VideoModel, commentUrl: string) { - logger.info('Fetching remote video comment %s.', commentUrl) +// --------------------------------------------------------------------------- - const { body } = await doRequest({ - uri: commentUrl, - json: true, - activityPub: true - }) +async function resolveCommentFromDB (params: ResolveThreadParams) { + const { url, comments, commentCreated } = params - if (sanitizeAndCheckVideoCommentObject(body) === false) { - logger.debug('Remote video comment JSON is not valid.', { body }) - return { created: false } - } + const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideoUrlAndAccount(url) + if (!commentFromDatabase) return undefined - const actorUrl = body.attributedTo - if (!actorUrl) return { created: false } + let parentComments = comments.concat([ commentFromDatabase ]) - const actor = await getOrCreateActorAndServerAndModel(actorUrl) - const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) - if (!entry) return { created: false } + // Speed up things and resolve directly the thread + if (commentFromDatabase.InReplyToVideoComment) { + const data = await VideoCommentModel.listThreadParentComments(commentFromDatabase, undefined, 'DESC') - const [ comment, created ] = await VideoCommentModel.findOrCreate({ - where: { - url: body.id - }, - defaults: entry - }) + parentComments = parentComments.concat(data) + } - return { comment, created } + return resolveThread({ + url: commentFromDatabase.Video.url, + comments: parentComments, + isVideo: true, + commentCreated + }) } -async function resolveThread (url: string, comments: VideoCommentModel[] = []) { - // Already have this comment? - const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideo(url) - if (commentFromDatabase) { - let parentComments = comments.concat([ commentFromDatabase ]) +async function tryToResolveThreadFromVideo (params: ResolveThreadParams) { + const { url, comments, commentCreated } = params - // Speed up things and resolve directly the thread - if (commentFromDatabase.InReplyToVideoComment) { - const data = await VideoCommentModel.listThreadParentComments(commentFromDatabase, undefined, 'DESC') + // Maybe it's a reply to a video? + // If yes, it's done: we resolved all the thread + const syncParam = { rates: true, shares: true, comments: false, thumbnail: true, refreshVideo: false } + const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam }) - parentComments = parentComments.concat(data) + if (video.isOwned() && !video.hasPrivacyForFederation()) { + throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation') + } + + let resultComment: MCommentOwnerVideo + if (comments.length !== 0) { + const firstReply = comments[comments.length - 1] as MCommentOwnerVideo + firstReply.inReplyToCommentId = null + firstReply.originCommentId = null + firstReply.videoId = video.id + firstReply.changed('updatedAt', true) + firstReply.Video = video + + comments[comments.length - 1] = await firstReply.save() + + for (let i = comments.length - 2; i >= 0; i--) { + const comment = comments[i] as MCommentOwnerVideo + comment.originCommentId = firstReply.id + comment.inReplyToCommentId = comments[i + 1].id + comment.videoId = video.id + comment.changed('updatedAt', true) + comment.Video = video + + comments[i] = await comment.save() } - return resolveThread(commentFromDatabase.Video.url, parentComments) + resultComment = comments[0] as MCommentOwnerVideo } - try { - // Maybe it's a reply to a video? - // If yes, it's done: we resolved all the thread - const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: url }) - - if (comments.length !== 0) { - const firstReply = comments[ comments.length - 1 ] - firstReply.inReplyToCommentId = null - firstReply.originCommentId = null - firstReply.videoId = video.id - comments[comments.length - 1] = await firstReply.save() - - for (let i = comments.length - 2; i >= 0; i--) { - const comment = comments[ i ] - comment.originCommentId = firstReply.id - comment.inReplyToCommentId = comments[ i + 1 ].id - comment.videoId = video.id - - comments[i] = await comment.save() - } - } + return { video, comment: resultComment, commentCreated } +} - return { video, parents: comments } - } catch (err) { - logger.debug('Cannot get or create account and video and channel for reply %s, fetch comment', url, { err }) +async function resolveRemoteParentComment (params: ResolveThreadParams) { + const { url, comments } = params - if (comments.length > ACTIVITY_PUB.MAX_RECURSION_COMMENTS) { - throw new Error('Recursion limit reached when resolving a thread') - } + if (comments.length > ACTIVITY_PUB.MAX_RECURSION_COMMENTS) { + throw new Error('Recursion limit reached when resolving a thread') + } - const { body } = await doRequest({ - uri: url, - json: true, - activityPub: true - }) + const { body } = await doJSONRequest(url, { activityPub: true }) - if (sanitizeAndCheckVideoCommentObject(body) === false) { - throw new Error('Remote video comment JSON is not valid :' + JSON.stringify(body)) - } + if (sanitizeAndCheckVideoCommentObject(body) === false) { + throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) + } + + const actorUrl = body.attributedTo + if (!actorUrl && body.type !== 'Tombstone') throw new Error('Miss attributed to in comment') - const actorUrl = body.attributedTo - if (!actorUrl) throw new Error('Miss attributed to in comment') - - const actor = await getOrCreateActorAndServerAndModel(actorUrl) - const comment = new VideoCommentModel({ - url: body.url, - text: body.content, - videoId: null, - accountId: actor.Account.id, - inReplyToCommentId: null, - originCommentId: null, - createdAt: new Date(body.published), - updatedAt: new Date(body.updated) - }) - - return resolveThread(body.inReplyTo, comments.concat([ comment ])) + if (actorUrl && checkUrlsSameHost(url, actorUrl) !== true) { + throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`) } -} + if (checkUrlsSameHost(body.id, url) !== true) { + throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`) + } -export { - videoCommentActivityObjectToDBAttributes, - addVideoComments, - addVideoComment, - resolveThread + const actor = actorUrl + ? await getOrCreateAPActor(actorUrl, 'all') + : null + + const comment = new VideoCommentModel({ + url: body.id, + text: body.content ? body.content : '', + videoId: null, + accountId: actor ? actor.Account.id : null, + inReplyToCommentId: null, + originCommentId: null, + createdAt: new Date(body.published), + updatedAt: new Date(body.updated), + deletedAt: body.deleted ? new Date(body.deleted) : null + }) as MCommentOwner + comment.Account = actor ? actor.Account : null + + return resolveThread({ + url: body.inReplyTo, + comments: comments.concat([ comment ]), + commentCreated: true + }) }