video-comments.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  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'
  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. async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
  13. let originCommentId: number = null
  14. let inReplyToCommentId: number = null
  15. // If this is not a reply to the video (thread), create or get the parent comment
  16. if (video.url !== comment.inReplyTo) {
  17. const { comment: parent } = await addVideoComment(video, comment.inReplyTo)
  18. if (!parent) {
  19. logger.warn('Cannot fetch or get parent comment %s of comment %s.', comment.inReplyTo, comment.id)
  20. return undefined
  21. }
  22. originCommentId = parent.originCommentId || parent.id
  23. inReplyToCommentId = parent.id
  24. }
  25. return {
  26. url: comment.id,
  27. text: comment.content,
  28. videoId: video.id,
  29. accountId: actor.Account.id,
  30. inReplyToCommentId,
  31. originCommentId,
  32. createdAt: new Date(comment.published),
  33. updatedAt: new Date(comment.updated)
  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. const actor = await getOrCreateActorAndServerAndModel(actorUrl)
  55. const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
  56. if (!entry) return { created: false }
  57. const [ comment, created ] = await VideoCommentModel.findOrCreate({
  58. where: {
  59. url: body.id
  60. },
  61. defaults: entry
  62. })
  63. return { comment, created }
  64. }
  65. async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
  66. // Already have this comment?
  67. const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideo(url)
  68. if (commentFromDatabase) {
  69. let parentComments = comments.concat([ commentFromDatabase ])
  70. // Speed up things and resolve directly the thread
  71. if (commentFromDatabase.InReplyToVideoComment) {
  72. const data = await VideoCommentModel.listThreadParentComments(commentFromDatabase, undefined, 'DESC')
  73. parentComments = parentComments.concat(data)
  74. }
  75. return resolveThread(commentFromDatabase.Video.url, parentComments)
  76. }
  77. try {
  78. // Maybe it's a reply to a video?
  79. // If yes, it's done: we resolved all the thread
  80. const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: url })
  81. if (comments.length !== 0) {
  82. const firstReply = comments[ comments.length - 1 ]
  83. firstReply.inReplyToCommentId = null
  84. firstReply.originCommentId = null
  85. firstReply.videoId = video.id
  86. comments[comments.length - 1] = await firstReply.save()
  87. for (let i = comments.length - 2; i >= 0; i--) {
  88. const comment = comments[ i ]
  89. comment.originCommentId = firstReply.id
  90. comment.inReplyToCommentId = comments[ i + 1 ].id
  91. comment.videoId = video.id
  92. comments[i] = await comment.save()
  93. }
  94. }
  95. return { video, parents: comments }
  96. } catch (err) {
  97. logger.debug('Cannot get or create account and video and channel for reply %s, fetch comment', url, { err })
  98. if (comments.length > ACTIVITY_PUB.MAX_RECURSION_COMMENTS) {
  99. throw new Error('Recursion limit reached when resolving a thread')
  100. }
  101. const { body } = await doRequest({
  102. uri: url,
  103. json: true,
  104. activityPub: true
  105. })
  106. if (sanitizeAndCheckVideoCommentObject(body) === false) {
  107. throw new Error('Remote video comment JSON is not valid :' + JSON.stringify(body))
  108. }
  109. const actorUrl = body.attributedTo
  110. if (!actorUrl) throw new Error('Miss attributed to in comment')
  111. const actor = await getOrCreateActorAndServerAndModel(actorUrl)
  112. const comment = new VideoCommentModel({
  113. url: body.id,
  114. text: body.content,
  115. videoId: null,
  116. accountId: actor.Account.id,
  117. inReplyToCommentId: null,
  118. originCommentId: null,
  119. createdAt: new Date(body.published),
  120. updatedAt: new Date(body.updated)
  121. })
  122. return resolveThread(body.inReplyTo, comments.concat([ comment ]))
  123. }
  124. }
  125. export {
  126. videoCommentActivityObjectToDBAttributes,
  127. addVideoComments,
  128. addVideoComment,
  129. resolveThread
  130. }