video-comments.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
  2. import { logger } from '../../helpers/logger'
  3. import { doRequest } from '../../helpers/requests'
  4. import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
  5. import { VideoCommentModel } from '../../models/video/video-comment'
  6. import { getOrCreateActorAndServerAndModel } from './actor'
  7. import { getOrCreateVideoAndAccountAndChannel } from './videos'
  8. import * as Bluebird from 'bluebird'
  9. import { checkUrlsSameHost } from '../../helpers/activitypub'
  10. import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
  11. type ResolveThreadParams = {
  12. url: string
  13. comments?: MCommentOwner[]
  14. isVideo?: boolean
  15. commentCreated?: boolean
  16. }
  17. type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
  18. async function addVideoComments (commentUrls: string[]) {
  19. return Bluebird.map(commentUrls, commentUrl => {
  20. return resolveThread({ url: commentUrl, isVideo: false })
  21. }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
  22. }
  23. async function resolveThread (params: ResolveThreadParams): ResolveThreadResult {
  24. const { url, isVideo } = params
  25. if (params.commentCreated === undefined) params.commentCreated = false
  26. if (params.comments === undefined) params.comments = []
  27. // If it is not a video, or if we don't know if it's a video
  28. if (isVideo === false || isVideo === undefined) {
  29. const result = await resolveCommentFromDB(params)
  30. if (result) return result
  31. }
  32. try {
  33. // If it is a video, or if we don't know if it's a video
  34. if (isVideo === true || isVideo === undefined) {
  35. // Keep await so we catch the exception
  36. return await tryResolveThreadFromVideo(params)
  37. }
  38. } catch (err) {
  39. logger.debug('Cannot get or create account and video and channel for reply %s, fetch comment', url, { err })
  40. }
  41. return resolveParentComment(params)
  42. }
  43. export {
  44. addVideoComments,
  45. resolveThread
  46. }
  47. // ---------------------------------------------------------------------------
  48. async function resolveCommentFromDB (params: ResolveThreadParams) {
  49. const { url, comments, commentCreated } = params
  50. const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideoUrlAndAccount(url)
  51. if (commentFromDatabase) {
  52. let parentComments = comments.concat([ commentFromDatabase ])
  53. // Speed up things and resolve directly the thread
  54. if (commentFromDatabase.InReplyToVideoComment) {
  55. const data = await VideoCommentModel.listThreadParentComments(commentFromDatabase, undefined, 'DESC')
  56. parentComments = parentComments.concat(data)
  57. }
  58. return resolveThread({
  59. url: commentFromDatabase.Video.url,
  60. comments: parentComments,
  61. isVideo: true,
  62. commentCreated
  63. })
  64. }
  65. return undefined
  66. }
  67. async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
  68. const { url, comments, commentCreated } = params
  69. // Maybe it's a reply to a video?
  70. // If yes, it's done: we resolved all the thread
  71. const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false }
  72. const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam })
  73. if (video.isOwned() && !video.hasPrivacyForFederation()) {
  74. throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation')
  75. }
  76. let resultComment: MCommentOwnerVideo
  77. if (comments.length !== 0) {
  78. const firstReply = comments[comments.length - 1] as MCommentOwnerVideo
  79. firstReply.inReplyToCommentId = null
  80. firstReply.originCommentId = null
  81. firstReply.videoId = video.id
  82. firstReply.changed('updatedAt', true)
  83. firstReply.Video = video
  84. comments[comments.length - 1] = await firstReply.save()
  85. for (let i = comments.length - 2; i >= 0; i--) {
  86. const comment = comments[i] as MCommentOwnerVideo
  87. comment.originCommentId = firstReply.id
  88. comment.inReplyToCommentId = comments[i + 1].id
  89. comment.videoId = video.id
  90. comment.changed('updatedAt', true)
  91. comment.Video = video
  92. comments[i] = await comment.save()
  93. }
  94. resultComment = comments[0] as MCommentOwnerVideo
  95. }
  96. return { video, comment: resultComment, commentCreated }
  97. }
  98. async function resolveParentComment (params: ResolveThreadParams) {
  99. const { url, comments } = params
  100. if (comments.length > ACTIVITY_PUB.MAX_RECURSION_COMMENTS) {
  101. throw new Error('Recursion limit reached when resolving a thread')
  102. }
  103. const { body } = await doRequest<any>({
  104. uri: url,
  105. json: true,
  106. activityPub: true
  107. })
  108. if (sanitizeAndCheckVideoCommentObject(body) === false) {
  109. throw new Error('Remote video comment JSON is not valid:' + JSON.stringify(body))
  110. }
  111. const actorUrl = body.attributedTo
  112. if (!actorUrl && body.type !== 'Tombstone') throw new Error('Miss attributed to in comment')
  113. if (actorUrl && checkUrlsSameHost(url, actorUrl) !== true) {
  114. throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`)
  115. }
  116. if (checkUrlsSameHost(body.id, url) !== true) {
  117. throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`)
  118. }
  119. const actor = actorUrl
  120. ? await getOrCreateActorAndServerAndModel(actorUrl, 'all')
  121. : null
  122. const comment = new VideoCommentModel({
  123. url: body.id,
  124. text: body.content ? body.content : '',
  125. videoId: null,
  126. accountId: actor ? actor.Account.id : null,
  127. inReplyToCommentId: null,
  128. originCommentId: null,
  129. createdAt: new Date(body.published),
  130. updatedAt: new Date(body.updated),
  131. deletedAt: body.deleted ? new Date(body.deleted) : null
  132. }) as MCommentOwner
  133. comment.Account = actor ? actor.Account : null
  134. return resolveThread({
  135. url: body.inReplyTo,
  136. comments: comments.concat([ comment ]),
  137. commentCreated: true
  138. })
  139. }