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 | |
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)
-rw-r--r-- | server/controllers/activitypub/client.ts | 2 | ||||
-rw-r--r-- | server/controllers/api/videos/comment.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-create.ts | 23 | ||||
-rw-r--r-- | server/lib/blocklist.ts | 25 | ||||
-rw-r--r-- | server/lib/notifier.ts | 55 | ||||
-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 | ||||
-rw-r--r-- | server/tests/api/users/blocklist.ts | 170 | ||||
-rw-r--r-- | server/tests/api/videos/multiple-servers.ts | 9 | ||||
-rw-r--r-- | server/tests/feeds/feeds.ts | 41 | ||||
-rw-r--r-- | shared/extra-utils/videos/video-comments.ts | 7 | ||||
-rw-r--r-- | shared/extra-utils/videos/videos.ts | 9 |
14 files changed, 350 insertions, 102 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index f94abf808..e48641836 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -285,7 +285,7 @@ async function videoCommentsController (req: express.Request, res: express.Respo | |||
285 | const video = res.locals.onlyImmutableVideo | 285 | const video = res.locals.onlyImmutableVideo |
286 | 286 | ||
287 | const handler = async (start: number, count: number) => { | 287 | const handler = async (start: number, count: number) => { |
288 | const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count) | 288 | const result = await VideoCommentModel.listAndCountByVideoForAP(video, start, count) |
289 | return { | 289 | return { |
290 | total: result.count, | 290 | total: result.count, |
291 | data: result.rows.map(r => r.url) | 291 | data: result.rows.map(r => r.url) |
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index 2dcb85ecf..45ff969d9 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -78,6 +78,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) { | |||
78 | if (video.commentsEnabled === true) { | 78 | if (video.commentsEnabled === true) { |
79 | const apiOptions = await Hooks.wrapObject({ | 79 | const apiOptions = await Hooks.wrapObject({ |
80 | videoId: video.id, | 80 | videoId: video.id, |
81 | isVideoOwned: video.isOwned(), | ||
81 | start: req.query.start, | 82 | start: req.query.start, |
82 | count: req.query.count, | 83 | count: req.query.count, |
83 | sort: req.query.sort, | 84 | sort: req.query.sort, |
@@ -108,6 +109,7 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo | |||
108 | if (video.commentsEnabled === true) { | 109 | if (video.commentsEnabled === true) { |
109 | const apiOptions = await Hooks.wrapObject({ | 110 | const apiOptions = await Hooks.wrapObject({ |
110 | videoId: video.id, | 111 | videoId: video.id, |
112 | isVideoOwned: video.isOwned(), | ||
111 | threadId: res.locals.videoCommentThread.id, | 113 | threadId: res.locals.videoCommentThread.id, |
112 | user | 114 | user |
113 | }, 'filter:api.video-thread-comments.list.params') | 115 | }, 'filter:api.video-thread-comments.list.params') |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 566bf6992..f8f9b80c6 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,18 +1,19 @@ | |||
1 | import { isRedundancyAccepted } from '@server/lib/redundancy' | ||
1 | import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared' | 2 | import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared' |
3 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
2 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' | 4 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
5 | import { sequelizeTypescript } from '../../../initializers/database' | 7 | import { sequelizeTypescript } from '../../../initializers/database' |
6 | import { resolveThread } from '../video-comments' | ||
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
8 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
9 | import { createOrUpdateCacheFile } from '../cache-file' | ||
10 | import { Notifier } from '../../notifier' | ||
11 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
12 | import { createOrUpdateVideoPlaylist } from '../playlist' | ||
13 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' | 8 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' |
14 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models' | 9 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models' |
15 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 10 | import { Notifier } from '../../notifier' |
11 | import { createOrUpdateCacheFile } from '../cache-file' | ||
12 | import { createOrUpdateVideoPlaylist } from '../playlist' | ||
13 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
14 | import { resolveThread } from '../video-comments' | ||
15 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
16 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
16 | 17 | ||
17 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { | 18 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { |
18 | const { activity, byActor } = options | 19 | const { activity, byActor } = options |
@@ -101,6 +102,12 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc | |||
101 | return | 102 | return |
102 | } | 103 | } |
103 | 104 | ||
105 | // Try to not forward unwanted commments on our videos | ||
106 | if (video.isOwned() && await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) { | ||
107 | logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url) | ||
108 | return | ||
109 | } | ||
110 | |||
104 | if (video.isOwned() && created === true) { | 111 | if (video.isOwned() && created === true) { |
105 | // Don't resend the activity to the sender | 112 | // Don't resend the activity to the sender |
106 | const exceptions = [ byActor ] | 113 | const exceptions = [ byActor ] |
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts index 842eecb5b..d282d091b 100644 --- a/server/lib/blocklist.ts +++ b/server/lib/blocklist.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { sequelizeTypescript } from '@server/initializers/database' | 1 | import { sequelizeTypescript } from '@server/initializers/database' |
2 | import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { MAccountBlocklist, MAccountId, MAccountServer, MServerBlocklist } from '@server/typings/models' | ||
3 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 4 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
4 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | 5 | import { ServerBlocklistModel } from '../models/server/server-blocklist' |
5 | 6 | ||
@@ -33,9 +34,29 @@ function removeServerFromBlocklist (serverBlock: MServerBlocklist) { | |||
33 | }) | 34 | }) |
34 | } | 35 | } |
35 | 36 | ||
37 | async function isBlockedByServerOrAccount (targetAccount: MAccountServer, userAccount?: MAccountId) { | ||
38 | const serverAccountId = (await getServerActor()).Account.id | ||
39 | const sourceAccounts = [ serverAccountId ] | ||
40 | |||
41 | if (userAccount) sourceAccounts.push(userAccount.id) | ||
42 | |||
43 | const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, targetAccount.id) | ||
44 | if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) { | ||
45 | return true | ||
46 | } | ||
47 | |||
48 | const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, targetAccount.Actor.serverId) | ||
49 | if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) { | ||
50 | return true | ||
51 | } | ||
52 | |||
53 | return false | ||
54 | } | ||
55 | |||
36 | export { | 56 | export { |
37 | addAccountInBlocklist, | 57 | addAccountInBlocklist, |
38 | addServerInBlocklist, | 58 | addServerInBlocklist, |
39 | removeAccountFromBlocklist, | 59 | removeAccountFromBlocklist, |
40 | removeServerFromBlocklist | 60 | removeServerFromBlocklist, |
61 | isBlockedByServerOrAccount | ||
41 | } | 62 | } |
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 017739523..89f91e031 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts | |||
@@ -1,12 +1,22 @@ | |||
1 | import { getServerActor } from '@server/models/application/application' | ||
2 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
3 | import { | ||
4 | MUser, | ||
5 | MUserAccount, | ||
6 | MUserDefault, | ||
7 | MUserNotifSettingAccount, | ||
8 | MUserWithNotificationSetting, | ||
9 | UserNotificationModelForApi | ||
10 | } from '@server/typings/models/user' | ||
11 | import { MVideoImportVideo } from '@server/typings/models/video/video-import' | ||
1 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' | 12 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' |
13 | import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos' | ||
2 | import { logger } from '../helpers/logger' | 14 | import { logger } from '../helpers/logger' |
3 | import { Emailer } from './emailer' | ||
4 | import { UserNotificationModel } from '../models/account/user-notification' | ||
5 | import { UserModel } from '../models/account/user' | ||
6 | import { PeerTubeSocket } from './peertube-socket' | ||
7 | import { CONFIG } from '../initializers/config' | 15 | import { CONFIG } from '../initializers/config' |
8 | import { VideoPrivacy, VideoState, VideoAbuse } from '../../shared/models/videos' | ||
9 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 16 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
17 | import { UserModel } from '../models/account/user' | ||
18 | import { UserNotificationModel } from '../models/account/user-notification' | ||
19 | import { MAccountServer, MActorFollowFull } from '../typings/models' | ||
10 | import { | 20 | import { |
11 | MCommentOwnerVideo, | 21 | MCommentOwnerVideo, |
12 | MVideoAbuseVideo, | 22 | MVideoAbuseVideo, |
@@ -15,18 +25,9 @@ import { | |||
15 | MVideoBlacklistVideo, | 25 | MVideoBlacklistVideo, |
16 | MVideoFullLight | 26 | MVideoFullLight |
17 | } from '../typings/models/video' | 27 | } from '../typings/models/video' |
18 | import { | 28 | import { isBlockedByServerOrAccount } from './blocklist' |
19 | MUser, | 29 | import { Emailer } from './emailer' |
20 | MUserAccount, | 30 | import { PeerTubeSocket } from './peertube-socket' |
21 | MUserDefault, | ||
22 | MUserNotifSettingAccount, | ||
23 | MUserWithNotificationSetting, | ||
24 | UserNotificationModelForApi | ||
25 | } from '@server/typings/models/user' | ||
26 | import { MAccountDefault, MActorFollowFull } from '../typings/models' | ||
27 | import { MVideoImportVideo } from '@server/typings/models/video/video-import' | ||
28 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
29 | import { getServerActor } from '@server/models/application/application' | ||
30 | 31 | ||
31 | class Notifier { | 32 | class Notifier { |
32 | 33 | ||
@@ -169,7 +170,7 @@ class Notifier { | |||
169 | // Not our user or user comments its own video | 170 | // Not our user or user comments its own video |
170 | if (!user || comment.Account.userId === user.id) return | 171 | if (!user || comment.Account.userId === user.id) return |
171 | 172 | ||
172 | if (await this.isBlockedByServerOrAccount(user, comment.Account)) return | 173 | if (await this.isBlockedByServerOrUser(comment.Account, user)) return |
173 | 174 | ||
174 | logger.info('Notifying user %s of new comment %s.', user.username, comment.url) | 175 | logger.info('Notifying user %s of new comment %s.', user.username, comment.url) |
175 | 176 | ||
@@ -270,7 +271,7 @@ class Notifier { | |||
270 | const followerAccount = actorFollow.ActorFollower.Account | 271 | const followerAccount = actorFollow.ActorFollower.Account |
271 | const followerAccountWithActor = Object.assign(followerAccount, { Actor: actorFollow.ActorFollower }) | 272 | const followerAccountWithActor = Object.assign(followerAccount, { Actor: actorFollow.ActorFollower }) |
272 | 273 | ||
273 | if (await this.isBlockedByServerOrAccount(user, followerAccountWithActor)) return | 274 | if (await this.isBlockedByServerOrUser(followerAccountWithActor, user)) return |
274 | 275 | ||
275 | logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName()) | 276 | logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName()) |
276 | 277 | ||
@@ -299,6 +300,9 @@ class Notifier { | |||
299 | private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) { | 300 | private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) { |
300 | const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) | 301 | const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) |
301 | 302 | ||
303 | const follower = Object.assign(actorFollow.ActorFollower.Account, { Actor: actorFollow.ActorFollower }) | ||
304 | if (await this.isBlockedByServerOrUser(follower)) return | ||
305 | |||
302 | logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url) | 306 | logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url) |
303 | 307 | ||
304 | function settingGetter (user: MUserWithNotificationSetting) { | 308 | function settingGetter (user: MUserWithNotificationSetting) { |
@@ -590,17 +594,8 @@ class Notifier { | |||
590 | return value & UserNotificationSettingValue.WEB | 594 | return value & UserNotificationSettingValue.WEB |
591 | } | 595 | } |
592 | 596 | ||
593 | private async isBlockedByServerOrAccount (user: MUserAccount, targetAccount: MAccountDefault) { | 597 | private isBlockedByServerOrUser (targetAccount: MAccountServer, user?: MUserAccount) { |
594 | const serverAccountId = (await getServerActor()).Account.id | 598 | return isBlockedByServerOrAccount(targetAccount, user?.Account) |
595 | const sourceAccounts = [ serverAccountId, user.Account.id ] | ||
596 | |||
597 | const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, targetAccount.id) | ||
598 | if (accountMutedHash[serverAccountId] || accountMutedHash[user.Account.id]) return true | ||
599 | |||
600 | const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, targetAccount.Actor.serverId) | ||
601 | if (instanceMutedHash[serverAccountId] || instanceMutedHash[user.Account.id]) return true | ||
602 | |||
603 | return false | ||
604 | } | 599 | } |
605 | 600 | ||
606 | static get Instance () { | 601 | static get Instance () { |
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 | } |
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/users/blocklist.ts index e37dbb5a4..8c9107a50 100644 --- a/server/tests/api/users/blocklist.ts +++ b/server/tests/api/users/blocklist.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { AccountBlock, ServerBlock, Video } from '../../../../shared/index' | 5 | import { AccountBlock, ServerBlock, Video, UserNotification, UserNotificationType } from '../../../../shared/index' |
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
8 | createUser, | 8 | createUser, |
@@ -11,7 +11,9 @@ import { | |||
11 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
12 | ServerInfo, | 12 | ServerInfo, |
13 | uploadVideo, | 13 | uploadVideo, |
14 | userLogin | 14 | userLogin, |
15 | follow, | ||
16 | unfollow | ||
15 | } from '../../../../shared/extra-utils/index' | 17 | } from '../../../../shared/extra-utils/index' |
16 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | 18 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' |
17 | import { getVideosList, getVideosListWithToken } from '../../../../shared/extra-utils/videos/videos' | 19 | import { getVideosList, getVideosListWithToken } from '../../../../shared/extra-utils/videos/videos' |
@@ -19,7 +21,8 @@ import { | |||
19 | addVideoCommentReply, | 21 | addVideoCommentReply, |
20 | addVideoCommentThread, | 22 | addVideoCommentThread, |
21 | getVideoCommentThreads, | 23 | getVideoCommentThreads, |
22 | getVideoThreadComments | 24 | getVideoThreadComments, |
25 | findCommentId | ||
23 | } from '../../../../shared/extra-utils/videos/video-comments' | 26 | } from '../../../../shared/extra-utils/videos/video-comments' |
24 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 27 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
25 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | 28 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' |
@@ -45,13 +48,13 @@ async function checkAllVideos (url: string, token: string) { | |||
45 | { | 48 | { |
46 | const res = await getVideosListWithToken(url, token) | 49 | const res = await getVideosListWithToken(url, token) |
47 | 50 | ||
48 | expect(res.body.data).to.have.lengthOf(4) | 51 | expect(res.body.data).to.have.lengthOf(5) |
49 | } | 52 | } |
50 | 53 | ||
51 | { | 54 | { |
52 | const res = await getVideosList(url) | 55 | const res = await getVideosList(url) |
53 | 56 | ||
54 | expect(res.body.data).to.have.lengthOf(4) | 57 | expect(res.body.data).to.have.lengthOf(5) |
55 | } | 58 | } |
56 | } | 59 | } |
57 | 60 | ||
@@ -76,13 +79,15 @@ async function checkCommentNotification ( | |||
76 | check: 'presence' | 'absence' | 79 | check: 'presence' | 'absence' |
77 | ) { | 80 | ) { |
78 | const resComment = await addVideoCommentThread(comment.server.url, comment.token, comment.videoUUID, comment.text) | 81 | const resComment = await addVideoCommentThread(comment.server.url, comment.token, comment.videoUUID, comment.text) |
79 | const threadId = resComment.body.comment.id | 82 | const created = resComment.body.comment as VideoComment |
83 | const threadId = created.id | ||
84 | const createdAt = created.createdAt | ||
80 | 85 | ||
81 | await waitJobs([ mainServer, comment.server ]) | 86 | await waitJobs([ mainServer, comment.server ]) |
82 | 87 | ||
83 | const res = await getUserNotifications(mainServer.url, mainServer.accessToken, 0, 30) | 88 | const res = await getUserNotifications(mainServer.url, mainServer.accessToken, 0, 30) |
84 | const commentNotifications = res.body.data | 89 | const commentNotifications = (res.body.data as UserNotification[]) |
85 | .filter(n => n.comment && n.comment.id === threadId) | 90 | .filter(n => n.comment && n.comment.video.uuid === comment.videoUUID && n.createdAt >= createdAt) |
86 | 91 | ||
87 | if (check === 'presence') expect(commentNotifications).to.have.lengthOf(1) | 92 | if (check === 'presence') expect(commentNotifications).to.have.lengthOf(1) |
88 | else expect(commentNotifications).to.have.lengthOf(0) | 93 | else expect(commentNotifications).to.have.lengthOf(0) |
@@ -96,6 +101,7 @@ describe('Test blocklist', function () { | |||
96 | let servers: ServerInfo[] | 101 | let servers: ServerInfo[] |
97 | let videoUUID1: string | 102 | let videoUUID1: string |
98 | let videoUUID2: string | 103 | let videoUUID2: string |
104 | let videoUUID3: string | ||
99 | let userToken1: string | 105 | let userToken1: string |
100 | let userModeratorToken: string | 106 | let userModeratorToken: string |
101 | let userToken2: string | 107 | let userToken2: string |
@@ -103,7 +109,7 @@ describe('Test blocklist', function () { | |||
103 | before(async function () { | 109 | before(async function () { |
104 | this.timeout(60000) | 110 | this.timeout(60000) |
105 | 111 | ||
106 | servers = await flushAndRunMultipleServers(2) | 112 | servers = await flushAndRunMultipleServers(3) |
107 | await setAccessTokensToServers(servers) | 113 | await setAccessTokensToServers(servers) |
108 | 114 | ||
109 | { | 115 | { |
@@ -139,7 +145,13 @@ describe('Test blocklist', function () { | |||
139 | videoUUID2 = res.body.video.uuid | 145 | videoUUID2 = res.body.video.uuid |
140 | } | 146 | } |
141 | 147 | ||
148 | { | ||
149 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 2 server 1' }) | ||
150 | videoUUID3 = res.body.video.uuid | ||
151 | } | ||
152 | |||
142 | await doubleFollow(servers[0], servers[1]) | 153 | await doubleFollow(servers[0], servers[1]) |
154 | await doubleFollow(servers[0], servers[2]) | ||
143 | 155 | ||
144 | { | 156 | { |
145 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID1, 'comment root 1') | 157 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID1, 'comment root 1') |
@@ -174,7 +186,7 @@ describe('Test blocklist', function () { | |||
174 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) | 186 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) |
175 | 187 | ||
176 | const videos: Video[] = res.body.data | 188 | const videos: Video[] = res.body.data |
177 | expect(videos).to.have.lengthOf(3) | 189 | expect(videos).to.have.lengthOf(4) |
178 | 190 | ||
179 | const v = videos.find(v => v.name === 'video user 2') | 191 | const v = videos.find(v => v.name === 'video user 2') |
180 | expect(v).to.be.undefined | 192 | expect(v).to.be.undefined |
@@ -188,7 +200,7 @@ describe('Test blocklist', function () { | |||
188 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) | 200 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) |
189 | 201 | ||
190 | const videos: Video[] = res.body.data | 202 | const videos: Video[] = res.body.data |
191 | expect(videos).to.have.lengthOf(2) | 203 | expect(videos).to.have.lengthOf(3) |
192 | 204 | ||
193 | const v = videos.find(v => v.name === 'video user 1') | 205 | const v = videos.find(v => v.name === 'video user 1') |
194 | expect(v).to.be.undefined | 206 | expect(v).to.be.undefined |
@@ -235,10 +247,6 @@ describe('Test blocklist', function () { | |||
235 | return checkAllVideos(servers[0].url, userToken1) | 247 | return checkAllVideos(servers[0].url, userToken1) |
236 | }) | 248 | }) |
237 | 249 | ||
238 | it('Should list all the comments with another user', async function () { | ||
239 | return checkAllComments(servers[0].url, userToken1, videoUUID1) | ||
240 | }) | ||
241 | |||
242 | it('Should list blocked accounts', async function () { | 250 | it('Should list blocked accounts', async function () { |
243 | { | 251 | { |
244 | const res = await getAccountBlocklistByAccount(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt') | 252 | const res = await getAccountBlocklistByAccount(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt') |
@@ -269,6 +277,61 @@ describe('Test blocklist', function () { | |||
269 | } | 277 | } |
270 | }) | 278 | }) |
271 | 279 | ||
280 | it('Should not allow a remote blocked user to comment my videos', async function () { | ||
281 | this.timeout(60000) | ||
282 | |||
283 | { | ||
284 | await addVideoCommentThread(servers[1].url, userToken2, videoUUID3, 'comment user 2') | ||
285 | await waitJobs(servers) | ||
286 | |||
287 | await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID3, 'uploader') | ||
288 | await waitJobs(servers) | ||
289 | |||
290 | const commentId = await findCommentId(servers[1].url, videoUUID3, 'uploader') | ||
291 | const message = 'reply by user 2' | ||
292 | const resReply = await addVideoCommentReply(servers[1].url, userToken2, videoUUID3, commentId, message) | ||
293 | await addVideoCommentReply(servers[1].url, servers[1].accessToken, videoUUID3, resReply.body.comment.id, 'another reply') | ||
294 | |||
295 | await waitJobs(servers) | ||
296 | } | ||
297 | |||
298 | // Server 2 has all the comments | ||
299 | { | ||
300 | const resThreads = await getVideoCommentThreads(servers[1].url, videoUUID3, 0, 25, '-createdAt') | ||
301 | const threads: VideoComment[] = resThreads.body.data | ||
302 | |||
303 | expect(threads).to.have.lengthOf(2) | ||
304 | expect(threads[0].text).to.equal('uploader') | ||
305 | expect(threads[1].text).to.equal('comment user 2') | ||
306 | |||
307 | const resReplies = await getVideoThreadComments(servers[1].url, videoUUID3, threads[0].id) | ||
308 | |||
309 | const tree: VideoCommentThreadTree = resReplies.body | ||
310 | expect(tree.children).to.have.lengthOf(1) | ||
311 | expect(tree.children[0].comment.text).to.equal('reply by user 2') | ||
312 | expect(tree.children[0].children).to.have.lengthOf(1) | ||
313 | expect(tree.children[0].children[0].comment.text).to.equal('another reply') | ||
314 | } | ||
315 | |||
316 | // Server 1 and 3 should only have uploader comments | ||
317 | for (const server of [ servers[0], servers[2] ]) { | ||
318 | const resThreads = await getVideoCommentThreads(server.url, videoUUID3, 0, 25, '-createdAt') | ||
319 | const threads: VideoComment[] = resThreads.body.data | ||
320 | |||
321 | expect(threads).to.have.lengthOf(1) | ||
322 | expect(threads[0].text).to.equal('uploader') | ||
323 | |||
324 | const resReplies = await getVideoThreadComments(server.url, videoUUID3, threads[0].id) | ||
325 | |||
326 | const tree: VideoCommentThreadTree = resReplies.body | ||
327 | if (server.serverNumber === 1) { | ||
328 | expect(tree.children).to.have.lengthOf(0) | ||
329 | } else { | ||
330 | expect(tree.children).to.have.lengthOf(1) | ||
331 | } | ||
332 | } | ||
333 | }) | ||
334 | |||
272 | it('Should unblock the remote account', async function () { | 335 | it('Should unblock the remote account', async function () { |
273 | await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port) | 336 | await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port) |
274 | }) | 337 | }) |
@@ -277,12 +340,37 @@ describe('Test blocklist', function () { | |||
277 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) | 340 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) |
278 | 341 | ||
279 | const videos: Video[] = res.body.data | 342 | const videos: Video[] = res.body.data |
280 | expect(videos).to.have.lengthOf(3) | 343 | expect(videos).to.have.lengthOf(4) |
281 | 344 | ||
282 | const v = videos.find(v => v.name === 'video user 2') | 345 | const v = videos.find(v => v.name === 'video user 2') |
283 | expect(v).not.to.be.undefined | 346 | expect(v).not.to.be.undefined |
284 | }) | 347 | }) |
285 | 348 | ||
349 | it('Should display its comments on my video', async function () { | ||
350 | for (const server of servers) { | ||
351 | const resThreads = await getVideoCommentThreads(server.url, videoUUID3, 0, 25, '-createdAt') | ||
352 | const threads: VideoComment[] = resThreads.body.data | ||
353 | |||
354 | // Server 3 should not have 2 comment threads, because server 1 did not forward the server 2 comment | ||
355 | if (server.serverNumber === 3) { | ||
356 | expect(threads).to.have.lengthOf(1) | ||
357 | continue | ||
358 | } | ||
359 | |||
360 | expect(threads).to.have.lengthOf(2) | ||
361 | expect(threads[0].text).to.equal('uploader') | ||
362 | expect(threads[1].text).to.equal('comment user 2') | ||
363 | |||
364 | const resReplies = await getVideoThreadComments(server.url, videoUUID3, threads[0].id) | ||
365 | |||
366 | const tree: VideoCommentThreadTree = resReplies.body | ||
367 | expect(tree.children).to.have.lengthOf(1) | ||
368 | expect(tree.children[0].comment.text).to.equal('reply by user 2') | ||
369 | expect(tree.children[0].children).to.have.lengthOf(1) | ||
370 | expect(tree.children[0].children[0].comment.text).to.equal('another reply') | ||
371 | } | ||
372 | }) | ||
373 | |||
286 | it('Should unblock the local account', async function () { | 374 | it('Should unblock the local account', async function () { |
287 | await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1') | 375 | await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1') |
288 | }) | 376 | }) |
@@ -328,7 +416,7 @@ describe('Test blocklist', function () { | |||
328 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) | 416 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) |
329 | 417 | ||
330 | const videos: Video[] = res.body.data | 418 | const videos: Video[] = res.body.data |
331 | expect(videos).to.have.lengthOf(2) | 419 | expect(videos).to.have.lengthOf(3) |
332 | 420 | ||
333 | const v1 = videos.find(v => v.name === 'video user 2') | 421 | const v1 = videos.find(v => v.name === 'video user 2') |
334 | const v2 = videos.find(v => v.name === 'video server 2') | 422 | const v2 = videos.find(v => v.name === 'video server 2') |
@@ -442,7 +530,7 @@ describe('Test blocklist', function () { | |||
442 | const res = await getVideosListWithToken(servers[0].url, token) | 530 | const res = await getVideosListWithToken(servers[0].url, token) |
443 | 531 | ||
444 | const videos: Video[] = res.body.data | 532 | const videos: Video[] = res.body.data |
445 | expect(videos).to.have.lengthOf(3) | 533 | expect(videos).to.have.lengthOf(4) |
446 | 534 | ||
447 | const v = videos.find(v => v.name === 'video user 2') | 535 | const v = videos.find(v => v.name === 'video user 2') |
448 | expect(v).to.be.undefined | 536 | expect(v).to.be.undefined |
@@ -458,7 +546,7 @@ describe('Test blocklist', function () { | |||
458 | const res = await getVideosListWithToken(servers[0].url, token) | 546 | const res = await getVideosListWithToken(servers[0].url, token) |
459 | 547 | ||
460 | const videos: Video[] = res.body.data | 548 | const videos: Video[] = res.body.data |
461 | expect(videos).to.have.lengthOf(2) | 549 | expect(videos).to.have.lengthOf(3) |
462 | 550 | ||
463 | const v = videos.find(v => v.name === 'video user 1') | 551 | const v = videos.find(v => v.name === 'video user 1') |
464 | expect(v).to.be.undefined | 552 | expect(v).to.be.undefined |
@@ -545,7 +633,7 @@ describe('Test blocklist', function () { | |||
545 | const res = await getVideosListWithToken(servers[0].url, token) | 633 | const res = await getVideosListWithToken(servers[0].url, token) |
546 | 634 | ||
547 | const videos: Video[] = res.body.data | 635 | const videos: Video[] = res.body.data |
548 | expect(videos).to.have.lengthOf(3) | 636 | expect(videos).to.have.lengthOf(4) |
549 | 637 | ||
550 | const v = videos.find(v => v.name === 'video user 2') | 638 | const v = videos.find(v => v.name === 'video user 2') |
551 | expect(v).not.to.be.undefined | 639 | expect(v).not.to.be.undefined |
@@ -606,7 +694,7 @@ describe('Test blocklist', function () { | |||
606 | 694 | ||
607 | for (const res of [ res1, res2 ]) { | 695 | for (const res of [ res1, res2 ]) { |
608 | const videos: Video[] = res.body.data | 696 | const videos: Video[] = res.body.data |
609 | expect(videos).to.have.lengthOf(2) | 697 | expect(videos).to.have.lengthOf(3) |
610 | 698 | ||
611 | const v1 = videos.find(v => v.name === 'video user 2') | 699 | const v1 = videos.find(v => v.name === 'video user 2') |
612 | const v2 = videos.find(v => v.name === 'video server 2') | 700 | const v2 = videos.find(v => v.name === 'video server 2') |
@@ -631,7 +719,7 @@ describe('Test blocklist', function () { | |||
631 | }) | 719 | }) |
632 | 720 | ||
633 | it('Should not have notification from blocked instances by instance', async function () { | 721 | it('Should not have notification from blocked instances by instance', async function () { |
634 | this.timeout(20000) | 722 | this.timeout(50000) |
635 | 723 | ||
636 | { | 724 | { |
637 | const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' } | 725 | const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' } |
@@ -647,6 +735,24 @@ describe('Test blocklist', function () { | |||
647 | } | 735 | } |
648 | await checkCommentNotification(servers[0], comment, 'absence') | 736 | await checkCommentNotification(servers[0], comment, 'absence') |
649 | } | 737 | } |
738 | |||
739 | { | ||
740 | const now = new Date() | ||
741 | await unfollow(servers[1].url, servers[1].accessToken, servers[0]) | ||
742 | await waitJobs(servers) | ||
743 | await follow(servers[1].url, [ servers[0].host ], servers[1].accessToken) | ||
744 | |||
745 | await waitJobs(servers) | ||
746 | |||
747 | const res = await getUserNotifications(servers[0].url, servers[0].accessToken, 0, 30) | ||
748 | const commentNotifications = (res.body.data as UserNotification[]) | ||
749 | .filter(n => { | ||
750 | return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && | ||
751 | n.createdAt >= now.toISOString() | ||
752 | }) | ||
753 | |||
754 | expect(commentNotifications).to.have.lengthOf(0) | ||
755 | } | ||
650 | }) | 756 | }) |
651 | 757 | ||
652 | it('Should list blocked servers', async function () { | 758 | it('Should list blocked servers', async function () { |
@@ -678,7 +784,7 @@ describe('Test blocklist', function () { | |||
678 | }) | 784 | }) |
679 | 785 | ||
680 | it('Should have notification from unblocked instances', async function () { | 786 | it('Should have notification from unblocked instances', async function () { |
681 | this.timeout(20000) | 787 | this.timeout(50000) |
682 | 788 | ||
683 | { | 789 | { |
684 | const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' } | 790 | const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' } |
@@ -694,6 +800,24 @@ describe('Test blocklist', function () { | |||
694 | } | 800 | } |
695 | await checkCommentNotification(servers[0], comment, 'presence') | 801 | await checkCommentNotification(servers[0], comment, 'presence') |
696 | } | 802 | } |
803 | |||
804 | { | ||
805 | const now = new Date() | ||
806 | await unfollow(servers[1].url, servers[1].accessToken, servers[0]) | ||
807 | await waitJobs(servers) | ||
808 | await follow(servers[1].url, [ servers[0].host ], servers[1].accessToken) | ||
809 | |||
810 | await waitJobs(servers) | ||
811 | |||
812 | const res = await getUserNotifications(servers[0].url, servers[0].accessToken, 0, 30) | ||
813 | const commentNotifications = (res.body.data as UserNotification[]) | ||
814 | .filter(n => { | ||
815 | return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && | ||
816 | n.createdAt >= now.toISOString() | ||
817 | }) | ||
818 | |||
819 | expect(commentNotifications).to.have.lengthOf(1) | ||
820 | } | ||
697 | }) | 821 | }) |
698 | }) | 822 | }) |
699 | }) | 823 | }) |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index e3029f1ae..d7b04373f 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -37,7 +37,8 @@ import { | |||
37 | addVideoCommentThread, | 37 | addVideoCommentThread, |
38 | deleteVideoComment, | 38 | deleteVideoComment, |
39 | getVideoCommentThreads, | 39 | getVideoCommentThreads, |
40 | getVideoThreadComments | 40 | getVideoThreadComments, |
41 | findCommentId | ||
41 | } from '../../../../shared/extra-utils/videos/video-comments' | 42 | } from '../../../../shared/extra-utils/videos/video-comments' |
42 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 43 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
43 | 44 | ||
@@ -773,8 +774,7 @@ describe('Test multiple servers', function () { | |||
773 | await waitJobs(servers) | 774 | await waitJobs(servers) |
774 | 775 | ||
775 | { | 776 | { |
776 | const res = await getVideoCommentThreads(servers[1].url, videoUUID, 0, 5) | 777 | const threadId = await findCommentId(servers[1].url, videoUUID, 'my super first comment') |
777 | const threadId = res.body.data.find(c => c.text === 'my super first comment').id | ||
778 | 778 | ||
779 | const text = 'my super answer to thread 1' | 779 | const text = 'my super answer to thread 1' |
780 | await addVideoCommentReply(servers[1].url, servers[1].accessToken, videoUUID, threadId, text) | 780 | await addVideoCommentReply(servers[1].url, servers[1].accessToken, videoUUID, threadId, text) |
@@ -783,8 +783,7 @@ describe('Test multiple servers', function () { | |||
783 | await waitJobs(servers) | 783 | await waitJobs(servers) |
784 | 784 | ||
785 | { | 785 | { |
786 | const res1 = await getVideoCommentThreads(servers[2].url, videoUUID, 0, 5) | 786 | const threadId = await findCommentId(servers[2].url, videoUUID, 'my super first comment') |
787 | const threadId = res1.body.data.find(c => c.text === 'my super first comment').id | ||
788 | 787 | ||
789 | const res2 = await getVideoThreadComments(servers[2].url, videoUUID, threadId) | 788 | const res2 = await getVideoThreadComments(servers[2].url, videoUUID, threadId) |
790 | const childCommentId = res2.body.children[0].comment.id | 789 | const childCommentId = res2.body.children[0].comment.id |
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 7fac921a3..ba961cdba 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -1,7 +1,14 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import * as libxmljs from 'libxmljs' | ||
6 | import { | ||
7 | addAccountToAccountBlocklist, | ||
8 | addAccountToServerBlocklist, | ||
9 | removeAccountFromServerBlocklist | ||
10 | } from '@shared/extra-utils/users/blocklist' | ||
11 | import { VideoPrivacy } from '@shared/models' | ||
5 | import { | 12 | import { |
6 | cleanupTests, | 13 | cleanupTests, |
7 | createUser, | 14 | createUser, |
@@ -13,14 +20,12 @@ import { | |||
13 | ServerInfo, | 20 | ServerInfo, |
14 | setAccessTokensToServers, | 21 | setAccessTokensToServers, |
15 | uploadVideo, | 22 | uploadVideo, |
23 | uploadVideoAndGetId, | ||
16 | userLogin | 24 | userLogin |
17 | } from '../../../shared/extra-utils' | 25 | } from '../../../shared/extra-utils' |
18 | import * as libxmljs from 'libxmljs' | ||
19 | import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments' | ||
20 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' | 26 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' |
27 | import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments' | ||
21 | import { User } from '../../../shared/models/users' | 28 | import { User } from '../../../shared/models/users' |
22 | import { VideoPrivacy } from '@shared/models' | ||
23 | import { addAccountToServerBlocklist } from '@shared/extra-utils/users/blocklist' | ||
24 | 29 | ||
25 | chai.use(require('chai-xml')) | 30 | chai.use(require('chai-xml')) |
26 | chai.use(require('chai-json-schema')) | 31 | chai.use(require('chai-json-schema')) |
@@ -219,7 +224,11 @@ describe('Test syndication feeds', () => { | |||
219 | }) | 224 | }) |
220 | 225 | ||
221 | it('Should not list comments from muted accounts or instances', async function () { | 226 | it('Should not list comments from muted accounts or instances', async function () { |
222 | await addAccountToServerBlocklist(servers[1].url, servers[1].accessToken, 'root@localhost:' + servers[0].port) | 227 | this.timeout(30000) |
228 | |||
229 | const remoteHandle = 'root@localhost:' + servers[0].port | ||
230 | |||
231 | await addAccountToServerBlocklist(servers[1].url, servers[1].accessToken, remoteHandle) | ||
223 | 232 | ||
224 | { | 233 | { |
225 | const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 2 }) | 234 | const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 2 }) |
@@ -227,6 +236,26 @@ describe('Test syndication feeds', () => { | |||
227 | expect(jsonObj.items.length).to.be.equal(0) | 236 | expect(jsonObj.items.length).to.be.equal(0) |
228 | } | 237 | } |
229 | 238 | ||
239 | await removeAccountFromServerBlocklist(servers[1].url, servers[1].accessToken, remoteHandle) | ||
240 | |||
241 | { | ||
242 | const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' })).uuid | ||
243 | await waitJobs(servers) | ||
244 | await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'super comment') | ||
245 | await waitJobs(servers) | ||
246 | |||
247 | const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 3 }) | ||
248 | const jsonObj = JSON.parse(json.text) | ||
249 | expect(jsonObj.items.length).to.be.equal(3) | ||
250 | } | ||
251 | |||
252 | await addAccountToAccountBlocklist(servers[1].url, servers[1].accessToken, remoteHandle) | ||
253 | |||
254 | { | ||
255 | const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 4 }) | ||
256 | const jsonObj = JSON.parse(json.text) | ||
257 | expect(jsonObj.items.length).to.be.equal(2) | ||
258 | } | ||
230 | }) | 259 | }) |
231 | }) | 260 | }) |
232 | 261 | ||
diff --git a/shared/extra-utils/videos/video-comments.ts b/shared/extra-utils/videos/video-comments.ts index 81c48412d..831e5e7d4 100644 --- a/shared/extra-utils/videos/video-comments.ts +++ b/shared/extra-utils/videos/video-comments.ts | |||
@@ -61,6 +61,12 @@ function addVideoCommentReply ( | |||
61 | .expect(expectedStatus) | 61 | .expect(expectedStatus) |
62 | } | 62 | } |
63 | 63 | ||
64 | async function findCommentId (url: string, videoId: number | string, text: string) { | ||
65 | const res = await getVideoCommentThreads(url, videoId, 0, 25, '-createdAt') | ||
66 | |||
67 | return res.body.data.find(c => c.text === text).id as number | ||
68 | } | ||
69 | |||
64 | function deleteVideoComment ( | 70 | function deleteVideoComment ( |
65 | url: string, | 71 | url: string, |
66 | token: string, | 72 | token: string, |
@@ -85,5 +91,6 @@ export { | |||
85 | getVideoThreadComments, | 91 | getVideoThreadComments, |
86 | addVideoCommentThread, | 92 | addVideoCommentThread, |
87 | addVideoCommentReply, | 93 | addVideoCommentReply, |
94 | findCommentId, | ||
88 | deleteVideoComment | 95 | deleteVideoComment |
89 | } | 96 | } |
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts index 0d36a38a2..99e591cb2 100644 --- a/shared/extra-utils/videos/videos.ts +++ b/shared/extra-utils/videos/videos.ts | |||
@@ -95,6 +95,12 @@ function getVideo (url: string, id: number | string, expectedStatus = 200) { | |||
95 | .expect(expectedStatus) | 95 | .expect(expectedStatus) |
96 | } | 96 | } |
97 | 97 | ||
98 | async function getVideoIdFromUUID (url: string, uuid: string) { | ||
99 | const res = await getVideo(url, uuid) | ||
100 | |||
101 | return res.body.id | ||
102 | } | ||
103 | |||
98 | function getVideoFileMetadataUrl (url: string) { | 104 | function getVideoFileMetadataUrl (url: string) { |
99 | return request(url) | 105 | return request(url) |
100 | .get('/') | 106 | .get('/') |
@@ -669,5 +675,6 @@ export { | |||
669 | checkVideoFilesWereRemoved, | 675 | checkVideoFilesWereRemoved, |
670 | getPlaylistVideos, | 676 | getPlaylistVideos, |
671 | uploadVideoAndGetId, | 677 | uploadVideoAndGetId, |
672 | getLocalIdByUUID | 678 | getLocalIdByUUID, |
679 | getVideoIdFromUUID | ||
673 | } | 680 | } |