]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-comment.ts
Implement abuses check params
[github/Chocobozzz/PeerTube.git] / server / models / video / video-comment.ts
CommitLineData
444c0a0e
C
1import * as Bluebird from 'bluebird'
2import { uniq } from 'lodash'
3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
57f6896f
C
4import {
5 AllowNull,
6 BeforeDestroy,
7 BelongsTo,
8 Column,
9 CreatedAt,
10 DataType,
11 ForeignKey,
12 HasMany,
13 Is,
14 Model,
15 Scopes,
16 Table,
17 UpdatedAt
18} from 'sequelize-typescript'
19import { logger } from '@server/helpers/logger'
444c0a0e 20import { getServerActor } from '@server/models/application/application'
26d6bf65 21import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
444c0a0e 22import { VideoPrivacy } from '@shared/models'
69222afa 23import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
ea44f375 24import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
bf1f6508 25import { VideoComment } from '../../../shared/models/videos/video-comment.model'
f7cc67b4 26import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
444c0a0e 27import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
f7cc67b4 28import { regexpCapture } from '../../helpers/regexp'
444c0a0e 29import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
453e83ea
C
30import {
31 MComment,
b5fecbf4 32 MCommentAP,
1ca9f7c3 33 MCommentFormattable,
453e83ea
C
34 MCommentId,
35 MCommentOwner,
36 MCommentOwnerReplyVideoLight,
37 MCommentOwnerVideo,
38 MCommentOwnerVideoFeed,
696d83fd
C
39 MCommentOwnerVideoReply,
40 MVideoImmutable
26d6bf65 41} from '../../types/models/video'
57f6896f 42import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
444c0a0e 43import { AccountModel } from '../account/account'
8adf0a76 44import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
444c0a0e
C
45import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
46import { VideoModel } from './video'
47import { VideoChannelModel } from './video-channel'
6d852470 48
bf1f6508 49enum ScopeNames {
ea44f375 50 WITH_ACCOUNT = 'WITH_ACCOUNT',
8adf0a76 51 WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API',
4635f59d 52 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
da854ddd 53 WITH_VIDEO = 'WITH_VIDEO',
4635f59d 54 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
bf1f6508
C
55}
56
3acc5084 57@Scopes(() => ({
696d83fd 58 [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => {
7ad9b984
C
59 return {
60 attributes: {
61 include: [
62 [
63 Sequelize.literal(
64 '(' +
696d83fd 65 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' +
7ad9b984
C
66 'SELECT COUNT("replies"."id") - (' +
67 'SELECT COUNT("replies"."id") ' +
68 'FROM "videoComment" AS "replies" ' +
69 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
70 'AND "accountId" IN (SELECT "id" FROM "blocklist")' +
71 ')' +
72 'FROM "videoComment" AS "replies" ' +
73 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
74 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' +
75 ')'
76 ),
77 'totalReplies'
5b0413dd
RK
78 ],
79 [
80 Sequelize.literal(
81 '(' +
82 'SELECT COUNT("replies"."id") ' +
83 'FROM "videoComment" AS "replies" ' +
562724a1
C
84 'INNER JOIN "video" ON "video"."id" = "replies"."videoId" ' +
85 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
5b0413dd 86 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
562724a1 87 'AND "replies"."accountId" = "videoChannel"."accountId"' +
5b0413dd
RK
88 ')'
89 ),
90 'totalRepliesFromVideoAuthor'
7ad9b984 91 ]
4635f59d 92 ]
7ad9b984 93 }
3acc5084 94 } as FindOptions
4635f59d 95 },
d3ea8975 96 [ScopeNames.WITH_ACCOUNT]: {
bf1f6508 97 include: [
4635f59d 98 {
453e83ea 99 model: AccountModel
4635f59d 100 }
3acc5084 101 ]
ea44f375 102 },
8adf0a76
C
103 [ScopeNames.WITH_ACCOUNT_FOR_API]: {
104 include: [
105 {
106 model: AccountModel.unscoped(),
107 include: [
108 {
109 attributes: {
110 exclude: unusedActorAttributesForAPI
111 },
112 model: ActorModel, // Default scope includes avatar and server
113 required: true
114 }
115 ]
116 }
117 ]
118 },
ea44f375
C
119 [ScopeNames.WITH_IN_REPLY_TO]: {
120 include: [
121 {
3acc5084 122 model: VideoCommentModel,
da854ddd
C
123 as: 'InReplyToVideoComment'
124 }
125 ]
126 },
127 [ScopeNames.WITH_VIDEO]: {
128 include: [
129 {
3acc5084 130 model: VideoModel,
4cb6d457
C
131 required: true,
132 include: [
133 {
453e83ea 134 model: VideoChannelModel,
4cb6d457
C
135 required: true,
136 include: [
137 {
3acc5084 138 model: AccountModel,
453e83ea 139 required: true
4cb6d457
C
140 }
141 ]
142 }
143 ]
ea44f375 144 }
3acc5084 145 ]
bf1f6508 146 }
3acc5084 147}))
6d852470
C
148@Table({
149 tableName: 'videoComment',
150 indexes: [
151 {
152 fields: [ 'videoId' ]
bf1f6508
C
153 },
154 {
155 fields: [ 'videoId', 'originCommentId' ]
0776d83f
C
156 },
157 {
158 fields: [ 'url' ],
159 unique: true
8cd72bd3
C
160 },
161 {
162 fields: [ 'accountId' ]
b84d4c80
C
163 },
164 {
165 fields: [
166 { name: 'createdAt', order: 'DESC' }
167 ]
6d852470
C
168 }
169 ]
170})
171export class VideoCommentModel extends Model<VideoCommentModel> {
172 @CreatedAt
173 createdAt: Date
174
175 @UpdatedAt
176 updatedAt: Date
177
69222afa
JM
178 @AllowNull(true)
179 @Column(DataType.DATE)
180 deletedAt: Date
181
6d852470
C
182 @AllowNull(false)
183 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
184 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
185 url: string
186
187 @AllowNull(false)
188 @Column(DataType.TEXT)
189 text: string
190
191 @ForeignKey(() => VideoCommentModel)
192 @Column
193 originCommentId: number
194
195 @BelongsTo(() => VideoCommentModel, {
196 foreignKey: {
db799da3 197 name: 'originCommentId',
6d852470
C
198 allowNull: true
199 },
db799da3 200 as: 'OriginVideoComment',
6d852470
C
201 onDelete: 'CASCADE'
202 })
203 OriginVideoComment: VideoCommentModel
204
205 @ForeignKey(() => VideoCommentModel)
206 @Column
207 inReplyToCommentId: number
208
209 @BelongsTo(() => VideoCommentModel, {
210 foreignKey: {
db799da3 211 name: 'inReplyToCommentId',
6d852470
C
212 allowNull: true
213 },
da854ddd 214 as: 'InReplyToVideoComment',
6d852470
C
215 onDelete: 'CASCADE'
216 })
c1e791ba 217 InReplyToVideoComment: VideoCommentModel | null
6d852470
C
218
219 @ForeignKey(() => VideoModel)
220 @Column
221 videoId: number
222
223 @BelongsTo(() => VideoModel, {
224 foreignKey: {
225 allowNull: false
226 },
227 onDelete: 'CASCADE'
228 })
229 Video: VideoModel
230
d3ea8975 231 @ForeignKey(() => AccountModel)
6d852470 232 @Column
d3ea8975 233 accountId: number
6d852470 234
d3ea8975 235 @BelongsTo(() => AccountModel, {
6d852470 236 foreignKey: {
69222afa 237 allowNull: true
6d852470
C
238 },
239 onDelete: 'CASCADE'
240 })
d3ea8975 241 Account: AccountModel
6d852470 242
57f6896f
C
243 @HasMany(() => VideoCommentAbuseModel, {
244 foreignKey: {
245 name: 'commentId',
246 allowNull: true
247 },
248 onDelete: 'set null'
249 })
250 CommentAbuses: VideoCommentAbuseModel[]
251
252 @BeforeDestroy
253 static async saveEssentialDataToAbuses (instance: VideoCommentModel, options) {
254 const tasks: Promise<any>[] = []
255
256 if (!Array.isArray(instance.CommentAbuses)) {
257 instance.CommentAbuses = await instance.$get('CommentAbuses')
258
259 if (instance.CommentAbuses.length === 0) return undefined
260 }
261
262 if (!instance.Video) {
263 instance.Video = await instance.$get('Video')
264 }
265
266 logger.info('Saving video comment %s for abuse.', instance.url)
267
268 const details = Object.assign(instance.toFormattedJSON(), {
269 Video: {
270 id: instance.Video.id,
271 name: instance.Video.name,
272 uuid: instance.Video.uuid
273 }
274 })
275
276 for (const abuse of instance.CommentAbuses) {
277 abuse.deletedComment = details
278
279 tasks.push(abuse.save({ transaction: options.transaction }))
280 }
281
282 Promise.all(tasks)
283 .catch(err => {
284 logger.error('Some errors when saving details of comment %s in its abuses before destroy hook.', instance.url, { err })
285 })
286
287 return undefined
288 }
289
453e83ea 290 static loadById (id: number, t?: Transaction): Bluebird<MComment> {
1735c825 291 const query: FindOptions = {
bf1f6508
C
292 where: {
293 id
294 }
295 }
296
297 if (t !== undefined) query.transaction = t
298
299 return VideoCommentModel.findOne(query)
300 }
301
453e83ea 302 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Transaction): Bluebird<MCommentOwnerVideoReply> {
1735c825 303 const query: FindOptions = {
da854ddd
C
304 where: {
305 id
306 }
307 }
308
309 if (t !== undefined) query.transaction = t
310
311 return VideoCommentModel
312 .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
313 .findOne(query)
314 }
315
453e83ea 316 static loadByUrlAndPopulateAccountAndVideo (url: string, t?: Transaction): Bluebird<MCommentOwnerVideo> {
1735c825 317 const query: FindOptions = {
6d852470
C
318 where: {
319 url
320 }
321 }
322
323 if (t !== undefined) query.transaction = t
324
511765c9 325 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEO ]).findOne(query)
6d852470 326 }
bf1f6508 327
453e83ea 328 static loadByUrlAndPopulateReplyAndVideoUrlAndAccount (url: string, t?: Transaction): Bluebird<MCommentOwnerReplyVideoLight> {
1735c825 329 const query: FindOptions = {
4cb6d457
C
330 where: {
331 url
6b9c966f
C
332 },
333 include: [
334 {
335 attributes: [ 'id', 'url' ],
336 model: VideoModel.unscoped()
337 }
338 ]
4cb6d457
C
339 }
340
341 if (t !== undefined) query.transaction = t
342
6b9c966f 343 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_ACCOUNT ]).findOne(query)
4cb6d457
C
344 }
345
b4055e1c 346 static async listThreadsForApi (parameters: {
a1587156 347 videoId: number
696d83fd 348 isVideoOwned: boolean
a1587156
C
349 start: number
350 count: number
351 sort: string
453e83ea 352 user?: MUserAccountId
b4055e1c 353 }) {
696d83fd 354 const { videoId, isVideoOwned, start, count, sort, user } = parameters
b4055e1c 355
696d83fd 356 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned })
7ad9b984 357
bf1f6508
C
358 const query = {
359 offset: start,
360 limit: count,
c1125bca 361 order: getCommentSort(sort),
bf1f6508 362 where: {
8adf0a76
C
363 [Op.and]: [
364 {
365 videoId
366 },
367 {
368 inReplyToCommentId: null
369 },
370 {
371 [Op.or]: [
372 {
373 accountId: {
374 [Op.notIn]: Sequelize.literal(
696d83fd 375 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
8adf0a76
C
376 )
377 }
378 },
379 {
380 accountId: null
381 }
382 ]
383 }
384 ]
bf1f6508
C
385 }
386 }
387
3acc5084 388 const scopes: (string | ScopeOptions)[] = [
8adf0a76 389 ScopeNames.WITH_ACCOUNT_FOR_API,
7ad9b984 390 {
696d83fd 391 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
7ad9b984
C
392 }
393 ]
394
bf1f6508 395 return VideoCommentModel
7ad9b984 396 .scope(scopes)
bf1f6508
C
397 .findAndCountAll(query)
398 .then(({ rows, count }) => {
399 return { total: count, data: rows }
400 })
401 }
402
b4055e1c 403 static async listThreadCommentsForApi (parameters: {
a1587156 404 videoId: number
696d83fd 405 isVideoOwned: boolean
a1587156 406 threadId: number
453e83ea 407 user?: MUserAccountId
b4055e1c 408 }) {
696d83fd 409 const { videoId, threadId, user, isVideoOwned } = parameters
b4055e1c 410
696d83fd 411 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned })
7ad9b984 412
bf1f6508 413 const query = {
1735c825 414 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order,
bf1f6508
C
415 where: {
416 videoId,
a1587156 417 [Op.or]: [
bf1f6508
C
418 { id: threadId },
419 { originCommentId: threadId }
7ad9b984
C
420 ],
421 accountId: {
1735c825 422 [Op.notIn]: Sequelize.literal(
696d83fd 423 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
7ad9b984
C
424 )
425 }
bf1f6508
C
426 }
427 }
428
7ad9b984 429 const scopes: any[] = [
8adf0a76 430 ScopeNames.WITH_ACCOUNT_FOR_API,
7ad9b984 431 {
696d83fd 432 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
7ad9b984
C
433 }
434 ]
435
bf1f6508 436 return VideoCommentModel
7ad9b984 437 .scope(scopes)
bf1f6508
C
438 .findAndCountAll(query)
439 .then(({ rows, count }) => {
440 return { total: count, data: rows }
441 })
442 }
443
453e83ea 444 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Bluebird<MCommentOwner[]> {
d7e70384 445 const query = {
1735c825 446 order: [ [ 'createdAt', order ] ] as Order,
d7e70384 447 where: {
d7e70384 448 id: {
a1587156 449 [Op.in]: Sequelize.literal('(' +
a3cffab4 450 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
f7cc67b4
C
451 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
452 'UNION ' +
453 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' +
454 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
455 ') ' +
a3cffab4
C
456 'SELECT id FROM children' +
457 ')'),
a1587156 458 [Op.ne]: comment.id
d7e70384
C
459 }
460 },
461 transaction: t
462 }
463
464 return VideoCommentModel
465 .scope([ ScopeNames.WITH_ACCOUNT ])
466 .findAll(query)
467 }
468
696d83fd
C
469 static async listAndCountByVideoForAP (video: MVideoImmutable, start: number, count: number, t?: Transaction) {
470 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({
471 videoId: video.id,
472 isVideoOwned: video.isOwned()
473 })
474
8fffe21a 475 const query = {
696d83fd 476 order: [ [ 'createdAt', 'ASC' ] ] as Order,
9a4a9b6c
C
477 offset: start,
478 limit: count,
8fffe21a 479 where: {
696d83fd
C
480 videoId: video.id,
481 accountId: {
482 [Op.notIn]: Sequelize.literal(
483 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
484 )
485 }
8fffe21a
C
486 },
487 transaction: t
488 }
489
453e83ea 490 return VideoCommentModel.findAndCountAll<MComment>(query)
8fffe21a
C
491 }
492
00494d6e
RK
493 static async listForFeed (parameters: {
494 start: number
495 count: number
496 videoId?: number
497 accountId?: number
498 videoChannelId?: number
499 }): Promise<MCommentOwnerVideoFeed[]> {
1df8a4d7 500 const serverActor = await getServerActor()
00494d6e
RK
501 const { start, count, videoId, accountId, videoChannelId } = parameters
502
503 const accountExclusion = {
504 [Op.notIn]: Sequelize.literal(
505 '(' + buildBlockedAccountSQL([ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ]) + ')'
506 )
507 }
508 const accountWhere = accountId
509 ? {
d473fd94
RK
510 [Op.and]: {
511 ...accountExclusion,
512 [Op.eq]: accountId
00494d6e 513 }
d473fd94 514 }
00494d6e
RK
515 : accountExclusion
516
517 const videoChannelWhere = videoChannelId ? { id: videoChannelId } : undefined
1df8a4d7 518
fe3a55b0 519 const query = {
1735c825 520 order: [ [ 'createdAt', 'DESC' ] ] as Order,
9a4a9b6c
C
521 offset: start,
522 limit: count,
193272b8 523 where: {
1df8a4d7 524 deletedAt: null,
00494d6e 525 accountId: accountWhere
193272b8 526 },
fe3a55b0
C
527 include: [
528 {
4dae00e6 529 attributes: [ 'name', 'uuid' ],
fe3a55b0 530 model: VideoModel.unscoped(),
68b6fd21
C
531 required: true,
532 where: {
533 privacy: VideoPrivacy.PUBLIC
696d83fd
C
534 },
535 include: [
536 {
537 attributes: [ 'accountId' ],
538 model: VideoChannelModel.unscoped(),
00494d6e
RK
539 required: true,
540 where: videoChannelWhere
696d83fd
C
541 }
542 ]
fe3a55b0
C
543 }
544 ]
545 }
546
547 if (videoId) query.where['videoId'] = videoId
548
549 return VideoCommentModel
550 .scope([ ScopeNames.WITH_ACCOUNT ])
551 .findAll(query)
552 }
553
444c0a0e
C
554 static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) {
555 const accountWhere = filter.onVideosOfAccount
556 ? { id: filter.onVideosOfAccount.id }
557 : {}
558
559 const query = {
560 limit: 1000,
561 where: {
562 deletedAt: null,
563 accountId: ofAccount.id
564 },
565 include: [
566 {
567 model: VideoModel,
568 required: true,
569 include: [
570 {
571 model: VideoChannelModel,
572 required: true,
573 include: [
574 {
575 model: AccountModel,
576 required: true,
577 where: accountWhere
578 }
579 ]
580 }
581 ]
582 }
583 ]
584 }
585
586 return VideoCommentModel
587 .scope([ ScopeNames.WITH_ACCOUNT ])
588 .findAll(query)
589 }
590
09cababd
C
591 static async getStats () {
592 const totalLocalVideoComments = await VideoCommentModel.count({
593 include: [
594 {
595 model: AccountModel,
596 required: true,
597 include: [
598 {
599 model: ActorModel,
600 required: true,
601 where: {
602 serverId: null
603 }
604 }
605 ]
606 }
607 ]
608 })
609 const totalVideoComments = await VideoCommentModel.count()
610
611 return {
612 totalLocalVideoComments,
613 totalVideoComments
614 }
615 }
616
2ba92871
C
617 static cleanOldCommentsOf (videoId: number, beforeUpdatedAt: Date) {
618 const query = {
619 where: {
620 updatedAt: {
1735c825 621 [Op.lt]: beforeUpdatedAt
2ba92871 622 },
6b9c966f
C
623 videoId,
624 accountId: {
625 [Op.notIn]: buildLocalAccountIdsIn()
444c0a0e
C
626 },
627 // Do not delete Tombstones
628 deletedAt: null
6b9c966f 629 }
2ba92871
C
630 }
631
632 return VideoCommentModel.destroy(query)
633 }
634
cef534ed
C
635 getCommentStaticPath () {
636 return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId()
637 }
638
d7e70384
C
639 getThreadId (): number {
640 return this.originCommentId || this.id
641 }
642
4cb6d457 643 isOwned () {
69222afa
JM
644 if (!this.Account) {
645 return false
646 }
647
4cb6d457
C
648 return this.Account.isOwned()
649 }
650
69222afa 651 isDeleted () {
a1587156 652 return this.deletedAt !== null
69222afa
JM
653 }
654
f7cc67b4 655 extractMentions () {
1f6d57e3 656 let result: string[] = []
f7cc67b4
C
657
658 const localMention = `@(${actorNameAlphabet}+)`
6dd9de95 659 const remoteMention = `${localMention}@${WEBSERVER.HOST}`
f7cc67b4 660
1f6d57e3
C
661 const mentionRegex = this.isOwned()
662 ? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions?
663 : '(?:' + remoteMention + ')'
664
665 const firstMentionRegex = new RegExp(`^${mentionRegex} `, 'g')
666 const endMentionRegex = new RegExp(` ${mentionRegex}$`, 'g')
f7cc67b4 667 const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g')
f7cc67b4 668
1f6d57e3
C
669 result = result.concat(
670 regexpCapture(this.text, firstMentionRegex)
671 .map(([ , username1, username2 ]) => username1 || username2),
f7cc67b4 672
1f6d57e3
C
673 regexpCapture(this.text, endMentionRegex)
674 .map(([ , username1, username2 ]) => username1 || username2),
675
676 regexpCapture(this.text, remoteMentionsRegex)
677 .map(([ , username ]) => username)
678 )
f7cc67b4 679
1f6d57e3
C
680 // Include local mentions
681 if (this.isOwned()) {
682 const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
f7cc67b4 683
1f6d57e3
C
684 result = result.concat(
685 regexpCapture(this.text, localMentionsRegex)
686 .map(([ , username ]) => username)
f7cc67b4 687 )
1f6d57e3
C
688 }
689
690 return uniq(result)
f7cc67b4
C
691 }
692
1ca9f7c3 693 toFormattedJSON (this: MCommentFormattable) {
bf1f6508
C
694 return {
695 id: this.id,
696 url: this.url,
697 text: this.text,
698 threadId: this.originCommentId || this.id,
d50acfab 699 inReplyToCommentId: this.inReplyToCommentId || null,
bf1f6508
C
700 videoId: this.videoId,
701 createdAt: this.createdAt,
d3ea8975 702 updatedAt: this.updatedAt,
69222afa
JM
703 deletedAt: this.deletedAt,
704 isDeleted: this.isDeleted(),
5b0413dd 705 totalRepliesFromVideoAuthor: this.get('totalRepliesFromVideoAuthor') || 0,
4635f59d 706 totalReplies: this.get('totalReplies') || 0,
69222afa 707 account: this.Account ? this.Account.toFormattedJSON() : null
bf1f6508
C
708 } as VideoComment
709 }
ea44f375 710
69222afa 711 toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject | ActivityTombstoneObject {
b5206dfc
JM
712 let inReplyTo: string
713 // New thread, so in AS we reply to the video
714 if (this.inReplyToCommentId === null) {
715 inReplyTo = this.Video.url
716 } else {
717 inReplyTo = this.InReplyToVideoComment.url
718 }
719
69222afa
JM
720 if (this.isDeleted()) {
721 return {
722 id: this.url,
723 type: 'Tombstone',
724 formerType: 'Note',
b5206dfc 725 inReplyTo,
69222afa
JM
726 published: this.createdAt.toISOString(),
727 updated: this.updatedAt.toISOString(),
728 deleted: this.deletedAt.toISOString()
729 }
730 }
731
d7e70384
C
732 const tag: ActivityTagObject[] = []
733 for (const parentComment of threadParentComments) {
b5206dfc
JM
734 if (!parentComment.Account) continue
735
d7e70384
C
736 const actor = parentComment.Account.Actor
737
738 tag.push({
739 type: 'Mention',
740 href: actor.url,
741 name: `@${actor.preferredUsername}@${actor.getHost()}`
742 })
743 }
744
ea44f375
C
745 return {
746 type: 'Note' as 'Note',
747 id: this.url,
748 content: this.text,
749 inReplyTo,
da854ddd 750 updated: this.updatedAt.toISOString(),
ea44f375 751 published: this.createdAt.toISOString(),
da854ddd 752 url: this.url,
d7e70384
C
753 attributedTo: this.Account.Actor.url,
754 tag
ea44f375
C
755 }
756 }
696d83fd
C
757
758 private static async buildBlockerAccountIds (options: {
759 videoId: number
760 isVideoOwned: boolean
761 user?: MUserAccountId
762 }) {
763 const { videoId, user, isVideoOwned } = options
764
765 const serverActor = await getServerActor()
766 const blockerAccountIds = [ serverActor.Account.id ]
767
768 if (user) blockerAccountIds.push(user.Account.id)
769
770 if (isVideoOwned) {
771 const videoOwnerAccount = await AccountModel.loadAccountIdFromVideo(videoId)
772 blockerAccountIds.push(videoOwnerAccount.id)
773 }
774
775 return blockerAccountIds
776 }
6d852470 777}