aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-05-22 17:06:26 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-05-29 09:32:20 +0200
commit696d83fd1377486dd03cc1bd02a21d9b6ddd9fcd (patch)
treee1b88451c4357add80721f530993e2b48d197feb /server/models
parent72c33e716fecd1826dcf645957f8669821f91ff3 (diff)
downloadPeerTube-696d83fd1377486dd03cc1bd02a21d9b6ddd9fcd.tar.gz
PeerTube-696d83fd1377486dd03cc1bd02a21d9b6ddd9fcd.tar.zst
PeerTube-696d83fd1377486dd03cc1bd02a21d9b6ddd9fcd.zip
Block comments from muted accounts/servers
Add better control for users of comments displayed on their videos: * Do not forward comments from muted remote accounts/servers (muted by the current server or by the video owner) * Do not list threads and hide replies (with their children) of accounts/servers muted by the video owner * Hide from RSS comments of muted accounts/servers by video owners Use case: * Try to limit spam propagation in the federation * Add ability for users to automatically hide comments on their videos from undesirable accounts/servers (the comment section belongs to videomakers, so they choose what's posted there)
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account.ts26
-rw-r--r--server/models/utils.ts5
-rw-r--r--server/models/video/video-abuse.ts2
-rw-r--r--server/models/video/video-comment.ts76
4 files changed, 83 insertions, 26 deletions
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index a0081f259..ad649837a 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -32,9 +32,10 @@ import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequ
32import { AccountBlocklistModel } from './account-blocklist' 32import { AccountBlocklistModel } from './account-blocklist'
33import { ServerBlocklistModel } from '../server/server-blocklist' 33import { ServerBlocklistModel } from '../server/server-blocklist'
34import { ActorFollowModel } from '../activitypub/actor-follow' 34import { ActorFollowModel } from '../activitypub/actor-follow'
35import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable } from '../../typings/models' 35import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable, MAccount } from '../../typings/models'
36import * as Bluebird from 'bluebird' 36import * as Bluebird from 'bluebird'
37import { ModelCache } from '@server/models/model-cache' 37import { ModelCache } from '@server/models/model-cache'
38import { VideoModel } from '../video/video'
38 39
39export enum ScopeNames { 40export enum ScopeNames {
40 SUMMARY = 'SUMMARY' 41 SUMMARY = 'SUMMARY'
@@ -343,6 +344,29 @@ export class AccountModel extends Model<AccountModel> {
343 }) 344 })
344 } 345 }
345 346
347 static loadAccountIdFromVideo (videoId: number): Bluebird<MAccount> {
348 const query = {
349 include: [
350 {
351 attributes: [ 'id', 'accountId' ],
352 model: VideoChannelModel.unscoped(),
353 required: true,
354 include: [
355 {
356 attributes: [ 'id', 'channelId' ],
357 model: VideoModel.unscoped(),
358 where: {
359 id: videoId
360 }
361 }
362 ]
363 }
364 ]
365 }
366
367 return AccountModel.findOne(query)
368 }
369
346 static listLocalsForSitemap (sort: string): Bluebird<MAccountActor[]> { 370 static listLocalsForSitemap (sort: string): Bluebird<MAccountActor[]> {
347 const query = { 371 const query = {
348 attributes: [ ], 372 attributes: [ ],
diff --git a/server/models/utils.ts b/server/models/utils.ts
index b2573cd35..88c9b4adb 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -136,10 +136,7 @@ function createSimilarityAttribute (col: string, value: string) {
136 ) 136 )
137} 137}
138 138
139function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number) { 139function buildBlockedAccountSQL (blockerIds: number[]) {
140 const blockerIds = [ serverAccountId ]
141 if (userAccountId) blockerIds.push(userAccountId)
142
143 const blockerIdsString = blockerIds.join(', ') 140 const blockerIdsString = blockerIds.join(', ')
144 141
145 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + 142 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
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 dfeb1c4e7..ba09522cc 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -21,7 +21,8 @@ import {
21 MCommentOwnerReplyVideoLight, 21 MCommentOwnerReplyVideoLight,
22 MCommentOwnerVideo, 22 MCommentOwnerVideo,
23 MCommentOwnerVideoFeed, 23 MCommentOwnerVideoFeed,
24 MCommentOwnerVideoReply 24 MCommentOwnerVideoReply,
25 MVideoImmutable
25} from '../../typings/models/video' 26} from '../../typings/models/video'
26import { AccountModel } from '../account/account' 27import { AccountModel } from '../account/account'
27import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
@@ -38,14 +39,14 @@ enum ScopeNames {
38} 39}
39 40
40@Scopes(() => ({ 41@Scopes(() => ({
41 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { 42 [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => {
42 return { 43 return {
43 attributes: { 44 attributes: {
44 include: [ 45 include: [
45 [ 46 [
46 Sequelize.literal( 47 Sequelize.literal(
47 '(' + 48 '(' +
48 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' + 49 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' +
49 'SELECT COUNT("replies"."id") - (' + 50 'SELECT COUNT("replies"."id") - (' +
50 'SELECT COUNT("replies"."id") ' + 51 'SELECT COUNT("replies"."id") ' +
51 'FROM "videoComment" AS "replies" ' + 52 'FROM "videoComment" AS "replies" ' +
@@ -276,16 +277,15 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
276 277
277 static async listThreadsForApi (parameters: { 278 static async listThreadsForApi (parameters: {
278 videoId: number 279 videoId: number
280 isVideoOwned: boolean
279 start: number 281 start: number
280 count: number 282 count: number
281 sort: string 283 sort: string
282 user?: MUserAccountId 284 user?: MUserAccountId
283 }) { 285 }) {
284 const { videoId, start, count, sort, user } = parameters 286 const { videoId, isVideoOwned, start, count, sort, user } = parameters
285 287
286 const serverActor = await getServerActor() 288 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned })
287 const serverAccountId = serverActor.Account.id
288 const userAccountId = user ? user.Account.id : undefined
289 289
290 const query = { 290 const query = {
291 offset: start, 291 offset: start,
@@ -304,7 +304,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
304 { 304 {
305 accountId: { 305 accountId: {
306 [Op.notIn]: Sequelize.literal( 306 [Op.notIn]: Sequelize.literal(
307 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' 307 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
308 ) 308 )
309 } 309 }
310 }, 310 },
@@ -320,7 +320,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
320 const scopes: (string | ScopeOptions)[] = [ 320 const scopes: (string | ScopeOptions)[] = [
321 ScopeNames.WITH_ACCOUNT_FOR_API, 321 ScopeNames.WITH_ACCOUNT_FOR_API,
322 { 322 {
323 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] 323 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
324 } 324 }
325 ] 325 ]
326 326
@@ -334,14 +334,13 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
334 334
335 static async listThreadCommentsForApi (parameters: { 335 static async listThreadCommentsForApi (parameters: {
336 videoId: number 336 videoId: number
337 isVideoOwned: boolean
337 threadId: number 338 threadId: number
338 user?: MUserAccountId 339 user?: MUserAccountId
339 }) { 340 }) {
340 const { videoId, threadId, user } = parameters 341 const { videoId, threadId, user, isVideoOwned } = parameters
341 342
342 const serverActor = await getServerActor() 343 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned })
343 const serverAccountId = serverActor.Account.id
344 const userAccountId = user ? user.Account.id : undefined
345 344
346 const query = { 345 const query = {
347 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, 346 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order,
@@ -353,7 +352,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
353 ], 352 ],
354 accountId: { 353 accountId: {
355 [Op.notIn]: Sequelize.literal( 354 [Op.notIn]: Sequelize.literal(
356 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' 355 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
357 ) 356 )
358 } 357 }
359 } 358 }
@@ -362,7 +361,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
362 const scopes: any[] = [ 361 const scopes: any[] = [
363 ScopeNames.WITH_ACCOUNT_FOR_API, 362 ScopeNames.WITH_ACCOUNT_FOR_API,
364 { 363 {
365 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] 364 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
366 } 365 }
367 ] 366 ]
368 367
@@ -399,13 +398,23 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
399 .findAll(query) 398 .findAll(query)
400 } 399 }
401 400
402 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
403 const query = { 407 const query = {
404 order: [ [ 'createdAt', order ] ] as Order, 408 order: [ [ 'createdAt', 'ASC' ] ] as Order,
405 offset: start, 409 offset: start,
406 limit: count, 410 limit: count,
407 where: { 411 where: {
408 videoId 412 videoId: video.id,
413 accountId: {
414 [Op.notIn]: Sequelize.literal(
415 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
416 )
417 }
409 }, 418 },
410 transaction: t 419 transaction: t
411 } 420 }
@@ -424,7 +433,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
424 deletedAt: null, 433 deletedAt: null,
425 accountId: { 434 accountId: {
426 [Op.notIn]: Sequelize.literal( 435 [Op.notIn]: Sequelize.literal(
427 '(' + buildBlockedAccountSQL(serverActor.Account.id) + ')' 436 '(' + buildBlockedAccountSQL([ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ]) + ')'
428 ) 437 )
429 } 438 }
430 }, 439 },
@@ -435,7 +444,14 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
435 required: true, 444 required: true,
436 where: { 445 where: {
437 privacy: VideoPrivacy.PUBLIC 446 privacy: VideoPrivacy.PUBLIC
438 } 447 },
448 include: [
449 {
450 attributes: [ 'accountId' ],
451 model: VideoChannelModel.unscoped(),
452 required: true
453 }
454 ]
439 } 455 }
440 ] 456 ]
441 } 457 }
@@ -650,4 +666,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
650 tag 666 tag
651 } 667 }
652 } 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 }
653} 689}