aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/video-abuse.ts2
-rw-r--r--server/models/video/video-comment.ts191
2 files changed, 150 insertions, 43 deletions
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 0844f702d..e0cf50b59 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -57,7 +57,7 @@ export enum ScopeNames {
57 }) => { 57 }) => {
58 const where = { 58 const where = {
59 reporterAccountId: { 59 reporterAccountId: {
60 [Op.notIn]: literal('(' + buildBlockedAccountSQL(options.serverAccountId, options.userAccountId) + ')') 60 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
61 } 61 }
62 } 62 }
63 63
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 @@
1import * as Bluebird from 'bluebird'
2import { uniq } from 'lodash'
3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
5import { getServerActor } from '@server/models/application/application'
6import { MAccount, MAccountId, MUserAccountId } from '@server/typings/models'
7import { VideoPrivacy } from '@shared/models'
2import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' 8import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
3import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 9import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
4import { VideoComment } from '../../../shared/models/videos/video-comment.model' 10import { VideoComment } from '../../../shared/models/videos/video-comment.model'
5import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
6import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
7import { AccountModel } from '../account/account'
8import { ActorModel } from '../activitypub/actor'
9import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
10import { VideoModel } from './video'
11import { VideoChannelModel } from './video-channel'
12import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' 11import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
12import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
13import { regexpCapture } from '../../helpers/regexp' 13import { regexpCapture } from '../../helpers/regexp'
14import { uniq } from 'lodash' 14import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
15import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
16import * as Bluebird from 'bluebird'
17import { 15import {
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'
28import { MUserAccountId } from '@server/typings/models' 27import { AccountModel } from '../account/account'
29import { VideoPrivacy } from '@shared/models' 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
30import { getServerActor } from '@server/models/application/application' 29import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
30import { VideoModel } from './video'
31import { VideoChannelModel } from './video-channel'
31 32
32enum ScopeNames { 33enum 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}