diff options
author | Chocobozzz <me@florianbigard.com> | 2020-05-22 17:06:26 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-05-29 09:32:20 +0200 |
commit | 696d83fd1377486dd03cc1bd02a21d9b6ddd9fcd (patch) | |
tree | e1b88451c4357add80721f530993e2b48d197feb /server/models | |
parent | 72c33e716fecd1826dcf645957f8669821f91ff3 (diff) | |
download | PeerTube-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.ts | 26 | ||||
-rw-r--r-- | server/models/utils.ts | 5 | ||||
-rw-r--r-- | server/models/video/video-abuse.ts | 2 | ||||
-rw-r--r-- | server/models/video/video-comment.ts | 76 |
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 | |||
32 | import { AccountBlocklistModel } from './account-blocklist' | 32 | import { AccountBlocklistModel } from './account-blocklist' |
33 | import { ServerBlocklistModel } from '../server/server-blocklist' | 33 | import { ServerBlocklistModel } from '../server/server-blocklist' |
34 | import { ActorFollowModel } from '../activitypub/actor-follow' | 34 | import { ActorFollowModel } from '../activitypub/actor-follow' |
35 | import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable } from '../../typings/models' | 35 | import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable, MAccount } from '../../typings/models' |
36 | import * as Bluebird from 'bluebird' | 36 | import * as Bluebird from 'bluebird' |
37 | import { ModelCache } from '@server/models/model-cache' | 37 | import { ModelCache } from '@server/models/model-cache' |
38 | import { VideoModel } from '../video/video' | ||
38 | 39 | ||
39 | export enum ScopeNames { | 40 | export 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 | ||
139 | function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number) { | 139 | function 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' |
26 | import { AccountModel } from '../account/account' | 27 | import { AccountModel } from '../account/account' |
27 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 28 | import { 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 | } |