video-comments.ts 6.1 KB

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