aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-03-18 11:17:35 +0100
committerChocobozzz <me@florianbigard.com>2022-03-18 11:21:50 +0100
commit57e4e1c1a95c3a81a967f54ecc2a510d8b0e129c (patch)
treefcf12670d643ec4a3b5eccdfa834227c0417d988 /server
parent2e3f7a5a6fbae276d3ba1cb1b08289917ec7604b (diff)
downloadPeerTube-57e4e1c1a95c3a81a967f54ecc2a510d8b0e129c.tar.gz
PeerTube-57e4e1c1a95c3a81a967f54ecc2a510d8b0e129c.tar.zst
PeerTube-57e4e1c1a95c3a81a967f54ecc2a510d8b0e129c.zip
Don't store remote rates of remote videos
In the future we'll stop to expose all available rates to improve users privacy
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts2
-rw-r--r--server/controllers/api/search/search-videos.ts3
-rw-r--r--server/helpers/activitypub.ts4
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0695-remove-remote-rates.ts28
-rw-r--r--server/lib/activitypub/audience.ts70
-rw-r--r--server/lib/activitypub/process/process-announce.ts2
-rw-r--r--server/lib/activitypub/process/process-create.ts4
-rw-r--r--server/lib/activitypub/process/process-delete.ts2
-rw-r--r--server/lib/activitypub/process/process-dislike.ts20
-rw-r--r--server/lib/activitypub/process/process-like.ts21
-rw-r--r--server/lib/activitypub/process/process-undo.ts61
-rw-r--r--server/lib/activitypub/process/process-update.ts2
-rw-r--r--server/lib/activitypub/process/process-view.ts2
-rw-r--r--server/lib/activitypub/send/send-accept.ts4
-rw-r--r--server/lib/activitypub/send/send-announce.ts7
-rw-r--r--server/lib/activitypub/send/send-create.ts21
-rw-r--r--server/lib/activitypub/send/send-delete.ts7
-rw-r--r--server/lib/activitypub/send/send-dislike.ts10
-rw-r--r--server/lib/activitypub/send/send-flag.ts4
-rw-r--r--server/lib/activitypub/send/send-follow.ts4
-rw-r--r--server/lib/activitypub/send/send-like.ts10
-rw-r--r--server/lib/activitypub/send/send-reject.ts4
-rw-r--r--server/lib/activitypub/send/send-undo.ts60
-rw-r--r--server/lib/activitypub/send/send-update.ts15
-rw-r--r--server/lib/activitypub/send/send-view.ts4
-rw-r--r--server/lib/activitypub/send/shared/audience-utils.ts74
-rw-r--r--server/lib/activitypub/send/shared/index.ts2
-rw-r--r--server/lib/activitypub/send/shared/send-utils.ts (renamed from server/lib/activitypub/send/utils.ts)54
-rw-r--r--server/lib/activitypub/video-comments.ts2
-rw-r--r--server/lib/activitypub/video-rates.ts79
-rw-r--r--server/lib/activitypub/videos/get.ts2
-rw-r--r--server/lib/activitypub/videos/shared/video-sync-attributes.ts56
-rw-r--r--server/lib/activitypub/videos/updater.ts4
-rw-r--r--server/lib/job-queue/handlers/activitypub-cleaner.ts2
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-fetcher.ts6
-rw-r--r--server/lib/job-queue/handlers/activitypub-refresher.ts2
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts2
-rw-r--r--server/models/account/account-video-rate.ts24
-rw-r--r--server/models/video/video.ts16
-rw-r--r--server/tests/api/videos/multiple-servers.ts4
41 files changed, 382 insertions, 320 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index c4d1be121..fc27ebbe8 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -66,11 +66,13 @@ activityPubClientRouter.get('/accounts?/:name/playlists',
66) 66)
67activityPubClientRouter.get('/accounts?/:name/likes/:videoId', 67activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
68 executeIfActivityPub, 68 executeIfActivityPub,
69 cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
69 asyncMiddleware(getAccountVideoRateValidatorFactory('like')), 70 asyncMiddleware(getAccountVideoRateValidatorFactory('like')),
70 getAccountVideoRateFactory('like') 71 getAccountVideoRateFactory('like')
71) 72)
72activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', 73activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
73 executeIfActivityPub, 74 executeIfActivityPub,
75 cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
74 asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')), 76 asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')),
75 getAccountVideoRateFactory('dislike') 77 getAccountVideoRateFactory('dislike')
76) 78)
diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts
index 68428d766..1d7a7b7bc 100644
--- a/server/controllers/api/search/search-videos.ts
+++ b/server/controllers/api/search/search-videos.ts
@@ -134,8 +134,7 @@ async function searchVideoURI (url: string, res: express.Response) {
134 if (isUserAbleToSearchRemoteURI(res)) { 134 if (isUserAbleToSearchRemoteURI(res)) {
135 try { 135 try {
136 const syncParam = { 136 const syncParam = {
137 likes: false, 137 rates: false,
138 dislikes: false,
139 shares: false, 138 shares: false,
140 comments: false, 139 comments: false,
141 thumbnail: true, 140 thumbnail: true,
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index d0bcc6785..9d6d8b2fa 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -154,7 +154,9 @@ async function activityPubCollectionPagination (
154 id: baseUrl, 154 id: baseUrl,
155 type: 'OrderedCollectionPage', 155 type: 'OrderedCollectionPage',
156 totalItems: result.total, 156 totalItems: result.total,
157 first: baseUrl + '?page=1' 157 first: result.data.length === 0
158 ? undefined
159 : baseUrl + '?page=1'
158 } 160 }
159 } 161 }
160 162
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index e0f6f2bd2..aaf39e6ec 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 690 27const LAST_MIGRATION_VERSION = 695
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
diff --git a/server/initializers/migrations/0695-remove-remote-rates.ts b/server/initializers/migrations/0695-remove-remote-rates.ts
new file mode 100644
index 000000000..f5c394bae
--- /dev/null
+++ b/server/initializers/migrations/0695-remove-remote-rates.ts
@@ -0,0 +1,28 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const query = 'DELETE FROM "accountVideoRate" ' +
10 'WHERE "accountVideoRate".id IN (' +
11 'SELECT "accountVideoRate".id FROM "accountVideoRate" ' +
12 'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' +
13 'INNER JOIN actor ON actor.id = account."actorId" ' +
14 'INNER JOIN video ON video.id = "accountVideoRate"."videoId" ' +
15 'WHERE actor."serverId" IS NOT NULL AND video.remote IS TRUE' +
16 ')'
17
18 await utils.sequelize.query(query, { type: Sequelize.QueryTypes.BULKDELETE, transaction: utils.transaction })
19}
20
21function down () {
22 throw new Error('Not implemented.')
23}
24
25export {
26 up,
27 down
28}
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts
index d0558f191..2bd5bb066 100644
--- a/server/lib/activitypub/audience.ts
+++ b/server/lib/activitypub/audience.ts
@@ -1,68 +1,6 @@
1import { Transaction } from 'sequelize'
2import { ActivityAudience } from '../../../shared/models/activitypub' 1import { ActivityAudience } from '../../../shared/models/activitypub'
3import { ACTIVITY_PUB } from '../../initializers/constants' 2import { ACTIVITY_PUB } from '../../initializers/constants'
4import { ActorModel } from '../../models/actor/actor' 3import { MActorFollowersUrl } from '../../types/models'
5import { VideoModel } from '../../models/video/video'
6import { VideoShareModel } from '../../models/video/video-share'
7import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../types/models'
8
9function getRemoteVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience {
10 return {
11 to: [ accountActor.url ],
12 cc: actorsInvolvedInVideo.map(a => a.followersUrl)
13 }
14}
15
16function getVideoCommentAudience (
17 videoComment: MCommentOwnerVideo,
18 threadParentComments: MCommentOwner[],
19 actorsInvolvedInVideo: MActorFollowersUrl[],
20 isOrigin = false
21): ActivityAudience {
22 const to = [ ACTIVITY_PUB.PUBLIC ]
23 const cc: string[] = []
24
25 // Owner of the video we comment
26 if (isOrigin === false) {
27 cc.push(videoComment.Video.VideoChannel.Account.Actor.url)
28 }
29
30 // Followers of the poster
31 cc.push(videoComment.Account.Actor.followersUrl)
32
33 // Send to actors we reply to
34 for (const parentComment of threadParentComments) {
35 if (parentComment.isDeleted()) continue
36
37 cc.push(parentComment.Account.Actor.url)
38 }
39
40 return {
41 to,
42 cc: cc.concat(actorsInvolvedInVideo.map(a => a.followersUrl))
43 }
44}
45
46function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
47 return {
48 to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
49 cc: []
50 }
51}
52
53async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
54 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
55
56 const videoAll = video as VideoModel
57
58 const videoActor = videoAll.VideoChannel?.Account
59 ? videoAll.VideoChannel.Account.Actor
60 : await ActorModel.loadFromAccountByVideoId(video.id, t)
61
62 actors.push(videoActor)
63
64 return actors
65}
66 4
67function getAudience (actorSender: MActorFollowersUrl, isPublic = true) { 5function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
68 return buildAudience([ actorSender.followersUrl ], isPublic) 6 return buildAudience([ actorSender.followersUrl ], isPublic)
@@ -92,9 +30,5 @@ function audiencify<T> (object: T, audience: ActivityAudience) {
92export { 30export {
93 buildAudience, 31 buildAudience,
94 getAudience, 32 getAudience,
95 getRemoteVideoAudience, 33 audiencify
96 getActorsInvolvedInVideo,
97 getAudienceFromFollowersOf,
98 audiencify,
99 getVideoCommentAudience
100} 34}
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
index 2619d9754..200f8ce11 100644
--- a/server/lib/activitypub/process/process-announce.ts
+++ b/server/lib/activitypub/process/process-announce.ts
@@ -2,7 +2,7 @@ import { ActivityAnnounce } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { sequelizeTypescript } from '../../../initializers/database' 3import { sequelizeTypescript } from '../../../initializers/database'
4import { VideoShareModel } from '../../../models/video/video-share' 4import { VideoShareModel } from '../../../models/video/video-share'
5import { forwardVideoRelatedActivity } from '../send/utils' 5import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
6import { getOrCreateAPVideo } from '../videos' 6import { getOrCreateAPVideo } from '../videos'
7import { Notifier } from '../../notifier' 7import { Notifier } from '../../notifier'
8import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 3e8ad184c..b5b1a0feb 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -9,7 +9,7 @@ import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFile
9import { Notifier } from '../../notifier' 9import { Notifier } from '../../notifier'
10import { createOrUpdateCacheFile } from '../cache-file' 10import { createOrUpdateCacheFile } from '../cache-file'
11import { createOrUpdateVideoPlaylist } from '../playlists' 11import { createOrUpdateVideoPlaylist } from '../playlists'
12import { forwardVideoRelatedActivity } from '../send/utils' 12import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
13import { resolveThread } from '../video-comments' 13import { resolveThread } from '../video-comments'
14import { getOrCreateAPVideo } from '../videos' 14import { getOrCreateAPVideo } from '../videos'
15 15
@@ -55,7 +55,7 @@ export {
55async function processCreateVideo (activity: ActivityCreate, notify: boolean) { 55async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
56 const videoToCreateData = activity.object as VideoObject 56 const videoToCreateData = activity.object as VideoObject
57 57
58 const syncParam = { likes: false, dislikes: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } 58 const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false }
59 const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) 59 const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam })
60 60
61 if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video) 61 if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video)
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index 1d2279df5..ac0e7e235 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -16,7 +16,7 @@ import {
16 MChannelActor, 16 MChannelActor,
17 MCommentOwnerVideo 17 MCommentOwnerVideo
18} from '../../../types/models' 18} from '../../../types/models'
19import { forwardVideoRelatedActivity } from '../send/utils' 19import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
20 20
21async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) { 21async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) {
22 const { activity, byActor } = options 22 const { activity, byActor } = options
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts
index 2f46b83d1..97a994e94 100644
--- a/server/lib/activitypub/process/process-dislike.ts
+++ b/server/lib/activitypub/process/process-dislike.ts
@@ -1,11 +1,11 @@
1import { VideoModel } from '@server/models/video/video'
1import { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models' 2import { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { sequelizeTypescript } from '../../../initializers/database' 4import { sequelizeTypescript } from '../../../initializers/database'
4import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 5import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
5import { APProcessorOptions } from '../../../types/activitypub-processor.model' 6import { APProcessorOptions } from '../../../types/activitypub-processor.model'
6import { MActorSignature } from '../../../types/models' 7import { MActorSignature } from '../../../types/models'
7import { forwardVideoRelatedActivity } from '../send/utils' 8import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
8import { getOrCreateAPVideo } from '../videos'
9 9
10async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { 10async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) {
11 const { activity, byActor } = options 11 const { activity, byActor } = options
@@ -29,16 +29,23 @@ async function processDislike (activity: ActivityCreate | ActivityDislike, byAct
29 29
30 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) 30 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
31 31
32 const { video } = await getOrCreateAPVideo({ videoObject: dislikeObject }) 32 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeObject, fetchType: 'only-video' })
33
34 // We don't care about dislikes of remote videos
35 if (!onlyVideo.isOwned()) return
33 36
34 return sequelizeTypescript.transaction(async t => { 37 return sequelizeTypescript.transaction(async t => {
38 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
39
35 const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) 40 const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
36 if (existingRate && existingRate.type === 'dislike') return 41 if (existingRate && existingRate.type === 'dislike') return
37 42
38 await video.increment('dislikes', { transaction: t }) 43 await video.increment('dislikes', { transaction: t })
44 video.dislikes++
39 45
40 if (existingRate && existingRate.type === 'like') { 46 if (existingRate && existingRate.type === 'like') {
41 await video.decrement('likes', { transaction: t }) 47 await video.decrement('likes', { transaction: t })
48 video.likes--
42 } 49 }
43 50
44 const rate = existingRate || new AccountVideoRateModel() 51 const rate = existingRate || new AccountVideoRateModel()
@@ -49,11 +56,6 @@ async function processDislike (activity: ActivityCreate | ActivityDislike, byAct
49 56
50 await rate.save({ transaction: t }) 57 await rate.save({ transaction: t })
51 58
52 if (video.isOwned()) { 59 await federateVideoIfNeeded(video, false, t)
53 // Don't resend the activity to the sender
54 const exceptions = [ byActor ]
55
56 await forwardVideoRelatedActivity(activity, t, exceptions, video)
57 }
58 }) 60 })
59} 61}
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
index cd4e86cbb..93afb5edf 100644
--- a/server/lib/activitypub/process/process-like.ts
+++ b/server/lib/activitypub/process/process-like.ts
@@ -1,3 +1,4 @@
1import { VideoModel } from '@server/models/video/video'
1import { ActivityLike } from '../../../../shared/models/activitypub' 2import { ActivityLike } from '../../../../shared/models/activitypub'
2import { getAPId } from '../../../helpers/activitypub' 3import { getAPId } from '../../../helpers/activitypub'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 4import { retryTransactionWrapper } from '../../../helpers/database-utils'
@@ -5,11 +6,11 @@ import { sequelizeTypescript } from '../../../initializers/database'
5import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 6import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
6import { APProcessorOptions } from '../../../types/activitypub-processor.model' 7import { APProcessorOptions } from '../../../types/activitypub-processor.model'
7import { MActorSignature } from '../../../types/models' 8import { MActorSignature } from '../../../types/models'
8import { forwardVideoRelatedActivity } from '../send/utils' 9import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
9import { getOrCreateAPVideo } from '../videos'
10 10
11async function processLikeActivity (options: APProcessorOptions<ActivityLike>) { 11async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
12 const { activity, byActor } = options 12 const { activity, byActor } = options
13
13 return retryTransactionWrapper(processLikeVideo, byActor, activity) 14 return retryTransactionWrapper(processLikeVideo, byActor, activity)
14} 15}
15 16
@@ -27,17 +28,24 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
27 const byAccount = byActor.Account 28 const byAccount = byActor.Account
28 if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) 29 if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
29 30
30 const { video } = await getOrCreateAPVideo({ videoObject: videoUrl }) 31 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video' })
32
33 // We don't care about likes of remote videos
34 if (!onlyVideo.isOwned()) return
31 35
32 return sequelizeTypescript.transaction(async t => { 36 return sequelizeTypescript.transaction(async t => {
37 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
38
33 const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) 39 const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
34 if (existingRate && existingRate.type === 'like') return 40 if (existingRate && existingRate.type === 'like') return
35 41
36 if (existingRate && existingRate.type === 'dislike') { 42 if (existingRate && existingRate.type === 'dislike') {
37 await video.decrement('dislikes', { transaction: t }) 43 await video.decrement('dislikes', { transaction: t })
44 video.dislikes--
38 } 45 }
39 46
40 await video.increment('likes', { transaction: t }) 47 await video.increment('likes', { transaction: t })
48 video.likes++
41 49
42 const rate = existingRate || new AccountVideoRateModel() 50 const rate = existingRate || new AccountVideoRateModel()
43 rate.type = 'like' 51 rate.type = 'like'
@@ -47,11 +55,6 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
47 55
48 await rate.save({ transaction: t }) 56 await rate.save({ transaction: t })
49 57
50 if (video.isOwned()) { 58 await federateVideoIfNeeded(video, false, t)
51 // Don't resend the activity to the sender
52 const exceptions = [ byActor ]
53
54 await forwardVideoRelatedActivity(activity, t, exceptions, video)
55 }
56 }) 59 })
57} 60}
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index d4b2a795f..257eb6c2b 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -1,3 +1,4 @@
1import { VideoModel } from '@server/models/video/video'
1import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub' 2import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub'
2import { DislikeObject } from '../../../../shared/models/activitypub/objects' 3import { DislikeObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 4import { retryTransactionWrapper } from '../../../helpers/database-utils'
@@ -10,8 +11,8 @@ import { VideoRedundancyModel } from '../../../models/redundancy/video-redundanc
10import { VideoShareModel } from '../../../models/video/video-share' 11import { VideoShareModel } from '../../../models/video/video-share'
11import { APProcessorOptions } from '../../../types/activitypub-processor.model' 12import { APProcessorOptions } from '../../../types/activitypub-processor.model'
12import { MActorSignature } from '../../../types/models' 13import { MActorSignature } from '../../../types/models'
13import { forwardVideoRelatedActivity } from '../send/utils' 14import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
14import { getOrCreateAPVideo } from '../videos' 15import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
15 16
16async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { 17async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) {
17 const { activity, byActor } = options 18 const { activity, byActor } = options
@@ -55,23 +56,22 @@ export {
55async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { 56async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) {
56 const likeActivity = activity.object as ActivityLike 57 const likeActivity = activity.object as ActivityLike
57 58
58 const { video } = await getOrCreateAPVideo({ videoObject: likeActivity.object }) 59 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object })
60 // We don't care about likes of remote videos
61 if (!onlyVideo.isOwned()) return
59 62
60 return sequelizeTypescript.transaction(async t => { 63 return sequelizeTypescript.transaction(async t => {
61 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) 64 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
62 65
66 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
63 const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, likeActivity.id, t) 67 const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, likeActivity.id, t)
64 if (!rate || rate.type !== 'like') throw new Error(`Unknown like by account ${byActor.Account.id} for video ${video.id}.`) 68 if (!rate || rate.type !== 'like') throw new Error(`Unknown like by account ${byActor.Account.id} for video ${video.id}.`)
65 69
66 await rate.destroy({ transaction: t }) 70 await rate.destroy({ transaction: t })
67 await video.decrement('likes', { transaction: t }) 71 await video.decrement('likes', { transaction: t })
68 72
69 if (video.isOwned()) { 73 video.likes--
70 // Don't resend the activity to the sender 74 await federateVideoIfNeeded(video, false, t)
71 const exceptions = [ byActor ]
72
73 await forwardVideoRelatedActivity(activity, t, exceptions, video)
74 }
75 }) 75 })
76} 76}
77 77
@@ -80,26 +80,27 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
80 ? activity.object 80 ? activity.object
81 : activity.object.object as DislikeObject 81 : activity.object.object as DislikeObject
82 82
83 const { video } = await getOrCreateAPVideo({ videoObject: dislike.object }) 83 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislike.object })
84 // We don't care about likes of remote videos
85 if (!onlyVideo.isOwned()) return
84 86
85 return sequelizeTypescript.transaction(async t => { 87 return sequelizeTypescript.transaction(async t => {
86 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) 88 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
87 89
90 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
88 const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislike.id, t) 91 const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislike.id, t)
89 if (!rate || rate.type !== 'dislike') throw new Error(`Unknown dislike by account ${byActor.Account.id} for video ${video.id}.`) 92 if (!rate || rate.type !== 'dislike') throw new Error(`Unknown dislike by account ${byActor.Account.id} for video ${video.id}.`)
90 93
91 await rate.destroy({ transaction: t }) 94 await rate.destroy({ transaction: t })
92 await video.decrement('dislikes', { transaction: t }) 95 await video.decrement('dislikes', { transaction: t })
96 video.dislikes--
93 97
94 if (video.isOwned()) { 98 await federateVideoIfNeeded(video, false, t)
95 // Don't resend the activity to the sender
96 const exceptions = [ byActor ]
97
98 await forwardVideoRelatedActivity(activity, t, exceptions, video)
99 }
100 }) 99 })
101} 100}
102 101
102// ---------------------------------------------------------------------------
103
103async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { 104async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) {
104 const cacheFileObject = activity.object.object as CacheFileObject 105 const cacheFileObject = activity.object.object as CacheFileObject
105 106
@@ -125,19 +126,6 @@ async function processUndoCacheFile (byActor: MActorSignature, activity: Activit
125 }) 126 })
126} 127}
127 128
128function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) {
129 return sequelizeTypescript.transaction(async t => {
130 const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t)
131 const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
132
133 if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${following.id}.`)
134
135 await actorFollow.destroy({ transaction: t })
136
137 return undefined
138 })
139}
140
141function processUndoAnnounce (byActor: MActorSignature, announceActivity: ActivityAnnounce) { 129function processUndoAnnounce (byActor: MActorSignature, announceActivity: ActivityAnnounce) {
142 return sequelizeTypescript.transaction(async t => { 130 return sequelizeTypescript.transaction(async t => {
143 const share = await VideoShareModel.loadByUrl(announceActivity.id, t) 131 const share = await VideoShareModel.loadByUrl(announceActivity.id, t)
@@ -155,3 +143,18 @@ function processUndoAnnounce (byActor: MActorSignature, announceActivity: Activi
155 } 143 }
156 }) 144 })
157} 145}
146
147// ---------------------------------------------------------------------------
148
149function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) {
150 return sequelizeTypescript.transaction(async t => {
151 const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t)
152 const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
153
154 if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${following.id}.`)
155
156 await actorFollow.destroy({ transaction: t })
157
158 return undefined
159 })
160}
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index f40008a6b..4afdbd430 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -13,7 +13,7 @@ import { MActorFull, MActorSignature } from '../../../types/models'
13import { APActorUpdater } from '../actors/updater' 13import { APActorUpdater } from '../actors/updater'
14import { createOrUpdateCacheFile } from '../cache-file' 14import { createOrUpdateCacheFile } from '../cache-file'
15import { createOrUpdateVideoPlaylist } from '../playlists' 15import { createOrUpdateVideoPlaylist } from '../playlists'
16import { forwardVideoRelatedActivity } from '../send/utils' 16import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
17import { APVideoUpdater, getOrCreateAPVideo } from '../videos' 17import { APVideoUpdater, getOrCreateAPVideo } from '../videos'
18 18
19async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 19async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts
index 720385f9b..c59940164 100644
--- a/server/lib/activitypub/process/process-view.ts
+++ b/server/lib/activitypub/process/process-view.ts
@@ -2,7 +2,7 @@ import { VideoViews } from '@server/lib/video-views'
2import { ActivityView } from '../../../../shared/models/activitypub' 2import { ActivityView } from '../../../../shared/models/activitypub'
3import { APProcessorOptions } from '../../../types/activitypub-processor.model' 3import { APProcessorOptions } from '../../../types/activitypub-processor.model'
4import { MActorSignature } from '../../../types/models' 4import { MActorSignature } from '../../../types/models'
5import { forwardVideoRelatedActivity } from '../send/utils' 5import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
6import { getOrCreateAPVideo } from '../videos' 6import { getOrCreateAPVideo } from '../videos'
7 7
8async function processViewActivity (options: APProcessorOptions<ActivityView>) { 8async function processViewActivity (options: APProcessorOptions<ActivityView>) {
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts
index bb387e2c0..939f06d9e 100644
--- a/server/lib/activitypub/send/send-accept.ts
+++ b/server/lib/activitypub/send/send-accept.ts
@@ -1,9 +1,9 @@
1import { ActivityAccept, ActivityFollow } from '../../../../shared/models/activitypub' 1import { ActivityAccept, ActivityFollow } from '@shared/models'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { MActor, MActorFollowActors } from '../../../types/models' 3import { MActor, MActorFollowActors } from '../../../types/models'
4import { getLocalActorFollowAcceptActivityPubUrl } from '../url' 4import { getLocalActorFollowAcceptActivityPubUrl } from '../url'
5import { buildFollowActivity } from './send-follow' 5import { buildFollowActivity } from './send-follow'
6import { unicastTo } from './utils' 6import { unicastTo } from './shared/send-utils'
7 7
8function sendAccept (actorFollow: MActorFollowActors) { 8function sendAccept (actorFollow: MActorFollowActors) {
9 const follower = actorFollow.ActorFollower 9 const follower = actorFollow.ActorFollower
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts
index 471dcfa77..7897beb75 100644
--- a/server/lib/activitypub/send/send-announce.ts
+++ b/server/lib/activitypub/send/send-announce.ts
@@ -1,10 +1,11 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAnnounce, ActivityAudience } from '../../../../shared/models/activitypub' 2import { ActivityAnnounce, ActivityAudience } from '@shared/models'
3import { broadcastToFollowers } from './utils'
4import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf } from '../audience'
5import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
6import { MActorLight, MVideo } from '../../../types/models' 4import { MActorLight, MVideo } from '../../../types/models'
7import { MVideoShare } from '../../../types/models/video' 5import { MVideoShare } from '../../../types/models/video'
6import { audiencify, getAudience } from '../audience'
7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf } from './shared'
8import { broadcastToFollowers } from './shared/send-utils'
8 9
9async function buildAnnounceWithVideoAudience ( 10async function buildAnnounceWithVideoAudience (
10 byActor: MActorLight, 11 byActor: MActorLight,
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index baded642a..f6d897220 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -1,11 +1,8 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub' 2import { getServerActor } from '@server/models/application/application'
3import { VideoPrivacy } from '../../../../shared/models/videos' 3import { ActivityAudience, ActivityCreate, ContextType, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
4import { VideoCommentModel } from '../../../models/video/video-comment'
5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
7import { logger, loggerTagsFactory } from '../../../helpers/logger' 4import { logger, loggerTagsFactory } from '../../../helpers/logger'
8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 5import { VideoCommentModel } from '../../../models/video/video-comment'
9import { 6import {
10 MActorLight, 7 MActorLight,
11 MCommentOwnerVideo, 8 MCommentOwnerVideo,
@@ -15,8 +12,16 @@ import {
15 MVideoRedundancyFileVideo, 12 MVideoRedundancyFileVideo,
16 MVideoRedundancyStreamingPlaylistVideo 13 MVideoRedundancyStreamingPlaylistVideo
17} from '../../../types/models' 14} from '../../../types/models'
18import { getServerActor } from '@server/models/application/application' 15import { audiencify, getAudience } from '../audience'
19import { ContextType } from '@shared/models/activitypub/context' 16import {
17 broadcastToActors,
18 broadcastToFollowers,
19 getActorsInvolvedInVideo,
20 getAudienceFromFollowersOf,
21 getVideoCommentAudience,
22 sendVideoRelatedActivity,
23 unicastTo
24} from './shared'
20 25
21const lTags = loggerTagsFactory('ap', 'create') 26const lTags = loggerTagsFactory('ap', 'create')
22 27
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts
index d31f8c10b..39216cdeb 100644
--- a/server/lib/activitypub/send/send-delete.ts
+++ b/server/lib/activitypub/send/send-delete.ts
@@ -1,15 +1,16 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { getServerActor } from '@server/models/application/application' 2import { getServerActor } from '@server/models/application/application'
3import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub' 3import { ActivityAudience, ActivityDelete } from '@shared/models'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { ActorModel } from '../../../models/actor/actor' 5import { ActorModel } from '../../../models/actor/actor'
6import { VideoCommentModel } from '../../../models/video/video-comment' 6import { VideoCommentModel } from '../../../models/video/video-comment'
7import { VideoShareModel } from '../../../models/video/video-share' 7import { VideoShareModel } from '../../../models/video/video-share'
8import { MActorUrl } from '../../../types/models' 8import { MActorUrl } from '../../../types/models'
9import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../types/models/video' 9import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../types/models/video'
10import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' 10import { audiencify } from '../audience'
11import { getDeleteActivityPubUrl } from '../url' 11import { getDeleteActivityPubUrl } from '../url'
12import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 12import { getActorsInvolvedInVideo, getVideoCommentAudience } from './shared'
13import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './shared/send-utils'
13 14
14async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) { 15async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
15 logger.info('Creating job to broadcast delete of video %s.', video.url) 16 logger.info('Creating job to broadcast delete of video %s.', video.url)
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts
index 274230535..ecb11e9bf 100644
--- a/server/lib/activitypub/send/send-dislike.ts
+++ b/server/lib/activitypub/send/send-dislike.ts
@@ -1,10 +1,10 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { getVideoDislikeActivityPubUrlByLocalActor } from '../url' 2import { ActivityAudience, ActivityDislike } from '@shared/models'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub'
5import { sendVideoRelatedActivity } from './utils'
6import { audiencify, getAudience } from '../audience'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models' 4import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models'
5import { audiencify, getAudience } from '../audience'
6import { getVideoDislikeActivityPubUrlByLocalActor } from '../url'
7import { sendVideoActivityToOrigin } from './shared/send-utils'
8 8
9function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 9function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
10 logger.info('Creating job to dislike %s.', video.url) 10 logger.info('Creating job to dislike %s.', video.url)
@@ -15,7 +15,7 @@ function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction
15 return buildDislikeActivity(url, byActor, video, audience) 15 return buildDislikeActivity(url, byActor, video, audience)
16 } 16 }
17 17
18 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) 18 return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction: t })
19} 19}
20 20
21function buildDislikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityDislike { 21function buildDislikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityDislike {
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts
index b0483b5a0..6df4e7eb8 100644
--- a/server/lib/activitypub/send/send-flag.ts
+++ b/server/lib/activitypub/send/send-flag.ts
@@ -1,10 +1,10 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityFlag } from '@shared/models'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { MAbuseAP, MAccountLight, MActor } from '../../../types/models' 4import { MAbuseAP, MAccountLight, MActor } from '../../../types/models'
5import { audiencify, getAudience } from '../audience' 5import { audiencify, getAudience } from '../audience'
6import { getLocalAbuseActivityPubUrl } from '../url' 6import { getLocalAbuseActivityPubUrl } from '../url'
7import { unicastTo } from './utils' 7import { unicastTo } from './shared/send-utils'
8 8
9function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) { 9function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) {
10 if (!flaggedAccount.Actor.serverId) return // Local user 10 if (!flaggedAccount.Actor.serverId) return // Local user
diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts
index 9219640dd..aeeb50a2a 100644
--- a/server/lib/activitypub/send/send-follow.ts
+++ b/server/lib/activitypub/send/send-follow.ts
@@ -1,8 +1,8 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityFollow } from '../../../../shared/models/activitypub' 2import { ActivityFollow } from '@shared/models'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { MActor, MActorFollowActors } from '../../../types/models' 4import { MActor, MActorFollowActors } from '../../../types/models'
5import { unicastTo } from './utils' 5import { unicastTo } from './shared/send-utils'
6 6
7function sendFollow (actorFollow: MActorFollowActors, t: Transaction) { 7function sendFollow (actorFollow: MActorFollowActors, t: Transaction) {
8 const me = actorFollow.ActorFollower 8 const me = actorFollow.ActorFollower
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
index ed6dfcf56..a5fe95e0a 100644
--- a/server/lib/activitypub/send/send-like.ts
+++ b/server/lib/activitypub/send/send-like.ts
@@ -1,10 +1,10 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityLike } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityLike } from '@shared/models'
3import { getVideoLikeActivityPubUrlByLocalActor } from '../url'
4import { sendVideoRelatedActivity } from './utils'
5import { audiencify, getAudience } from '../audience'
6import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models' 4import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models'
5import { audiencify, getAudience } from '../audience'
6import { getVideoLikeActivityPubUrlByLocalActor } from '../url'
7import { sendVideoActivityToOrigin } from './shared/send-utils'
8 8
9function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 9function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
10 logger.info('Creating job to like %s.', video.url) 10 logger.info('Creating job to like %s.', video.url)
@@ -15,7 +15,7 @@ function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
15 return buildLikeActivity(url, byActor, video, audience) 15 return buildLikeActivity(url, byActor, video, audience)
16 } 16 }
17 17
18 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) 18 return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction: t })
19} 19}
20 20
21function buildLikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityLike { 21function buildLikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityLike {
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts
index 8d74a7848..01b8f743b 100644
--- a/server/lib/activitypub/send/send-reject.ts
+++ b/server/lib/activitypub/send/send-reject.ts
@@ -1,9 +1,9 @@
1import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub' 1import { ActivityFollow, ActivityReject } from '@shared/models'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { MActor } from '../../../types/models' 3import { MActor } from '../../../types/models'
4import { getLocalActorFollowRejectActivityPubUrl } from '../url' 4import { getLocalActorFollowRejectActivityPubUrl } from '../url'
5import { buildFollowActivity } from './send-follow' 5import { buildFollowActivity } from './send-follow'
6import { unicastTo } from './utils' 6import { unicastTo } from './shared/send-utils'
7 7
8function sendReject (followUrl: string, follower: MActor, following: MActor) { 8function sendReject (followUrl: string, follower: MActor, following: MActor) {
9 if (!follower.serverId) { // This should never happen 9 if (!follower.serverId) { // This should never happen
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index d2b738bef..948ca0d7a 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -7,7 +7,7 @@ import {
7 ActivityFollow, 7 ActivityFollow,
8 ActivityLike, 8 ActivityLike,
9 ActivityUndo 9 ActivityUndo
10} from '../../../../shared/models/activitypub' 10} from '@shared/models'
11import { logger } from '../../../helpers/logger' 11import { logger } from '../../../helpers/logger'
12import { VideoModel } from '../../../models/video/video' 12import { VideoModel } from '../../../models/video/video'
13import { 13import {
@@ -27,7 +27,7 @@ import { buildCreateActivity } from './send-create'
27import { buildDislikeActivity } from './send-dislike' 27import { buildDislikeActivity } from './send-dislike'
28import { buildFollowActivity } from './send-follow' 28import { buildFollowActivity } from './send-follow'
29import { buildLikeActivity } from './send-like' 29import { buildLikeActivity } from './send-like'
30import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 30import { broadcastToFollowers, sendVideoActivityToOrigin, sendVideoRelatedActivity, unicastTo } from './shared/send-utils'
31 31
32function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) { 32function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
33 const me = actorFollow.ActorFollower 33 const me = actorFollow.ActorFollower
@@ -46,6 +46,8 @@ function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
46 t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl)) 46 t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl))
47} 47}
48 48
49// ---------------------------------------------------------------------------
50
49async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) { 51async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) {
50 logger.info('Creating job to undo announce %s.', videoShare.url) 52 logger.info('Creating job to undo announce %s.', videoShare.url)
51 53
@@ -58,13 +60,30 @@ async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare,
58 return broadcastToFollowers(undoActivity, byActor, actorsInvolvedInVideo, t, followersException) 60 return broadcastToFollowers(undoActivity, byActor, actorsInvolvedInVideo, t, followersException)
59} 61}
60 62
63async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, t: Transaction) {
64 logger.info('Creating job to undo cache file %s.', redundancyModel.url)
65
66 const associatedVideo = redundancyModel.getVideo()
67 if (!associatedVideo) {
68 logger.warn('Cannot send undo activity for redundancy %s: no video files associated.', redundancyModel.url)
69 return
70 }
71
72 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(associatedVideo.id)
73 const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject())
74
75 return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t })
76}
77
78// ---------------------------------------------------------------------------
79
61async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 80async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
62 logger.info('Creating job to undo a like of video %s.', video.url) 81 logger.info('Creating job to undo a like of video %s.', video.url)
63 82
64 const likeUrl = getVideoLikeActivityPubUrlByLocalActor(byActor, video) 83 const likeUrl = getVideoLikeActivityPubUrlByLocalActor(byActor, video)
65 const likeActivity = buildLikeActivity(likeUrl, byActor, video) 84 const likeActivity = buildLikeActivity(likeUrl, byActor, video)
66 85
67 return sendUndoVideoRelatedActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t }) 86 return sendUndoVideoToOriginActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t })
68} 87}
69 88
70async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 89async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
@@ -73,22 +92,7 @@ async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: T
73 const dislikeUrl = getVideoDislikeActivityPubUrlByLocalActor(byActor, video) 92 const dislikeUrl = getVideoDislikeActivityPubUrlByLocalActor(byActor, video)
74 const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video) 93 const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
75 94
76 return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t }) 95 return sendUndoVideoToOriginActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t })
77}
78
79async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, t: Transaction) {
80 logger.info('Creating job to undo cache file %s.', redundancyModel.url)
81
82 const associatedVideo = redundancyModel.getVideo()
83 if (!associatedVideo) {
84 logger.warn('Cannot send undo activity for redundancy %s: no video files associated.', redundancyModel.url)
85 return
86 }
87
88 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(associatedVideo.id)
89 const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject())
90
91 return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t })
92} 96}
93 97
94// --------------------------------------------------------------------------- 98// ---------------------------------------------------------------------------
@@ -126,7 +130,7 @@ async function sendUndoVideoRelatedActivity (options: {
126 byActor: MActor 130 byActor: MActor
127 video: MVideoAccountLight 131 video: MVideoAccountLight
128 url: string 132 url: string
129 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce 133 activity: ActivityFollow | ActivityCreate | ActivityAnnounce
130 transaction: Transaction 134 transaction: Transaction
131}) { 135}) {
132 const activityBuilder = (audience: ActivityAudience) => { 136 const activityBuilder = (audience: ActivityAudience) => {
@@ -137,3 +141,19 @@ async function sendUndoVideoRelatedActivity (options: {
137 141
138 return sendVideoRelatedActivity(activityBuilder, options) 142 return sendVideoRelatedActivity(activityBuilder, options)
139} 143}
144
145async function sendUndoVideoToOriginActivity (options: {
146 byActor: MActor
147 video: MVideoAccountLight
148 url: string
149 activity: ActivityLike | ActivityDislike
150 transaction: Transaction
151}) {
152 const activityBuilder = (audience: ActivityAudience) => {
153 const undoUrl = getUndoActivityPubUrl(options.url)
154
155 return undoActivityData(undoUrl, options.byActor, options.activity, audience)
156 }
157
158 return sendVideoActivityToOrigin(activityBuilder, options)
159}
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index bcf6e1569..7c9e72cbc 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -1,14 +1,10 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' 2import { getServerActor } from '@server/models/application/application'
3import { VideoPrivacy } from '../../../../shared/models/videos' 3import { ActivityAudience, ActivityUpdate, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
4import { logger } from '../../../helpers/logger'
4import { AccountModel } from '../../../models/account/account' 5import { AccountModel } from '../../../models/account/account'
5import { VideoModel } from '../../../models/video/video' 6import { VideoModel } from '../../../models/video/video'
6import { VideoShareModel } from '../../../models/video/video-share' 7import { VideoShareModel } from '../../../models/video/video-share'
7import { getUpdateActivityPubUrl } from '../url'
8import { broadcastToFollowers, sendVideoRelatedActivity } from './utils'
9import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience'
10import { logger } from '../../../helpers/logger'
11import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
12import { 8import {
13 MAccountDefault, 9 MAccountDefault,
14 MActor, 10 MActor,
@@ -19,7 +15,10 @@ import {
19 MVideoPlaylistFull, 15 MVideoPlaylistFull,
20 MVideoRedundancyVideo 16 MVideoRedundancyVideo
21} from '../../../types/models' 17} from '../../../types/models'
22import { getServerActor } from '@server/models/application/application' 18import { audiencify, getAudience } from '../audience'
19import { getUpdateActivityPubUrl } from '../url'
20import { getActorsInvolvedInVideo } from './shared'
21import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils'
23 22
24async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) { 23async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
25 const video = videoArg as MVideoAP 24 const video = videoArg as MVideoAP
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts
index b12583e26..1f97307b9 100644
--- a/server/lib/activitypub/send/send-view.ts
+++ b/server/lib/activitypub/send/send-view.ts
@@ -1,12 +1,12 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { VideoViews } from '@server/lib/video-views' 2import { VideoViews } from '@server/lib/video-views'
3import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models' 3import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models'
4import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub' 4import { ActivityAudience, ActivityView } from '@shared/models'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { ActorModel } from '../../../models/actor/actor' 6import { ActorModel } from '../../../models/actor/actor'
7import { audiencify, getAudience } from '../audience' 7import { audiencify, getAudience } from '../audience'
8import { getLocalVideoViewActivityPubUrl } from '../url' 8import { getLocalVideoViewActivityPubUrl } from '../url'
9import { sendVideoRelatedActivity } from './utils' 9import { sendVideoRelatedActivity } from './shared/send-utils'
10 10
11async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) { 11async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) {
12 logger.info('Creating job to send view of %s.', video.url) 12 logger.info('Creating job to send view of %s.', video.url)
diff --git a/server/lib/activitypub/send/shared/audience-utils.ts b/server/lib/activitypub/send/shared/audience-utils.ts
new file mode 100644
index 000000000..a5f64a08d
--- /dev/null
+++ b/server/lib/activitypub/send/shared/audience-utils.ts
@@ -0,0 +1,74 @@
1import { Transaction } from 'sequelize/dist'
2import { ACTIVITY_PUB } from '@server/initializers/constants'
3import { ActorModel } from '@server/models/actor/actor'
4import { VideoModel } from '@server/models/video/video'
5import { VideoShareModel } from '@server/models/video/video-share'
6import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '@server/types/models'
7import { ActivityAudience } from '@shared/models'
8
9function getOriginVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[] = []): ActivityAudience {
10 return {
11 to: [ accountActor.url ],
12 cc: actorsInvolvedInVideo.map(a => a.followersUrl)
13 }
14}
15
16function getVideoCommentAudience (
17 videoComment: MCommentOwnerVideo,
18 threadParentComments: MCommentOwner[],
19 actorsInvolvedInVideo: MActorFollowersUrl[],
20 isOrigin = false
21): ActivityAudience {
22 const to = [ ACTIVITY_PUB.PUBLIC ]
23 const cc: string[] = []
24
25 // Owner of the video we comment
26 if (isOrigin === false) {
27 cc.push(videoComment.Video.VideoChannel.Account.Actor.url)
28 }
29
30 // Followers of the poster
31 cc.push(videoComment.Account.Actor.followersUrl)
32
33 // Send to actors we reply to
34 for (const parentComment of threadParentComments) {
35 if (parentComment.isDeleted()) continue
36
37 cc.push(parentComment.Account.Actor.url)
38 }
39
40 return {
41 to,
42 cc: cc.concat(actorsInvolvedInVideo.map(a => a.followersUrl))
43 }
44}
45
46function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
47 return {
48 to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
49 cc: []
50 }
51}
52
53async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
54 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
55
56 const videoAll = video as VideoModel
57
58 const videoActor = videoAll.VideoChannel?.Account
59 ? videoAll.VideoChannel.Account.Actor
60 : await ActorModel.loadFromAccountByVideoId(video.id, t)
61
62 actors.push(videoActor)
63
64 return actors
65}
66
67// ---------------------------------------------------------------------------
68
69export {
70 getOriginVideoAudience,
71 getActorsInvolvedInVideo,
72 getAudienceFromFollowersOf,
73 getVideoCommentAudience
74}
diff --git a/server/lib/activitypub/send/shared/index.ts b/server/lib/activitypub/send/shared/index.ts
new file mode 100644
index 000000000..bda579115
--- /dev/null
+++ b/server/lib/activitypub/send/shared/index.ts
@@ -0,0 +1,2 @@
1export * from './audience-utils'
2export * from './send-utils'
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/shared/send-utils.ts
index 7729703b8..9e8f12fa8 100644
--- a/server/lib/activitypub/send/utils.ts
+++ b/server/lib/activitypub/send/shared/send-utils.ts
@@ -1,15 +1,15 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache' 2import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache'
3import { getServerActor } from '@server/models/application/application' 3import { getServerActor } from '@server/models/application/application'
4import { Activity, ActivityAudience } from '@shared/models'
4import { ContextType } from '@shared/models/activitypub/context' 5import { ContextType } from '@shared/models/activitypub/context'
5import { Activity, ActivityAudience } from '../../../../shared/models/activitypub' 6import { afterCommitIfTransaction } from '../../../../helpers/database-utils'
6import { afterCommitIfTransaction } from '../../../helpers/database-utils' 7import { logger } from '../../../../helpers/logger'
7import { logger } from '../../../helpers/logger' 8import { ActorModel } from '../../../../models/actor/actor'
8import { ActorModel } from '../../../models/actor/actor' 9import { ActorFollowModel } from '../../../../models/actor/actor-follow'
9import { ActorFollowModel } from '../../../models/actor/actor-follow' 10import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../../types/models'
10import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models' 11import { JobQueue } from '../../../job-queue'
11import { JobQueue } from '../../job-queue' 12import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getOriginVideoAudience } from './audience-utils'
12import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
13 13
14async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { 14async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
15 byActor: MActorLight 15 byActor: MActorLight
@@ -23,16 +23,7 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
23 23
24 // Send to origin 24 // Send to origin
25 if (video.isOwned() === false) { 25 if (video.isOwned() === false) {
26 let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor 26 return sendVideoActivityToOrigin(activityBuilder, options)
27
28 if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)
29
30 const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo)
31 const activity = activityBuilder(audience)
32
33 return afterCommitIfTransaction(transaction, () => {
34 return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
35 })
36 } 27 }
37 28
38 // Send to followers 29 // Send to followers
@@ -44,6 +35,30 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
44 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType) 35 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType)
45} 36}
46 37
38async function sendVideoActivityToOrigin (activityBuilder: (audience: ActivityAudience) => Activity, options: {
39 byActor: MActorLight
40 video: MVideoImmutable | MVideoAccountLight
41 actorsInvolvedInVideo?: MActorLight[]
42 transaction?: Transaction
43 contextType?: ContextType
44}) {
45 const { byActor, video, actorsInvolvedInVideo, transaction, contextType } = options
46
47 if (video.isOwned()) throw new Error('Cannot send activity to owned video origin ' + video.url)
48
49 let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor
50 if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)
51
52 const audience = getOriginVideoAudience(accountActor, actorsInvolvedInVideo)
53 const activity = activityBuilder(audience)
54
55 return afterCommitIfTransaction(transaction, () => {
56 return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
57 })
58}
59
60// ---------------------------------------------------------------------------
61
47async function forwardVideoRelatedActivity ( 62async function forwardVideoRelatedActivity (
48 activity: Activity, 63 activity: Activity,
49 t: Transaction, 64 t: Transaction,
@@ -92,6 +107,8 @@ async function forwardActivity (
92 return afterCommitIfTransaction(t, () => JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })) 107 return afterCommitIfTransaction(t, () => JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }))
93} 108}
94 109
110// ---------------------------------------------------------------------------
111
95async function broadcastToFollowers ( 112async function broadcastToFollowers (
96 data: any, 113 data: any,
97 byActor: MActorId, 114 byActor: MActorId,
@@ -177,6 +194,7 @@ export {
177 unicastTo, 194 unicastTo,
178 forwardActivity, 195 forwardActivity,
179 broadcastToActors, 196 broadcastToActors,
197 sendVideoActivityToOrigin,
180 forwardVideoRelatedActivity, 198 forwardVideoRelatedActivity,
181 sendVideoRelatedActivity 199 sendVideoRelatedActivity
182} 200}
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index 2a14790fe..2c7da3e00 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -87,7 +87,7 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) {
87 87
88 // Maybe it's a reply to a video? 88 // Maybe it's a reply to a video?
89 // If yes, it's done: we resolved all the thread 89 // If yes, it's done: we resolved all the thread
90 const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false } 90 const syncParam = { rates: true, shares: true, comments: false, thumbnail: true, refreshVideo: false }
91 const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam }) 91 const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
92 92
93 if (video.isOwned() && !video.hasPrivacyForFederation()) { 93 if (video.isOwned() && !video.hasPrivacyForFederation()) {
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 04aa5eae9..2e7920f4e 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -1,49 +1,21 @@
1import { map } from 'bluebird'
2import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
3import { doJSONRequest } from '@server/helpers/requests'
4import { VideoRateType } from '../../../shared/models/videos' 2import { VideoRateType } from '../../../shared/models/videos'
5import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 3import { MAccountActor, MActorUrl, MVideoAccountLight, MVideoFullLight, MVideoId } from '../../types/models'
6import { logger, loggerTagsFactory } from '../../helpers/logger'
7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
9import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
10import { getOrCreateAPActor } from './actors'
11import { sendLike, sendUndoDislike, sendUndoLike } from './send' 4import { sendLike, sendUndoDislike, sendUndoLike } from './send'
12import { sendDislike } from './send/send-dislike' 5import { sendDislike } from './send/send-dislike'
13import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' 6import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
14 7import { federateVideoIfNeeded } from './videos'
15const lTags = loggerTagsFactory('ap', 'video-rate', 'create')
16
17async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
18 await map(ratesUrl, async rateUrl => {
19 try {
20 await createRate(rateUrl, video, rate)
21 } catch (err) {
22 logger.info('Cannot add rate %s.', rateUrl, { err, ...lTags(rateUrl, video.uuid, video.url) })
23 }
24 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
25}
26 8
27async function sendVideoRateChange ( 9async function sendVideoRateChange (
28 account: MAccountActor, 10 account: MAccountActor,
29 video: MVideoAccountLight, 11 video: MVideoFullLight,
30 likes: number, 12 likes: number,
31 dislikes: number, 13 dislikes: number,
32 t: Transaction 14 t: Transaction
33) { 15) {
34 const actor = account.Actor 16 if (video.isOwned()) return federateVideoIfNeeded(video, false, t)
35 17
36 // Keep the order: first we undo and then we create 18 return sendVideoRateChangeToOrigin(account, video, likes, dislikes, t)
37
38 // Undo Like
39 if (likes < 0) await sendUndoLike(actor, video, t)
40 // Undo Dislike
41 if (dislikes < 0) await sendUndoDislike(actor, video, t)
42
43 // Like
44 if (likes > 0) await sendLike(actor, video, t)
45 // Dislike
46 if (dislikes > 0) await sendDislike(actor, video, t)
47} 19}
48 20
49function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) { 21function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) {
@@ -56,35 +28,32 @@ function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVid
56 28
57export { 29export {
58 getLocalRateUrl, 30 getLocalRateUrl,
59 createRates,
60 sendVideoRateChange 31 sendVideoRateChange
61} 32}
62 33
63// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
64 35
65async function createRate (rateUrl: string, video: MVideo, rate: VideoRateType) { 36async function sendVideoRateChangeToOrigin (
66 // Fetch url 37 account: MAccountActor,
67 const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true }) 38 video: MVideoAccountLight,
68 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 39 likes: number,
69 40 dislikes: number,
70 const actorUrl = getAPId(body.actor) 41 t: Transaction
71 if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { 42) {
72 throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) 43 // Local video, we don't need to send like
73 } 44 if (video.isOwned()) return
74 45
75 if (checkUrlsSameHost(body.id, rateUrl) !== true) { 46 const actor = account.Actor
76 throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`)
77 }
78 47
79 const actor = await getOrCreateAPActor(actorUrl) 48 // Keep the order: first we undo and then we create
80 49
81 const entry = { 50 // Undo Like
82 videoId: video.id, 51 if (likes < 0) await sendUndoLike(actor, video, t)
83 accountId: actor.Account.id, 52 // Undo Dislike
84 type: rate, 53 if (dislikes < 0) await sendUndoDislike(actor, video, t)
85 url: body.id
86 }
87 54
88 // Video "likes"/"dislikes" will be updated by the caller 55 // Like
89 await AccountVideoRateModel.upsert(entry) 56 if (likes > 0) await sendLike(actor, video, t)
57 // Dislike
58 if (dislikes > 0) await sendDislike(actor, video, t)
90} 59}
diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts
index f3e2f0625..b13c6ceeb 100644
--- a/server/lib/activitypub/videos/get.ts
+++ b/server/lib/activitypub/videos/get.ts
@@ -42,7 +42,7 @@ async function getOrCreateAPVideo (
42 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther 42 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
43): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> { 43): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
44 // Default params 44 // Default params
45 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 45 const syncParam = options.syncParam || { rates: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
46 const fetchType = options.fetchType || 'all' 46 const fetchType = options.fetchType || 'all'
47 const allowRefresh = options.allowRefresh !== false 47 const allowRefresh = options.allowRefresh !== false
48 48
diff --git a/server/lib/activitypub/videos/shared/video-sync-attributes.ts b/server/lib/activitypub/videos/shared/video-sync-attributes.ts
index c4e101005..8cf0c87a6 100644
--- a/server/lib/activitypub/videos/shared/video-sync-attributes.ts
+++ b/server/lib/activitypub/videos/shared/video-sync-attributes.ts
@@ -1,20 +1,20 @@
1import { runInReadCommittedTransaction } from '@server/helpers/database-utils'
1import { logger, loggerTagsFactory } from '@server/helpers/logger' 2import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { doJSONRequest } from '@server/helpers/requests'
2import { JobQueue } from '@server/lib/job-queue' 4import { JobQueue } from '@server/lib/job-queue'
3import { AccountVideoRateModel } from '@server/models/account/account-video-rate' 5import { VideoModel } from '@server/models/video/video'
4import { VideoCommentModel } from '@server/models/video/video-comment' 6import { VideoCommentModel } from '@server/models/video/video-comment'
5import { VideoShareModel } from '@server/models/video/video-share' 7import { VideoShareModel } from '@server/models/video/video-share'
6import { MVideo } from '@server/types/models' 8import { MVideo } from '@server/types/models'
7import { ActivitypubHttpFetcherPayload, VideoObject } from '@shared/models' 9import { ActivitypubHttpFetcherPayload, ActivityPubOrderedCollection, VideoObject } from '@shared/models'
8import { crawlCollectionPage } from '../../crawl' 10import { crawlCollectionPage } from '../../crawl'
9import { addVideoShares } from '../../share' 11import { addVideoShares } from '../../share'
10import { addVideoComments } from '../../video-comments' 12import { addVideoComments } from '../../video-comments'
11import { createRates } from '../../video-rates'
12 13
13const lTags = loggerTagsFactory('ap', 'video') 14const lTags = loggerTagsFactory('ap', 'video')
14 15
15type SyncParam = { 16type SyncParam = {
16 likes: boolean 17 rates: boolean
17 dislikes: boolean
18 shares: boolean 18 shares: boolean
19 comments: boolean 19 comments: boolean
20 thumbnail: boolean 20 thumbnail: boolean
@@ -24,45 +24,57 @@ type SyncParam = {
24async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoObject, syncParam: SyncParam) { 24async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoObject, syncParam: SyncParam) {
25 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) 25 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
26 26
27 await syncRates('like', video, fetchedVideo, syncParam.likes) 27 const ratePromise = updateVideoRates(video, fetchedVideo)
28 await syncRates('dislike', video, fetchedVideo, syncParam.dislikes) 28 if (syncParam.rates) await ratePromise
29 29
30 await syncShares(video, fetchedVideo, syncParam.shares) 30 await syncShares(video, fetchedVideo, syncParam.shares)
31 31
32 await syncComments(video, fetchedVideo, syncParam.comments) 32 await syncComments(video, fetchedVideo, syncParam.comments)
33} 33}
34 34
35async function updateVideoRates (video: MVideo, fetchedVideo: VideoObject) {
36 const [ likes, dislikes ] = await Promise.all([
37 getRatesCount('like', video, fetchedVideo),
38 getRatesCount('dislike', video, fetchedVideo)
39 ])
40
41 return runInReadCommittedTransaction(async t => {
42 await VideoModel.updateRatesOf(video.id, 'like', likes, t)
43 await VideoModel.updateRatesOf(video.id, 'dislike', dislikes, t)
44 })
45}
46
35// --------------------------------------------------------------------------- 47// ---------------------------------------------------------------------------
36 48
37export { 49export {
38 SyncParam, 50 SyncParam,
39 syncVideoExternalAttributes 51 syncVideoExternalAttributes,
52 updateVideoRates
40} 53}
41 54
42// --------------------------------------------------------------------------- 55// ---------------------------------------------------------------------------
43 56
44function createJob (payload: ActivitypubHttpFetcherPayload) { 57async function getRatesCount (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject) {
45 return JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
46}
47
48function syncRates (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {
49 const uri = type === 'like' 58 const uri = type === 'like'
50 ? fetchedVideo.likes 59 ? fetchedVideo.likes
51 : fetchedVideo.dislikes 60 : fetchedVideo.dislikes
52 61
53 if (!isSync) { 62 logger.info('Sync %s of video %s', type, video.url)
54 const jobType = type === 'like' 63 const options = { activityPub: true }
55 ? 'video-likes' 64
56 : 'video-dislikes' 65 const response = await doJSONRequest<ActivityPubOrderedCollection<any>>(uri, options)
66 const totalItems = response.body.totalItems
57 67
58 return createJob({ uri, videoId: video.id, type: jobType }) 68 if (isNaN(totalItems)) {
69 logger.error('Cannot sync %s of video %s, totalItems is not a number', type, video.url, { body: response.body })
70 return
59 } 71 }
60 72
61 const handler = items => createRates(items, video, type) 73 return totalItems
62 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, type, crawlStartDate) 74}
63 75
64 return crawlCollectionPage<string>(uri, handler, cleaner) 76function createJob (payload: ActivitypubHttpFetcherPayload) {
65 .catch(err => logger.error('Cannot add rate of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) })) 77 return JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
66} 78}
67 79
68function syncShares (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) { 80function syncShares (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {
diff --git a/server/lib/activitypub/videos/updater.ts b/server/lib/activitypub/videos/updater.ts
index f786bb196..32cbf7e07 100644
--- a/server/lib/activitypub/videos/updater.ts
+++ b/server/lib/activitypub/videos/updater.ts
@@ -7,7 +7,7 @@ import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
7import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
8import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models' 8import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models'
9import { VideoObject, VideoPrivacy } from '@shared/models' 9import { VideoObject, VideoPrivacy } from '@shared/models'
10import { APVideoAbstractBuilder, getVideoAttributesFromObject } from './shared' 10import { APVideoAbstractBuilder, getVideoAttributesFromObject, updateVideoRates } from './shared'
11 11
12export class APVideoUpdater extends APVideoAbstractBuilder { 12export class APVideoUpdater extends APVideoAbstractBuilder {
13 private readonly wasPrivateVideo: boolean 13 private readonly wasPrivateVideo: boolean
@@ -74,6 +74,8 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
74 transaction: undefined 74 transaction: undefined
75 }) 75 })
76 76
77 await updateVideoRates(videoUpdated, this.videoObject)
78
77 // Notify our users? 79 // Notify our users?
78 if (this.wasPrivateVideo || this.wasUnlistedVideo) { 80 if (this.wasPrivateVideo || this.wasUnlistedVideo) {
79 Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) 81 Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated)
diff --git a/server/lib/job-queue/handlers/activitypub-cleaner.ts b/server/lib/job-queue/handlers/activitypub-cleaner.ts
index 509dd1cb5..07dd908cd 100644
--- a/server/lib/job-queue/handlers/activitypub-cleaner.ts
+++ b/server/lib/job-queue/handlers/activitypub-cleaner.ts
@@ -34,7 +34,7 @@ async function processActivityPubCleaner (_job: Job) {
34 if (result?.status === 'deleted') { 34 if (result?.status === 'deleted') {
35 const { videoId, type } = result.data 35 const { videoId, type } = result.data
36 36
37 await VideoModel.updateRatesOf(videoId, type, undefined) 37 await VideoModel.syncLocalRates(videoId, type, undefined)
38 } 38 }
39 }, { concurrency: AP_CLEANER.CONCURRENCY }) 39 }, { concurrency: AP_CLEANER.CONCURRENCY })
40 } 40 }
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
index 46016a0a7..128e14f94 100644
--- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
@@ -1,7 +1,6 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models' 2import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
5import { VideoModel } from '../../../models/video/video' 4import { VideoModel } from '../../../models/video/video'
6import { VideoCommentModel } from '../../../models/video/video-comment' 5import { VideoCommentModel } from '../../../models/video/video-comment'
7import { VideoShareModel } from '../../../models/video/video-share' 6import { VideoShareModel } from '../../../models/video/video-share'
@@ -11,7 +10,6 @@ import { createAccountPlaylists } from '../../activitypub/playlists'
11import { processActivities } from '../../activitypub/process' 10import { processActivities } from '../../activitypub/process'
12import { addVideoShares } from '../../activitypub/share' 11import { addVideoShares } from '../../activitypub/share'
13import { addVideoComments } from '../../activitypub/video-comments' 12import { addVideoComments } from '../../activitypub/video-comments'
14import { createRates } from '../../activitypub/video-rates'
15 13
16async function processActivityPubHttpFetcher (job: Job) { 14async function processActivityPubHttpFetcher (job: Job) {
17 logger.info('Processing ActivityPub fetcher in job %d.', job.id) 15 logger.info('Processing ActivityPub fetcher in job %d.', job.id)
@@ -23,16 +21,12 @@ async function processActivityPubHttpFetcher (job: Job) {
23 21
24 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { 22 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
25 'activity': items => processActivities(items, { outboxUrl: payload.uri, fromFetch: true }), 23 'activity': items => processActivities(items, { outboxUrl: payload.uri, fromFetch: true }),
26 'video-likes': items => createRates(items, video, 'like'),
27 'video-dislikes': items => createRates(items, video, 'dislike'),
28 'video-shares': items => addVideoShares(items, video), 24 'video-shares': items => addVideoShares(items, video),
29 'video-comments': items => addVideoComments(items), 25 'video-comments': items => addVideoComments(items),
30 'account-playlists': items => createAccountPlaylists(items) 26 'account-playlists': items => createAccountPlaylists(items)
31 } 27 }
32 28
33 const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Promise<any> } = { 29 const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Promise<any> } = {
34 'video-likes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate),
35 'video-dislikes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate),
36 'video-shares': crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate), 30 'video-shares': crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate),
37 'video-comments': crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate) 31 'video-comments': crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
38 } 32 }
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts
index 5037992d2..92ceed180 100644
--- a/server/lib/job-queue/handlers/activitypub-refresher.ts
+++ b/server/lib/job-queue/handlers/activitypub-refresher.ts
@@ -28,7 +28,7 @@ export {
28 28
29async function refreshVideo (videoUrl: string) { 29async function refreshVideo (videoUrl: string) {
30 const fetchType = 'all' as 'all' 30 const fetchType = 'all' as 'all'
31 const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true } 31 const syncParam = { rates: true, shares: true, comments: true, thumbnail: true }
32 32
33 const videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType) 33 const videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType)
34 if (videoFromDatabase) { 34 if (videoFromDatabase) {
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index 16562ad0b..91c217615 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -352,7 +352,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
352 // We need more attributes and check if the video still exists 352 // We need more attributes and check if the video still exists
353 const getVideoOptions = { 353 const getVideoOptions = {
354 videoObject: videoUrl, 354 videoObject: videoUrl,
355 syncParam: { likes: false, dislikes: false, shares: false, comments: false, thumbnail: false, refreshVideo: true }, 355 syncParam: { rates: false, shares: false, comments: false, thumbnail: false, refreshVideo: true },
356 fetchType: 'all' as 'all' 356 fetchType: 'all' as 'all'
357 } 357 }
358 const { video } = await getOrCreateAPVideo(getVideoOptions) 358 const { video } = await getOrCreateAPVideo(getVideoOptions)
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
index 7303651eb..5c7d9cfc0 100644
--- a/server/models/account/account-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -12,7 +12,7 @@ import { AttributesOnly } from '@shared/typescript-utils'
12import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 12import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
13import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' 13import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants'
14import { ActorModel } from '../actor/actor' 14import { ActorModel } from '../actor/actor'
15import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' 15import { getSort, throwIfNotValid } from '../utils'
16import { VideoModel } from '../video/video' 16import { VideoModel } from '../video/video'
17import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' 17import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
18import { AccountModel } from './account' 18import { AccountModel } from './account'
@@ -249,28 +249,6 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV
249 ]).then(([ total, data ]) => ({ total, data })) 249 ]).then(([ total, data ]) => ({ total, data }))
250 } 250 }
251 251
252 static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) {
253 return AccountVideoRateModel.sequelize.transaction(async t => {
254 const query = {
255 where: {
256 updatedAt: {
257 [Op.lt]: beforeUpdatedAt
258 },
259 videoId,
260 type,
261 accountId: {
262 [Op.notIn]: buildLocalAccountIdsIn()
263 }
264 },
265 transaction: t
266 }
267
268 await AccountVideoRateModel.destroy(query)
269
270 return VideoModel.updateRatesOf(videoId, type, t)
271 })
272 }
273
274 toFormattedJSON (this: MAccountVideoRateFormattable): AccountVideoRate { 252 toFormattedJSON (this: MAccountVideoRateFormattable): AccountVideoRate {
275 return { 253 return {
276 video: this.Video.toFormattedJSON(), 254 video: this.Video.toFormattedJSON(),
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 4147b3d62..8bad2a01e 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1402,7 +1402,21 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1402 }) 1402 })
1403 } 1403 }
1404 1404
1405 static updateRatesOf (videoId: number, type: VideoRateType, t: Transaction) { 1405 static updateRatesOf (videoId: number, type: VideoRateType, count: number, t: Transaction) {
1406 const field = type === 'like'
1407 ? 'likes'
1408 : 'dislikes'
1409
1410 const rawQuery = `UPDATE "video" SET "${field}" = :count WHERE "video"."id" = :videoId`
1411
1412 return AccountVideoRateModel.sequelize.query(rawQuery, {
1413 transaction: t,
1414 replacements: { videoId, rateType: type, count },
1415 type: QueryTypes.UPDATE
1416 })
1417 }
1418
1419 static syncLocalRates (videoId: number, type: VideoRateType, t: Transaction) {
1406 const field = type === 'like' 1420 const field = type === 'like'
1407 ? 'likes' 1421 ? 'likes'
1408 : 'dislikes' 1422 : 'dislikes'
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index 05ccee8ad..a9df262dc 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -606,8 +606,8 @@ describe('Test multiple servers', function () {
606 606
607 for (const baseVideo of baseVideos) { 607 for (const baseVideo of baseVideos) {
608 const sameVideo = data.find(video => video.name === baseVideo.name) 608 const sameVideo = data.find(video => video.name === baseVideo.name)
609 expect(baseVideo.likes).to.equal(sameVideo.likes) 609 expect(baseVideo.likes).to.equal(sameVideo.likes, `Likes of ${sameVideo.uuid} do not correspond`)
610 expect(baseVideo.dislikes).to.equal(sameVideo.dislikes) 610 expect(baseVideo.dislikes).to.equal(sameVideo.dislikes, `Dislikes of ${sameVideo.uuid} do not correspond`)
611 } 611 }
612 } 612 }
613 }) 613 })