ニジカ投稿局 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 21 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  1. import { pick } from '@peertube/peertube-core-utils'
  2. import {
  3. ActivityTagObject,
  4. ActivityTombstoneObject,
  5. UserRight,
  6. VideoComment,
  7. VideoCommentForAdminOrUser,
  8. VideoCommentObject
  9. } from '@peertube/peertube-models'
  10. import { extractMentions } from '@server/helpers/mentions.js'
  11. import { getLocalApproveReplyActivityPubUrl } from '@server/lib/activitypub/url.js'
  12. import { getServerActor } from '@server/models/application/application.js'
  13. import { MAccount, MAccountId, MUserAccountId } from '@server/types/models/index.js'
  14. import { Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize'
  15. import {
  16. AllowNull,
  17. BelongsTo, Column,
  18. CreatedAt,
  19. DataType,
  20. ForeignKey,
  21. HasMany,
  22. Is, Scopes,
  23. Table,
  24. UpdatedAt
  25. } from 'sequelize-typescript'
  26. import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
  27. import { CONSTRAINTS_FIELDS, USER_EXPORT_MAX_ITEMS } from '../../initializers/constants.js'
  28. import {
  29. MComment,
  30. MCommentAP,
  31. MCommentAdminOrUserFormattable,
  32. MCommentExport,
  33. MCommentFormattable,
  34. MCommentId,
  35. MCommentOwner,
  36. MCommentOwnerReplyVideoImmutable, MCommentOwnerVideoFeed,
  37. MCommentOwnerVideoReply,
  38. MVideo,
  39. MVideoImmutable
  40. } from '../../types/models/video/index.js'
  41. import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse.js'
  42. import { AccountModel } from '../account/account.js'
  43. import { ActorModel } from '../actor/actor.js'
  44. import { CommentAutomaticTagModel } from '../automatic-tag/comment-automatic-tag.js'
  45. import { SequelizeModel, buildLocalAccountIdsIn, buildSQLAttributes, throwIfNotValid } from '../shared/index.js'
  46. import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder.js'
  47. import { VideoChannelModel } from './video-channel.js'
  48. import { VideoModel } from './video.js'
  49. export enum ScopeNames {
  50. WITH_ACCOUNT = 'WITH_ACCOUNT',
  51. WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
  52. WITH_VIDEO = 'WITH_VIDEO'
  53. }
  54. @Scopes(() => ({
  55. [ScopeNames.WITH_ACCOUNT]: {
  56. include: [
  57. {
  58. model: AccountModel
  59. }
  60. ]
  61. },
  62. [ScopeNames.WITH_IN_REPLY_TO]: {
  63. include: [
  64. {
  65. model: VideoCommentModel,
  66. as: 'InReplyToVideoComment'
  67. }
  68. ]
  69. },
  70. [ScopeNames.WITH_VIDEO]: {
  71. include: [
  72. {
  73. model: VideoModel,
  74. required: true,
  75. include: [
  76. {
  77. model: VideoChannelModel.unscoped(),
  78. attributes: [ 'id', 'accountId' ],
  79. required: true,
  80. include: [
  81. {
  82. attributes: [ 'id', 'url' ],
  83. model: ActorModel.unscoped(),
  84. required: true
  85. },
  86. {
  87. attributes: [ 'id' ],
  88. model: AccountModel.unscoped(),
  89. required: true,
  90. include: [
  91. {
  92. attributes: [ 'id', 'url' ],
  93. model: ActorModel.unscoped(),
  94. required: true
  95. }
  96. ]
  97. }
  98. ]
  99. }
  100. ]
  101. }
  102. ]
  103. }
  104. }))
  105. @Table({
  106. tableName: 'videoComment',
  107. indexes: [
  108. {
  109. fields: [ 'videoId' ]
  110. },
  111. {
  112. fields: [ 'videoId', 'originCommentId' ]
  113. },
  114. {
  115. fields: [ 'url' ],
  116. unique: true
  117. },
  118. {
  119. fields: [ 'accountId' ]
  120. },
  121. {
  122. fields: [
  123. { name: 'createdAt', order: 'DESC' }
  124. ]
  125. }
  126. ]
  127. })
  128. export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
  129. @CreatedAt
  130. createdAt: Date
  131. @UpdatedAt
  132. updatedAt: Date
  133. @AllowNull(true)
  134. @Column(DataType.DATE)
  135. deletedAt: Date
  136. @AllowNull(false)
  137. @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
  138. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
  139. url: string
  140. @AllowNull(false)
  141. @Column(DataType.TEXT)
  142. text: string
  143. @AllowNull(false)
  144. @Column
  145. heldForReview: boolean
  146. @AllowNull(true)
  147. @Column
  148. replyApproval: string
  149. @ForeignKey(() => VideoCommentModel)
  150. @Column
  151. originCommentId: number
  152. @BelongsTo(() => VideoCommentModel, {
  153. foreignKey: {
  154. name: 'originCommentId',
  155. allowNull: true
  156. },
  157. as: 'OriginVideoComment',
  158. onDelete: 'CASCADE'
  159. })
  160. OriginVideoComment: Awaited<VideoCommentModel>
  161. @ForeignKey(() => VideoCommentModel)
  162. @Column
  163. inReplyToCommentId: number
  164. @BelongsTo(() => VideoCommentModel, {
  165. foreignKey: {
  166. name: 'inReplyToCommentId',
  167. allowNull: true
  168. },
  169. as: 'InReplyToVideoComment',
  170. onDelete: 'CASCADE'
  171. })
  172. InReplyToVideoComment: Awaited<VideoCommentModel> | null
  173. @ForeignKey(() => VideoModel)
  174. @Column
  175. videoId: number
  176. @BelongsTo(() => VideoModel, {
  177. foreignKey: {
  178. allowNull: false
  179. },
  180. onDelete: 'CASCADE'
  181. })
  182. Video: Awaited<VideoModel>
  183. @ForeignKey(() => AccountModel)
  184. @Column
  185. accountId: number
  186. @BelongsTo(() => AccountModel, {
  187. foreignKey: {
  188. allowNull: true
  189. },
  190. onDelete: 'CASCADE'
  191. })
  192. Account: Awaited<AccountModel>
  193. @HasMany(() => VideoCommentAbuseModel, {
  194. foreignKey: {
  195. name: 'videoCommentId',
  196. allowNull: true
  197. },
  198. onDelete: 'set null'
  199. })
  200. CommentAbuses: Awaited<VideoCommentAbuseModel>[]
  201. @HasMany(() => CommentAutomaticTagModel, {
  202. foreignKey: 'commentId',
  203. onDelete: 'CASCADE'
  204. })
  205. CommentAutomaticTags: Awaited<CommentAutomaticTagModel>[]
  206. // ---------------------------------------------------------------------------
  207. static getSQLAttributes (tableName: string, aliasPrefix = '') {
  208. return buildSQLAttributes({
  209. model: this,
  210. tableName,
  211. aliasPrefix
  212. })
  213. }
  214. // ---------------------------------------------------------------------------
  215. static loadById (id: number, transaction?: Transaction): Promise<MComment> {
  216. const query = {
  217. where: {
  218. id
  219. },
  220. transaction
  221. }
  222. return VideoCommentModel.findOne(query)
  223. }
  224. static loadByIdAndPopulateVideoAndAccountAndReply (id: number, transaction?: Transaction): Promise<MCommentOwnerVideoReply> {
  225. const query = {
  226. where: {
  227. id
  228. },
  229. transaction
  230. }
  231. return VideoCommentModel
  232. .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
  233. .findOne(query)
  234. }
  235. // ---------------------------------------------------------------------------
  236. static loadByUrlAndPopulateAccountAndVideoAndReply (url: string, transaction?: Transaction): Promise<MCommentOwnerVideoReply> {
  237. const query = {
  238. where: {
  239. url
  240. },
  241. transaction
  242. }
  243. return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEO, ScopeNames.WITH_IN_REPLY_TO ]).findOne(query)
  244. }
  245. static loadByUrlAndPopulateReplyAndVideoImmutableAndAccount (
  246. url: string,
  247. transaction?: Transaction
  248. ): Promise<MCommentOwnerReplyVideoImmutable> {
  249. const query = {
  250. where: {
  251. url
  252. },
  253. include: [
  254. {
  255. attributes: [ 'id', 'uuid', 'url', 'remote' ],
  256. model: VideoModel.unscoped()
  257. }
  258. ],
  259. transaction
  260. }
  261. return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_ACCOUNT ]).findOne(query)
  262. }
  263. // ---------------------------------------------------------------------------
  264. static listCommentsForApi (parameters: {
  265. start: number
  266. count: number
  267. sort: string
  268. autoTagOfAccountId: number
  269. videoAccountOwnerId?: number
  270. videoChannelOwnerId?: number
  271. onLocalVideo?: boolean
  272. isLocal?: boolean
  273. search?: string
  274. searchAccount?: string
  275. searchVideo?: string
  276. heldForReview: boolean
  277. videoId?: number
  278. videoChannelId?: number
  279. autoTagOneOf?: string[]
  280. }) {
  281. const queryOptions: ListVideoCommentsOptions = {
  282. ...pick(parameters, [
  283. 'start',
  284. 'count',
  285. 'sort',
  286. 'isLocal',
  287. 'search',
  288. 'searchVideo',
  289. 'searchAccount',
  290. 'onLocalVideo',
  291. 'videoId',
  292. 'videoChannelId',
  293. 'autoTagOneOf',
  294. 'autoTagOfAccountId',
  295. 'videoAccountOwnerId',
  296. 'videoChannelOwnerId',
  297. 'heldForReview'
  298. ]),
  299. selectType: 'api',
  300. notDeleted: true
  301. }
  302. return Promise.all([
  303. new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminOrUserFormattable>(),
  304. new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
  305. ]).then(([ rows, count ]) => {
  306. return { total: count, data: rows }
  307. })
  308. }
  309. static async listThreadsForApi (parameters: {
  310. video: MVideo
  311. start: number
  312. count: number
  313. sort: string
  314. user?: MUserAccountId
  315. }) {
  316. const { video, user } = parameters
  317. const { blockerAccountIds, canSeeHeldForReview } = await VideoCommentModel.buildBlockerAccountIdsAndCanSeeHeldForReview({ user, video })
  318. const commonOptions: ListVideoCommentsOptions = {
  319. selectType: 'api',
  320. videoId: video.id,
  321. blockerAccountIds,
  322. heldForReview: canSeeHeldForReview
  323. ? undefined // Display all comments for video owner or moderator
  324. : false,
  325. heldForReviewAccountIdException: user?.Account?.id
  326. }
  327. const listOptions: ListVideoCommentsOptions = {
  328. ...commonOptions,
  329. ...pick(parameters, [ 'sort', 'start', 'count' ]),
  330. isThread: true,
  331. includeReplyCounters: true
  332. }
  333. const countOptions: ListVideoCommentsOptions = {
  334. ...commonOptions,
  335. isThread: true
  336. }
  337. const notDeletedCountOptions: ListVideoCommentsOptions = {
  338. ...commonOptions,
  339. notDeleted: true
  340. }
  341. return Promise.all([
  342. new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, listOptions).listComments<MCommentAdminOrUserFormattable>(),
  343. new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, countOptions).countComments(),
  344. new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, notDeletedCountOptions).countComments()
  345. ]).then(([ rows, count, totalNotDeletedComments ]) => {
  346. return { total: count, data: rows, totalNotDeletedComments }
  347. })
  348. }
  349. static async listThreadCommentsForApi (parameters: {
  350. video: MVideo
  351. threadId: number
  352. user?: MUserAccountId
  353. }) {
  354. const { user, video, threadId } = parameters
  355. const { blockerAccountIds, canSeeHeldForReview } = await VideoCommentModel.buildBlockerAccountIdsAndCanSeeHeldForReview({ user, video })
  356. const queryOptions: ListVideoCommentsOptions = {
  357. threadId,
  358. videoId: video.id,
  359. selectType: 'api',
  360. sort: 'createdAt',
  361. blockerAccountIds,
  362. includeReplyCounters: true,
  363. heldForReview: canSeeHeldForReview
  364. ? undefined // Display all comments for video owner or moderator
  365. : false,
  366. heldForReviewAccountIdException: user?.Account?.id
  367. }
  368. return Promise.all([
  369. new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminOrUserFormattable>(),
  370. new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
  371. ]).then(([ rows, count ]) => {
  372. return { total: count, data: rows }
  373. })
  374. }
  375. static listThreadParentComments (options: {
  376. comment: MCommentId
  377. transaction?: Transaction
  378. order?: 'ASC' | 'DESC'
  379. }): Promise<MCommentOwner[]> {
  380. const { comment, transaction, order = 'ASC' } = options
  381. const query = {
  382. order: [ [ 'createdAt', order ] ] as Order,
  383. where: {
  384. id: {
  385. [Op.in]: Sequelize.literal('(' +
  386. 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
  387. `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
  388. 'UNION ' +
  389. 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' +
  390. 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
  391. ') ' +
  392. 'SELECT id FROM children' +
  393. ')'),
  394. [Op.ne]: comment.id
  395. }
  396. },
  397. transaction
  398. }
  399. return VideoCommentModel
  400. .scope([ ScopeNames.WITH_ACCOUNT ])
  401. .findAll(query)
  402. }
  403. static async listAndCountByVideoForAP (parameters: {
  404. video: MVideoImmutable
  405. start: number
  406. count: number
  407. }) {
  408. const { video } = parameters
  409. const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
  410. const queryOptions: ListVideoCommentsOptions = {
  411. ...pick(parameters, [ 'start', 'count' ]),
  412. selectType: 'comment-only',
  413. videoId: video.id,
  414. sort: 'createdAt',
  415. heldForReview: false,
  416. blockerAccountIds
  417. }
  418. return Promise.all([
  419. new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>(),
  420. new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
  421. ]).then(([ rows, count ]) => {
  422. return { total: count, data: rows }
  423. })
  424. }
  425. static async listForFeed (parameters: {
  426. start: number
  427. count: number
  428. videoId?: number
  429. videoAccountOwnerId?: number
  430. videoChannelOwnerId?: number
  431. }) {
  432. const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
  433. const queryOptions: ListVideoCommentsOptions = {
  434. ...pick(parameters, [ 'start', 'count', 'videoAccountOwnerId', 'videoId', 'videoChannelOwnerId' ]),
  435. selectType: 'feed',
  436. sort: '-createdAt',
  437. onPublicVideo: true,
  438. notDeleted: true,
  439. heldForReview: false,
  440. blockerAccountIds
  441. }
  442. return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentOwnerVideoFeed>()
  443. }
  444. static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) {
  445. const queryOptions: ListVideoCommentsOptions = {
  446. selectType: 'comment-only',
  447. accountId: ofAccount.id,
  448. videoAccountOwnerId: filter.onVideosOfAccount?.id,
  449. heldForReview: undefined,
  450. notDeleted: true,
  451. count: 5000
  452. }
  453. return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>()
  454. }
  455. static listForExport (ofAccountId: number): Promise<MCommentExport[]> {
  456. return VideoCommentModel.findAll({
  457. attributes: [ 'id', 'url', 'text', 'createdAt' ],
  458. where: {
  459. accountId: ofAccountId,
  460. deletedAt: null
  461. },
  462. include: [
  463. {
  464. attributes: [ 'id', 'uuid', 'url' ],
  465. required: true,
  466. model: VideoModel.unscoped()
  467. },
  468. {
  469. attributes: [ 'url' ],
  470. required: false,
  471. model: VideoCommentModel,
  472. as: 'InReplyToVideoComment'
  473. }
  474. ],
  475. limit: USER_EXPORT_MAX_ITEMS
  476. })
  477. }
  478. // ---------------------------------------------------------------------------
  479. static async getStats () {
  480. const where = {
  481. deletedAt: null,
  482. heldForReview: false
  483. }
  484. const totalLocalVideoComments = await VideoCommentModel.count({
  485. where,
  486. include: [
  487. {
  488. model: AccountModel.unscoped(),
  489. required: true,
  490. include: [
  491. {
  492. model: ActorModel.unscoped(),
  493. required: true,
  494. where: {
  495. serverId: null
  496. }
  497. }
  498. ]
  499. }
  500. ]
  501. })
  502. const totalVideoComments = await VideoCommentModel.count({ where })
  503. return {
  504. totalLocalVideoComments,
  505. totalVideoComments
  506. }
  507. }
  508. // ---------------------------------------------------------------------------
  509. static listRemoteCommentUrlsOfLocalVideos () {
  510. const query = `SELECT "videoComment".url FROM "videoComment" ` +
  511. `INNER JOIN account ON account.id = "videoComment"."accountId" ` +
  512. `INNER JOIN actor ON actor.id = "account"."actorId" AND actor."serverId" IS NOT NULL ` +
  513. `INNER JOIN video ON video.id = "videoComment"."videoId" AND video.remote IS FALSE`
  514. return VideoCommentModel.sequelize.query<{ url: string }>(query, {
  515. type: QueryTypes.SELECT,
  516. raw: true
  517. }).then(rows => rows.map(r => r.url))
  518. }
  519. static cleanOldCommentsOf (videoId: number, beforeUpdatedAt: Date) {
  520. const query = {
  521. where: {
  522. updatedAt: {
  523. [Op.lt]: beforeUpdatedAt
  524. },
  525. videoId,
  526. accountId: {
  527. [Op.notIn]: buildLocalAccountIdsIn()
  528. },
  529. // Do not delete Tombstones
  530. deletedAt: null
  531. }
  532. }
  533. return VideoCommentModel.destroy(query)
  534. }
  535. // ---------------------------------------------------------------------------
  536. getCommentStaticPath () {
  537. return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId()
  538. }
  539. getCommentUserReviewPath () {
  540. return '/my-account/videos/comments?search=heldForReview:true'
  541. }
  542. getThreadId (): number {
  543. return this.originCommentId || this.id
  544. }
  545. isOwned () {
  546. if (!this.Account) return false
  547. return this.Account.isOwned()
  548. }
  549. markAsDeleted () {
  550. this.text = ''
  551. this.deletedAt = new Date()
  552. this.accountId = null
  553. }
  554. isDeleted () {
  555. return this.deletedAt !== null
  556. }
  557. extractMentions () {
  558. return extractMentions(this.text, this.isOwned())
  559. }
  560. toFormattedJSON (this: MCommentFormattable) {
  561. return {
  562. id: this.id,
  563. url: this.url,
  564. text: this.text,
  565. threadId: this.getThreadId(),
  566. inReplyToCommentId: this.inReplyToCommentId || null,
  567. videoId: this.videoId,
  568. createdAt: this.createdAt,
  569. updatedAt: this.updatedAt,
  570. deletedAt: this.deletedAt,
  571. heldForReview: this.heldForReview,
  572. isDeleted: this.isDeleted(),
  573. totalRepliesFromVideoAuthor: this.get('totalRepliesFromVideoAuthor') || 0,
  574. totalReplies: this.get('totalReplies') || 0,
  575. account: this.Account
  576. ? this.Account.toFormattedJSON()
  577. : null
  578. } as VideoComment
  579. }
  580. toFormattedForAdminOrUserJSON (this: MCommentAdminOrUserFormattable) {
  581. return {
  582. id: this.id,
  583. url: this.url,
  584. text: this.text,
  585. threadId: this.getThreadId(),
  586. inReplyToCommentId: this.inReplyToCommentId || null,
  587. videoId: this.videoId,
  588. createdAt: this.createdAt,
  589. updatedAt: this.updatedAt,
  590. heldForReview: this.heldForReview,
  591. automaticTags: (this.CommentAutomaticTags || []).map(m => m.AutomaticTag.name),
  592. video: {
  593. id: this.Video.id,
  594. uuid: this.Video.uuid,
  595. name: this.Video.name
  596. },
  597. account: this.Account
  598. ? this.Account.toFormattedJSON()
  599. : null
  600. } as VideoCommentForAdminOrUser
  601. }
  602. toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject | ActivityTombstoneObject {
  603. const inReplyTo = this.inReplyToCommentId === null
  604. ? this.Video.url // New thread, so we reply to the video
  605. : this.InReplyToVideoComment.url
  606. if (this.isDeleted()) {
  607. return {
  608. id: this.url,
  609. type: 'Tombstone',
  610. formerType: 'Note',
  611. inReplyTo,
  612. published: this.createdAt.toISOString(),
  613. updated: this.updatedAt.toISOString(),
  614. deleted: this.deletedAt.toISOString()
  615. }
  616. }
  617. const tag: ActivityTagObject[] = []
  618. for (const parentComment of threadParentComments) {
  619. if (!parentComment.Account) continue
  620. const actor = parentComment.Account.Actor
  621. tag.push({
  622. type: 'Mention',
  623. href: actor.url,
  624. name: `@${actor.preferredUsername}@${actor.getHost()}`
  625. })
  626. }
  627. let replyApproval = this.replyApproval
  628. if (this.Video.isOwned() && !this.heldForReview) {
  629. replyApproval = getLocalApproveReplyActivityPubUrl(this.Video, this)
  630. }
  631. return {
  632. type: 'Note' as 'Note',
  633. id: this.url,
  634. content: this.text,
  635. mediaType: 'text/markdown',
  636. inReplyTo,
  637. updated: this.updatedAt.toISOString(),
  638. published: this.createdAt.toISOString(),
  639. url: this.url,
  640. attributedTo: this.Account.Actor.url,
  641. replyApproval,
  642. tag
  643. }
  644. }
  645. private static async buildBlockerAccountIds (options: {
  646. user: MUserAccountId
  647. }): Promise<number[]> {
  648. const { user } = options
  649. const serverActor = await getServerActor()
  650. const blockerAccountIds = [ serverActor.Account.id ]
  651. if (user) blockerAccountIds.push(user.Account.id)
  652. return blockerAccountIds
  653. }
  654. private static buildBlockerAccountIdsAndCanSeeHeldForReview (options: {
  655. video: MVideo
  656. user: MUserAccountId
  657. }) {
  658. const { video, user } = options
  659. const blockerAccountIdsPromise = this.buildBlockerAccountIds(options)
  660. let canSeeHeldForReviewPromise: Promise<boolean>
  661. if (user) {
  662. if (user.hasRight(UserRight.SEE_ALL_COMMENTS)) {
  663. canSeeHeldForReviewPromise = Promise.resolve(true)
  664. } else {
  665. canSeeHeldForReviewPromise = VideoChannelModel.loadAndPopulateAccount(video.channelId)
  666. .then(c => c.accountId === user.Account.id)
  667. }
  668. } else {
  669. canSeeHeldForReviewPromise = Promise.resolve(false)
  670. }
  671. return Promise.all([ blockerAccountIdsPromise, canSeeHeldForReviewPromise ])
  672. .then(([ blockerAccountIds, canSeeHeldForReview ]) => ({ blockerAccountIds, canSeeHeldForReview }))
  673. }
  674. }