aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video-comment.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video/video-comment.ts')
-rw-r--r--server/models/video/video-comment.ts446
1 files changed, 95 insertions, 351 deletions
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index af9614d30..fb9d15e55 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,4 +1,4 @@
1import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 1import { FindOptions, Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize'
2import { 2import {
3 AllowNull, 3 AllowNull,
4 BelongsTo, 4 BelongsTo,
@@ -13,11 +13,9 @@ import {
13 Table, 13 Table,
14 UpdatedAt 14 UpdatedAt
15} from 'sequelize-typescript' 15} from 'sequelize-typescript'
16import { exists } from '@server/helpers/custom-validators/misc'
17import { getServerActor } from '@server/models/application/application' 16import { getServerActor } from '@server/models/application/application'
18import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' 17import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
19import { uniqify } from '@shared/core-utils' 18import { pick, uniqify } from '@shared/core-utils'
20import { VideoPrivacy } from '@shared/models'
21import { AttributesOnly } from '@shared/typescript-utils' 19import { AttributesOnly } from '@shared/typescript-utils'
22import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' 20import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
23import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 21import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
@@ -41,61 +39,19 @@ import {
41} from '../../types/models/video' 39} from '../../types/models/video'
42import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' 40import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
43import { AccountModel } from '../account/account' 41import { AccountModel } from '../account/account'
44import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' 42import { ActorModel } from '../actor/actor'
45import { 43import { buildLocalAccountIdsIn, throwIfNotValid } from '../utils'
46 buildBlockedAccountSQL, 44import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder'
47 buildBlockedAccountSQLOptimized,
48 buildLocalAccountIdsIn,
49 getCommentSort,
50 searchAttribute,
51 throwIfNotValid
52} from '../utils'
53import { VideoModel } from './video' 45import { VideoModel } from './video'
54import { VideoChannelModel } from './video-channel' 46import { VideoChannelModel } from './video-channel'
55 47
56export enum ScopeNames { 48export enum ScopeNames {
57 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
58 WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API',
59 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', 50 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
60 WITH_VIDEO = 'WITH_VIDEO', 51 WITH_VIDEO = 'WITH_VIDEO'
61 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
62} 52}
63 53
64@Scopes(() => ({ 54@Scopes(() => ({
65 [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => {
66 return {
67 attributes: {
68 include: [
69 [
70 Sequelize.literal(
71 '(' +
72 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' +
73 'SELECT COUNT("replies"."id") ' +
74 'FROM "videoComment" AS "replies" ' +
75 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
76 'AND "deletedAt" IS NULL ' +
77 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' +
78 ')'
79 ),
80 'totalReplies'
81 ],
82 [
83 Sequelize.literal(
84 '(' +
85 'SELECT COUNT("replies"."id") ' +
86 'FROM "videoComment" AS "replies" ' +
87 'INNER JOIN "video" ON "video"."id" = "replies"."videoId" ' +
88 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
89 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
90 'AND "replies"."accountId" = "videoChannel"."accountId"' +
91 ')'
92 ),
93 'totalRepliesFromVideoAuthor'
94 ]
95 ]
96 }
97 } as FindOptions
98 },
99 [ScopeNames.WITH_ACCOUNT]: { 55 [ScopeNames.WITH_ACCOUNT]: {
100 include: [ 56 include: [
101 { 57 {
@@ -103,22 +59,6 @@ export enum ScopeNames {
103 } 59 }
104 ] 60 ]
105 }, 61 },
106 [ScopeNames.WITH_ACCOUNT_FOR_API]: {
107 include: [
108 {
109 model: AccountModel.unscoped(),
110 include: [
111 {
112 attributes: {
113 exclude: unusedActorAttributesForAPI
114 },
115 model: ActorModel, // Default scope includes avatar and server
116 required: true
117 }
118 ]
119 }
120 ]
121 },
122 [ScopeNames.WITH_IN_REPLY_TO]: { 62 [ScopeNames.WITH_IN_REPLY_TO]: {
123 include: [ 63 include: [
124 { 64 {
@@ -319,93 +259,19 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
319 searchAccount?: string 259 searchAccount?: string
320 searchVideo?: string 260 searchVideo?: string
321 }) { 261 }) {
322 const { start, count, sort, isLocal, search, searchAccount, searchVideo, onLocalVideo } = parameters 262 const queryOptions: ListVideoCommentsOptions = {
323 263 ...pick(parameters, [ 'start', 'count', 'sort', 'isLocal', 'search', 'searchVideo', 'searchAccount', 'onLocalVideo' ]),
324 const where: WhereOptions = {
325 deletedAt: null
326 }
327 264
328 const whereAccount: WhereOptions = {} 265 selectType: 'api',
329 const whereActor: WhereOptions = {} 266 notDeleted: true
330 const whereVideo: WhereOptions = {}
331
332 if (isLocal === true) {
333 Object.assign(whereActor, {
334 serverId: null
335 })
336 } else if (isLocal === false) {
337 Object.assign(whereActor, {
338 serverId: {
339 [Op.ne]: null
340 }
341 })
342 }
343
344 if (search) {
345 Object.assign(where, {
346 [Op.or]: [
347 searchAttribute(search, 'text'),
348 searchAttribute(search, '$Account.Actor.preferredUsername$'),
349 searchAttribute(search, '$Account.name$'),
350 searchAttribute(search, '$Video.name$')
351 ]
352 })
353 }
354
355 if (searchAccount) {
356 Object.assign(whereActor, {
357 [Op.or]: [
358 searchAttribute(searchAccount, '$Account.Actor.preferredUsername$'),
359 searchAttribute(searchAccount, '$Account.name$')
360 ]
361 })
362 }
363
364 if (searchVideo) {
365 Object.assign(whereVideo, searchAttribute(searchVideo, 'name'))
366 }
367
368 if (exists(onLocalVideo)) {
369 Object.assign(whereVideo, { remote: !onLocalVideo })
370 }
371
372 const getQuery = (forCount: boolean) => {
373 return {
374 offset: start,
375 limit: count,
376 order: getCommentSort(sort),
377 where,
378 include: [
379 {
380 model: AccountModel.unscoped(),
381 required: true,
382 where: whereAccount,
383 include: [
384 {
385 attributes: {
386 exclude: unusedActorAttributesForAPI
387 },
388 model: forCount === true
389 ? ActorModel.unscoped() // Default scope includes avatar and server
390 : ActorModel,
391 required: true,
392 where: whereActor
393 }
394 ]
395 },
396 {
397 model: VideoModel.unscoped(),
398 required: true,
399 where: whereVideo
400 }
401 ]
402 }
403 } 267 }
404 268
405 return Promise.all([ 269 return Promise.all([
406 VideoCommentModel.count(getQuery(true)), 270 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(),
407 VideoCommentModel.findAll(getQuery(false)) 271 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
408 ]).then(([ total, data ]) => ({ total, data })) 272 ]).then(([ rows, count ]) => {
273 return { total: count, data: rows }
274 })
409 } 275 }
410 276
411 static async listThreadsForApi (parameters: { 277 static async listThreadsForApi (parameters: {
@@ -416,67 +282,40 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
416 sort: string 282 sort: string
417 user?: MUserAccountId 283 user?: MUserAccountId
418 }) { 284 }) {
419 const { videoId, isVideoOwned, start, count, sort, user } = parameters 285 const { videoId, user } = parameters
420 286
421 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) 287 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user })
422 288
423 const accountBlockedWhere = { 289 const commonOptions: ListVideoCommentsOptions = {
424 accountId: { 290 selectType: 'api',
425 [Op.notIn]: Sequelize.literal( 291 videoId,
426 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' 292 blockerAccountIds
427 )
428 }
429 } 293 }
430 294
431 const queryList = { 295 const listOptions: ListVideoCommentsOptions = {
432 offset: start, 296 ...commonOptions,
433 limit: count, 297 ...pick(parameters, [ 'sort', 'start', 'count' ]),
434 order: getCommentSort(sort), 298
435 where: { 299 isThread: true,
436 [Op.and]: [ 300 includeReplyCounters: true
437 {
438 videoId
439 },
440 {
441 inReplyToCommentId: null
442 },
443 {
444 [Op.or]: [
445 accountBlockedWhere,
446 {
447 accountId: null
448 }
449 ]
450 }
451 ]
452 }
453 } 301 }
454 302
455 const findScopesList: (string | ScopeOptions)[] = [ 303 const countOptions: ListVideoCommentsOptions = {
456 ScopeNames.WITH_ACCOUNT_FOR_API, 304 ...commonOptions,
457 {
458 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
459 }
460 ]
461 305
462 const countScopesList: ScopeOptions[] = [ 306 isThread: true
463 { 307 }
464 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
465 }
466 ]
467 308
468 const notDeletedQueryCount = { 309 const notDeletedCountOptions: ListVideoCommentsOptions = {
469 where: { 310 ...commonOptions,
470 videoId, 311
471 deletedAt: null, 312 notDeleted: true
472 ...accountBlockedWhere
473 }
474 } 313 }
475 314
476 return Promise.all([ 315 return Promise.all([
477 VideoCommentModel.scope(findScopesList).findAll(queryList), 316 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, listOptions).listComments<MCommentAdminFormattable>(),
478 VideoCommentModel.scope(countScopesList).count(queryList), 317 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, countOptions).countComments(),
479 VideoCommentModel.count(notDeletedQueryCount) 318 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, notDeletedCountOptions).countComments()
480 ]).then(([ rows, count, totalNotDeletedComments ]) => { 319 ]).then(([ rows, count, totalNotDeletedComments ]) => {
481 return { total: count, data: rows, totalNotDeletedComments } 320 return { total: count, data: rows, totalNotDeletedComments }
482 }) 321 })
@@ -484,54 +323,29 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
484 323
485 static async listThreadCommentsForApi (parameters: { 324 static async listThreadCommentsForApi (parameters: {
486 videoId: number 325 videoId: number
487 isVideoOwned: boolean
488 threadId: number 326 threadId: number
489 user?: MUserAccountId 327 user?: MUserAccountId
490 }) { 328 }) {
491 const { videoId, threadId, user, isVideoOwned } = parameters 329 const { user } = parameters
492 330
493 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) 331 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user })
494 332
495 const query = { 333 const queryOptions: ListVideoCommentsOptions = {
496 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, 334 ...pick(parameters, [ 'videoId', 'threadId' ]),
497 where: {
498 videoId,
499 [Op.and]: [
500 {
501 [Op.or]: [
502 { id: threadId },
503 { originCommentId: threadId }
504 ]
505 },
506 {
507 [Op.or]: [
508 {
509 accountId: {
510 [Op.notIn]: Sequelize.literal(
511 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
512 )
513 }
514 },
515 {
516 accountId: null
517 }
518 ]
519 }
520 ]
521 }
522 }
523 335
524 const scopes: any[] = [ 336 selectType: 'api',
525 ScopeNames.WITH_ACCOUNT_FOR_API, 337 sort: 'createdAt',
526 { 338
527 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] 339 blockerAccountIds,
528 } 340 includeReplyCounters: true
529 ] 341 }
530 342
531 return Promise.all([ 343 return Promise.all([
532 VideoCommentModel.count(query), 344 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(),
533 VideoCommentModel.scope(scopes).findAll(query) 345 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
534 ]).then(([ total, data ]) => ({ total, data })) 346 ]).then(([ rows, count ]) => {
347 return { total: count, data: rows }
348 })
535 } 349 }
536 350
537 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { 351 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> {
@@ -559,31 +373,31 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
559 .findAll(query) 373 .findAll(query)
560 } 374 }
561 375
562 static async listAndCountByVideoForAP (video: MVideoImmutable, start: number, count: number, t?: Transaction) { 376 static async listAndCountByVideoForAP (parameters: {
563 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ 377 video: MVideoImmutable
378 start: number
379 count: number
380 }) {
381 const { video } = parameters
382
383 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
384
385 const queryOptions: ListVideoCommentsOptions = {
386 ...pick(parameters, [ 'start', 'count' ]),
387
388 selectType: 'comment-only',
564 videoId: video.id, 389 videoId: video.id,
565 isVideoOwned: video.isOwned() 390 sort: 'createdAt',
566 })
567 391
568 const query = { 392 blockerAccountIds
569 order: [ [ 'createdAt', 'ASC' ] ] as Order,
570 offset: start,
571 limit: count,
572 where: {
573 videoId: video.id,
574 accountId: {
575 [Op.notIn]: Sequelize.literal(
576 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
577 )
578 }
579 },
580 transaction: t
581 } 393 }
582 394
583 return Promise.all([ 395 return Promise.all([
584 VideoCommentModel.count(query), 396 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>(),
585 VideoCommentModel.findAll<MComment>(query) 397 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
586 ]).then(([ total, data ]) => ({ total, data })) 398 ]).then(([ rows, count ]) => {
399 return { total: count, data: rows }
400 })
587 } 401 }
588 402
589 static async listForFeed (parameters: { 403 static async listForFeed (parameters: {
@@ -592,97 +406,36 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
592 videoId?: number 406 videoId?: number
593 accountId?: number 407 accountId?: number
594 videoChannelId?: number 408 videoChannelId?: number
595 }): Promise<MCommentOwnerVideoFeed[]> { 409 }) {
596 const serverActor = await getServerActor() 410 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
597 const { start, count, videoId, accountId, videoChannelId } = parameters
598 411
599 const whereAnd: WhereOptions[] = buildBlockedAccountSQLOptimized( 412 const queryOptions: ListVideoCommentsOptions = {
600 '"VideoCommentModel"."accountId"', 413 ...pick(parameters, [ 'start', 'count', 'accountId', 'videoId', 'videoChannelId' ]),
601 [ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ]
602 )
603 414
604 if (accountId) { 415 selectType: 'feed',
605 whereAnd.push({
606 accountId
607 })
608 }
609 416
610 const accountWhere = { 417 sort: '-createdAt',
611 [Op.and]: whereAnd 418 onPublicVideo: true,
612 } 419 notDeleted: true,
613
614 const videoChannelWhere = videoChannelId ? { id: videoChannelId } : undefined
615 420
616 const query = { 421 blockerAccountIds
617 order: [ [ 'createdAt', 'DESC' ] ] as Order,
618 offset: start,
619 limit: count,
620 where: {
621 deletedAt: null,
622 accountId: accountWhere
623 },
624 include: [
625 {
626 attributes: [ 'name', 'uuid' ],
627 model: VideoModel.unscoped(),
628 required: true,
629 where: {
630 privacy: VideoPrivacy.PUBLIC
631 },
632 include: [
633 {
634 attributes: [ 'accountId' ],
635 model: VideoChannelModel.unscoped(),
636 required: true,
637 where: videoChannelWhere
638 }
639 ]
640 }
641 ]
642 } 422 }
643 423
644 if (videoId) query.where['videoId'] = videoId 424 return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentOwnerVideoFeed>()
645
646 return VideoCommentModel
647 .scope([ ScopeNames.WITH_ACCOUNT ])
648 .findAll(query)
649 } 425 }
650 426
651 static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { 427 static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) {
652 const accountWhere = filter.onVideosOfAccount 428 const queryOptions: ListVideoCommentsOptions = {
653 ? { id: filter.onVideosOfAccount.id } 429 selectType: 'comment-only',
654 : {}
655 430
656 const query = { 431 accountId: ofAccount.id,
657 limit: 1000, 432 videoAccountOwnerId: filter.onVideosOfAccount?.id,
658 where: { 433
659 deletedAt: null, 434 notDeleted: true,
660 accountId: ofAccount.id 435 count: 5000
661 },
662 include: [
663 {
664 model: VideoModel,
665 required: true,
666 include: [
667 {
668 model: VideoChannelModel,
669 required: true,
670 include: [
671 {
672 model: AccountModel,
673 required: true,
674 where: accountWhere
675 }
676 ]
677 }
678 ]
679 }
680 ]
681 } 436 }
682 437
683 return VideoCommentModel 438 return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>()
684 .scope([ ScopeNames.WITH_ACCOUNT ])
685 .findAll(query)
686 } 439 }
687 440
688 static async getStats () { 441 static async getStats () {
@@ -750,9 +503,7 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
750 } 503 }
751 504
752 isOwned () { 505 isOwned () {
753 if (!this.Account) { 506 if (!this.Account) return false
754 return false
755 }
756 507
757 return this.Account.isOwned() 508 return this.Account.isOwned()
758 } 509 }
@@ -906,22 +657,15 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
906 } 657 }
907 658
908 private static async buildBlockerAccountIds (options: { 659 private static async buildBlockerAccountIds (options: {
909 videoId: number 660 user: MUserAccountId
910 isVideoOwned: boolean 661 }): Promise<number[]> {
911 user?: MUserAccountId 662 const { user } = options
912 }) {
913 const { videoId, user, isVideoOwned } = options
914 663
915 const serverActor = await getServerActor() 664 const serverActor = await getServerActor()
916 const blockerAccountIds = [ serverActor.Account.id ] 665 const blockerAccountIds = [ serverActor.Account.id ]
917 666
918 if (user) blockerAccountIds.push(user.Account.id) 667 if (user) blockerAccountIds.push(user.Account.id)
919 668
920 if (isVideoOwned) {
921 const videoOwnerAccount = await AccountModel.loadAccountIdFromVideo(videoId)
922 if (videoOwnerAccount) blockerAccountIds.push(videoOwnerAccount.id)
923 }
924
925 return blockerAccountIds 669 return blockerAccountIds
926 } 670 }
927} 671}