diff options
Diffstat (limited to 'server/models/video/video-comment.ts')
-rw-r--r-- | server/models/video/video-comment.ts | 191 |
1 files changed, 149 insertions, 42 deletions
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 6d60271e6..ba09522cc 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,19 +1,17 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { uniq } from 'lodash' | ||
3 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { MAccount, MAccountId, MUserAccountId } from '@server/typings/models' | ||
7 | import { VideoPrivacy } from '@shared/models' | ||
2 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 8 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
3 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 9 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
4 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' | 10 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' |
5 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
6 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | ||
7 | import { AccountModel } from '../account/account' | ||
8 | import { ActorModel } from '../activitypub/actor' | ||
9 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' | ||
10 | import { VideoModel } from './video' | ||
11 | import { VideoChannelModel } from './video-channel' | ||
12 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' | 11 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' |
12 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
13 | import { regexpCapture } from '../../helpers/regexp' | 13 | import { regexpCapture } from '../../helpers/regexp' |
14 | import { uniq } from 'lodash' | 14 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
15 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' | ||
16 | import * as Bluebird from 'bluebird' | ||
17 | import { | 15 | import { |
18 | MComment, | 16 | MComment, |
19 | MCommentAP, | 17 | MCommentAP, |
@@ -23,28 +21,32 @@ import { | |||
23 | MCommentOwnerReplyVideoLight, | 21 | MCommentOwnerReplyVideoLight, |
24 | MCommentOwnerVideo, | 22 | MCommentOwnerVideo, |
25 | MCommentOwnerVideoFeed, | 23 | MCommentOwnerVideoFeed, |
26 | MCommentOwnerVideoReply | 24 | MCommentOwnerVideoReply, |
25 | MVideoImmutable | ||
27 | } from '../../typings/models/video' | 26 | } from '../../typings/models/video' |
28 | import { MUserAccountId } from '@server/typings/models' | 27 | import { AccountModel } from '../account/account' |
29 | import { VideoPrivacy } from '@shared/models' | 28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
30 | import { getServerActor } from '@server/models/application/application' | 29 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' |
30 | import { VideoModel } from './video' | ||
31 | import { VideoChannelModel } from './video-channel' | ||
31 | 32 | ||
32 | enum ScopeNames { | 33 | enum ScopeNames { |
33 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 34 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
35 | WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API', | ||
34 | WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', | 36 | WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', |
35 | WITH_VIDEO = 'WITH_VIDEO', | 37 | WITH_VIDEO = 'WITH_VIDEO', |
36 | ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' | 38 | ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' |
37 | } | 39 | } |
38 | 40 | ||
39 | @Scopes(() => ({ | 41 | @Scopes(() => ({ |
40 | [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { | 42 | [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => { |
41 | return { | 43 | return { |
42 | attributes: { | 44 | attributes: { |
43 | include: [ | 45 | include: [ |
44 | [ | 46 | [ |
45 | Sequelize.literal( | 47 | Sequelize.literal( |
46 | '(' + | 48 | '(' + |
47 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' + | 49 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' + |
48 | 'SELECT COUNT("replies"."id") - (' + | 50 | 'SELECT COUNT("replies"."id") - (' + |
49 | 'SELECT COUNT("replies"."id") ' + | 51 | 'SELECT COUNT("replies"."id") ' + |
50 | 'FROM "videoComment" AS "replies" ' + | 52 | 'FROM "videoComment" AS "replies" ' + |
@@ -82,6 +84,22 @@ enum ScopeNames { | |||
82 | } | 84 | } |
83 | ] | 85 | ] |
84 | }, | 86 | }, |
87 | [ScopeNames.WITH_ACCOUNT_FOR_API]: { | ||
88 | include: [ | ||
89 | { | ||
90 | model: AccountModel.unscoped(), | ||
91 | include: [ | ||
92 | { | ||
93 | attributes: { | ||
94 | exclude: unusedActorAttributesForAPI | ||
95 | }, | ||
96 | model: ActorModel, // Default scope includes avatar and server | ||
97 | required: true | ||
98 | } | ||
99 | ] | ||
100 | } | ||
101 | ] | ||
102 | }, | ||
85 | [ScopeNames.WITH_IN_REPLY_TO]: { | 103 | [ScopeNames.WITH_IN_REPLY_TO]: { |
86 | include: [ | 104 | include: [ |
87 | { | 105 | { |
@@ -259,36 +277,50 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
259 | 277 | ||
260 | static async listThreadsForApi (parameters: { | 278 | static async listThreadsForApi (parameters: { |
261 | videoId: number | 279 | videoId: number |
280 | isVideoOwned: boolean | ||
262 | start: number | 281 | start: number |
263 | count: number | 282 | count: number |
264 | sort: string | 283 | sort: string |
265 | user?: MUserAccountId | 284 | user?: MUserAccountId |
266 | }) { | 285 | }) { |
267 | const { videoId, start, count, sort, user } = parameters | 286 | const { videoId, isVideoOwned, start, count, sort, user } = parameters |
268 | 287 | ||
269 | const serverActor = await getServerActor() | 288 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) |
270 | const serverAccountId = serverActor.Account.id | ||
271 | const userAccountId = user ? user.Account.id : undefined | ||
272 | 289 | ||
273 | const query = { | 290 | const query = { |
274 | offset: start, | 291 | offset: start, |
275 | limit: count, | 292 | limit: count, |
276 | order: getCommentSort(sort), | 293 | order: getCommentSort(sort), |
277 | where: { | 294 | where: { |
278 | videoId, | 295 | [Op.and]: [ |
279 | inReplyToCommentId: null, | 296 | { |
280 | accountId: { | 297 | videoId |
281 | [Op.notIn]: Sequelize.literal( | 298 | }, |
282 | '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' | 299 | { |
283 | ) | 300 | inReplyToCommentId: null |
284 | } | 301 | }, |
302 | { | ||
303 | [Op.or]: [ | ||
304 | { | ||
305 | accountId: { | ||
306 | [Op.notIn]: Sequelize.literal( | ||
307 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | ||
308 | ) | ||
309 | } | ||
310 | }, | ||
311 | { | ||
312 | accountId: null | ||
313 | } | ||
314 | ] | ||
315 | } | ||
316 | ] | ||
285 | } | 317 | } |
286 | } | 318 | } |
287 | 319 | ||
288 | const scopes: (string | ScopeOptions)[] = [ | 320 | const scopes: (string | ScopeOptions)[] = [ |
289 | ScopeNames.WITH_ACCOUNT, | 321 | ScopeNames.WITH_ACCOUNT_FOR_API, |
290 | { | 322 | { |
291 | method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] | 323 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] |
292 | } | 324 | } |
293 | ] | 325 | ] |
294 | 326 | ||
@@ -302,14 +334,13 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
302 | 334 | ||
303 | static async listThreadCommentsForApi (parameters: { | 335 | static async listThreadCommentsForApi (parameters: { |
304 | videoId: number | 336 | videoId: number |
337 | isVideoOwned: boolean | ||
305 | threadId: number | 338 | threadId: number |
306 | user?: MUserAccountId | 339 | user?: MUserAccountId |
307 | }) { | 340 | }) { |
308 | const { videoId, threadId, user } = parameters | 341 | const { videoId, threadId, user, isVideoOwned } = parameters |
309 | 342 | ||
310 | const serverActor = await getServerActor() | 343 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) |
311 | const serverAccountId = serverActor.Account.id | ||
312 | const userAccountId = user ? user.Account.id : undefined | ||
313 | 344 | ||
314 | const query = { | 345 | const query = { |
315 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, | 346 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, |
@@ -321,16 +352,16 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
321 | ], | 352 | ], |
322 | accountId: { | 353 | accountId: { |
323 | [Op.notIn]: Sequelize.literal( | 354 | [Op.notIn]: Sequelize.literal( |
324 | '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' | 355 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' |
325 | ) | 356 | ) |
326 | } | 357 | } |
327 | } | 358 | } |
328 | } | 359 | } |
329 | 360 | ||
330 | const scopes: any[] = [ | 361 | const scopes: any[] = [ |
331 | ScopeNames.WITH_ACCOUNT, | 362 | ScopeNames.WITH_ACCOUNT_FOR_API, |
332 | { | 363 | { |
333 | method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] | 364 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] |
334 | } | 365 | } |
335 | ] | 366 | ] |
336 | 367 | ||
@@ -367,13 +398,23 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
367 | .findAll(query) | 398 | .findAll(query) |
368 | } | 399 | } |
369 | 400 | ||
370 | static listAndCountByVideoId (videoId: number, start: number, count: number, t?: Transaction, order: 'ASC' | 'DESC' = 'ASC') { | 401 | static async listAndCountByVideoForAP (video: MVideoImmutable, start: number, count: number, t?: Transaction) { |
402 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ | ||
403 | videoId: video.id, | ||
404 | isVideoOwned: video.isOwned() | ||
405 | }) | ||
406 | |||
371 | const query = { | 407 | const query = { |
372 | order: [ [ 'createdAt', order ] ] as Order, | 408 | order: [ [ 'createdAt', 'ASC' ] ] as Order, |
373 | offset: start, | 409 | offset: start, |
374 | limit: count, | 410 | limit: count, |
375 | where: { | 411 | where: { |
376 | videoId | 412 | videoId: video.id, |
413 | accountId: { | ||
414 | [Op.notIn]: Sequelize.literal( | ||
415 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | ||
416 | ) | ||
417 | } | ||
377 | }, | 418 | }, |
378 | transaction: t | 419 | transaction: t |
379 | } | 420 | } |
@@ -392,7 +433,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
392 | deletedAt: null, | 433 | deletedAt: null, |
393 | accountId: { | 434 | accountId: { |
394 | [Op.notIn]: Sequelize.literal( | 435 | [Op.notIn]: Sequelize.literal( |
395 | '(' + buildBlockedAccountSQL(serverActor.Account.id) + ')' | 436 | '(' + buildBlockedAccountSQL([ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ]) + ')' |
396 | ) | 437 | ) |
397 | } | 438 | } |
398 | }, | 439 | }, |
@@ -403,7 +444,14 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
403 | required: true, | 444 | required: true, |
404 | where: { | 445 | where: { |
405 | privacy: VideoPrivacy.PUBLIC | 446 | privacy: VideoPrivacy.PUBLIC |
406 | } | 447 | }, |
448 | include: [ | ||
449 | { | ||
450 | attributes: [ 'accountId' ], | ||
451 | model: VideoChannelModel.unscoped(), | ||
452 | required: true | ||
453 | } | ||
454 | ] | ||
407 | } | 455 | } |
408 | ] | 456 | ] |
409 | } | 457 | } |
@@ -415,6 +463,43 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
415 | .findAll(query) | 463 | .findAll(query) |
416 | } | 464 | } |
417 | 465 | ||
466 | static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { | ||
467 | const accountWhere = filter.onVideosOfAccount | ||
468 | ? { id: filter.onVideosOfAccount.id } | ||
469 | : {} | ||
470 | |||
471 | const query = { | ||
472 | limit: 1000, | ||
473 | where: { | ||
474 | deletedAt: null, | ||
475 | accountId: ofAccount.id | ||
476 | }, | ||
477 | include: [ | ||
478 | { | ||
479 | model: VideoModel, | ||
480 | required: true, | ||
481 | include: [ | ||
482 | { | ||
483 | model: VideoChannelModel, | ||
484 | required: true, | ||
485 | include: [ | ||
486 | { | ||
487 | model: AccountModel, | ||
488 | required: true, | ||
489 | where: accountWhere | ||
490 | } | ||
491 | ] | ||
492 | } | ||
493 | ] | ||
494 | } | ||
495 | ] | ||
496 | } | ||
497 | |||
498 | return VideoCommentModel | ||
499 | .scope([ ScopeNames.WITH_ACCOUNT ]) | ||
500 | .findAll(query) | ||
501 | } | ||
502 | |||
418 | static async getStats () { | 503 | static async getStats () { |
419 | const totalLocalVideoComments = await VideoCommentModel.count({ | 504 | const totalLocalVideoComments = await VideoCommentModel.count({ |
420 | include: [ | 505 | include: [ |
@@ -450,7 +535,9 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
450 | videoId, | 535 | videoId, |
451 | accountId: { | 536 | accountId: { |
452 | [Op.notIn]: buildLocalAccountIdsIn() | 537 | [Op.notIn]: buildLocalAccountIdsIn() |
453 | } | 538 | }, |
539 | // Do not delete Tombstones | ||
540 | deletedAt: null | ||
454 | } | 541 | } |
455 | } | 542 | } |
456 | 543 | ||
@@ -579,4 +666,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
579 | tag | 666 | tag |
580 | } | 667 | } |
581 | } | 668 | } |
669 | |||
670 | private static async buildBlockerAccountIds (options: { | ||
671 | videoId: number | ||
672 | isVideoOwned: boolean | ||
673 | user?: MUserAccountId | ||
674 | }) { | ||
675 | const { videoId, user, isVideoOwned } = options | ||
676 | |||
677 | const serverActor = await getServerActor() | ||
678 | const blockerAccountIds = [ serverActor.Account.id ] | ||
679 | |||
680 | if (user) blockerAccountIds.push(user.Account.id) | ||
681 | |||
682 | if (isVideoOwned) { | ||
683 | const videoOwnerAccount = await AccountModel.loadAccountIdFromVideo(videoId) | ||
684 | blockerAccountIds.push(videoOwnerAccount.id) | ||
685 | } | ||
686 | |||
687 | return blockerAccountIds | ||
688 | } | ||
582 | } | 689 | } |