ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

video-comment.ts 6.0 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { AutomaticTagPolicy, ResultList, UserRight, VideoCommentPolicy, VideoCommentThreadTree } from '@peertube/peertube-models'
  2. import { logger } from '@server/helpers/logger.js'
  3. import { sequelizeTypescript } from '@server/initializers/database.js'
  4. import { AccountModel } from '@server/models/account/account.js'
  5. import { AccountAutomaticTagPolicyModel } from '@server/models/automatic-tag/account-automatic-tag-policy.js'
  6. import express from 'express'
  7. import cloneDeep from 'lodash-es/cloneDeep.js'
  8. import { Transaction } from 'sequelize'
  9. import { VideoCommentModel } from '../models/video/video-comment.js'
  10. import {
  11. MComment,
  12. MCommentFormattable,
  13. MCommentOwnerVideo,
  14. MCommentOwnerVideoReply, MUserAccountId, MVideoAccountLight,
  15. MVideoFullLight
  16. } from '../types/models/index.js'
  17. import { sendCreateVideoCommentIfNeeded, sendDeleteVideoComment, sendReplyApproval } from './activitypub/send/index.js'
  18. import { getLocalVideoCommentActivityPubUrl } from './activitypub/url.js'
  19. import { AutomaticTagger } from './automatic-tags/automatic-tagger.js'
  20. import { setAndSaveCommentAutomaticTags } from './automatic-tags/automatic-tags.js'
  21. import { Notifier } from './notifier/notifier.js'
  22. import { Hooks } from './plugins/hooks.js'
  23. export async function removeComment (commentArg: MComment, req: express.Request, res: express.Response) {
  24. let videoCommentInstanceBefore: MCommentOwnerVideo
  25. await sequelizeTypescript.transaction(async t => {
  26. const comment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideoAndReply(commentArg.url, t)
  27. videoCommentInstanceBefore = cloneDeep(comment)
  28. if (comment.isOwned() || comment.Video.isOwned()) {
  29. await sendDeleteVideoComment(comment, t)
  30. }
  31. comment.markAsDeleted()
  32. await comment.save({ transaction: t })
  33. logger.info('Video comment %d deleted.', comment.id)
  34. })
  35. Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore, req, res })
  36. }
  37. export async function approveComment (commentArg: MComment) {
  38. await sequelizeTypescript.transaction(async t => {
  39. const comment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(commentArg.id, t)
  40. const oldHeldForReview = comment.heldForReview
  41. comment.heldForReview = false
  42. await comment.save({ transaction: t })
  43. if (comment.isOwned()) {
  44. await sendCreateVideoCommentIfNeeded(comment, t)
  45. } else {
  46. sendReplyApproval(comment, 'ApproveReply')
  47. }
  48. if (oldHeldForReview !== comment.heldForReview) {
  49. Notifier.Instance.notifyOnNewCommentApproval(comment)
  50. }
  51. logger.info('Video comment %d approved.', comment.id)
  52. })
  53. }
  54. export async function createLocalVideoComment (options: {
  55. text: string
  56. inReplyToComment: MComment | null
  57. video: MVideoFullLight
  58. user: MUserAccountId
  59. }) {
  60. const { user, video, text, inReplyToComment } = options
  61. let originCommentId: number | null = null
  62. let inReplyToCommentId: number | null = null
  63. if (inReplyToComment && inReplyToComment !== null) {
  64. originCommentId = inReplyToComment.originCommentId || inReplyToComment.id
  65. inReplyToCommentId = inReplyToComment.id
  66. }
  67. return sequelizeTypescript.transaction(async transaction => {
  68. const account = await AccountModel.load(user.Account.id, transaction)
  69. const automaticTags = await new AutomaticTagger().buildCommentsAutomaticTags({
  70. ownerAccount: video.VideoChannel.Account,
  71. text,
  72. transaction
  73. })
  74. const heldForReview = await shouldCommentBeHeldForReview({ user, video, automaticTags, transaction })
  75. const comment = await VideoCommentModel.create({
  76. text,
  77. originCommentId,
  78. inReplyToCommentId,
  79. videoId: video.id,
  80. accountId: account.id,
  81. heldForReview,
  82. url: new Date().toISOString()
  83. }, { transaction, validate: false })
  84. comment.url = getLocalVideoCommentActivityPubUrl(video, comment)
  85. const savedComment: MCommentOwnerVideoReply = await comment.save({ transaction })
  86. await setAndSaveCommentAutomaticTags({ comment: savedComment, automaticTags, transaction })
  87. savedComment.InReplyToVideoComment = inReplyToComment
  88. savedComment.Video = video
  89. savedComment.Account = account
  90. await sendCreateVideoCommentIfNeeded(savedComment, transaction)
  91. return savedComment
  92. })
  93. }
  94. export function buildFormattedCommentTree (resultList: ResultList<MCommentFormattable>): VideoCommentThreadTree {
  95. // Comments are sorted by id ASC
  96. const comments = resultList.data
  97. const comment = comments.shift()
  98. const thread: VideoCommentThreadTree = {
  99. comment: comment.toFormattedJSON(),
  100. children: []
  101. }
  102. const idx = {
  103. [comment.id]: thread
  104. }
  105. while (comments.length !== 0) {
  106. const childComment = comments.shift()
  107. const childCommentThread: VideoCommentThreadTree = {
  108. comment: childComment.toFormattedJSON(),
  109. children: []
  110. }
  111. const parentCommentThread = idx[childComment.inReplyToCommentId]
  112. // Maybe the parent comment was blocked by the admin/user
  113. if (!parentCommentThread) continue
  114. parentCommentThread.children.push(childCommentThread)
  115. idx[childComment.id] = childCommentThread
  116. }
  117. return thread
  118. }
  119. export async function shouldCommentBeHeldForReview (options: {
  120. user: MUserAccountId
  121. video: MVideoAccountLight
  122. automaticTags: { name: string, accountId: number }[]
  123. transaction?: Transaction
  124. }) {
  125. const { user, video, transaction, automaticTags } = options
  126. if (video.isOwned() && user) {
  127. if (user.hasRight(UserRight.MANAGE_ANY_VIDEO_COMMENT)) return false
  128. if (user.Account.id === video.VideoChannel.accountId) return false
  129. }
  130. if (video.commentsPolicy === VideoCommentPolicy.REQUIRES_APPROVAL) return true
  131. if (video.isOwned() !== true) return false
  132. const ownerAccountTags = automaticTags
  133. .filter(t => t.accountId === video.VideoChannel.accountId)
  134. .map(t => t.name)
  135. if (ownerAccountTags.length === 0) return false
  136. return AccountAutomaticTagPolicyModel.hasPolicyOnTags({
  137. accountId: video.VideoChannel.accountId,
  138. policy: AutomaticTagPolicy.REVIEW_COMMENT,
  139. tags: ownerAccountTags,
  140. transaction
  141. })
  142. }