From 57e4e1c1a95c3a81a967f54ecc2a510d8b0e129c Mon Sep 17 00:00:00 2001
From: Chocobozzz <me@florianbigard.com>
Date: Fri, 18 Mar 2022 11:17:35 +0100
Subject: [PATCH] Don't store remote rates of remote videos

In the future we'll stop to expose all available rates to improve users
privacy
---
 scripts/ci.sh                                 | 15 +--
 server/controllers/activitypub/client.ts      |  2 +
 .../controllers/api/search/search-videos.ts   |  3 +-
 server/helpers/activitypub.ts                 |  4 +-
 server/initializers/constants.ts              |  2 +-
 .../migrations/0695-remove-remote-rates.ts    | 28 ++++++
 server/lib/activitypub/audience.ts            | 70 +-------------
 .../activitypub/process/process-announce.ts   |  2 +-
 .../lib/activitypub/process/process-create.ts |  4 +-
 .../lib/activitypub/process/process-delete.ts |  2 +-
 .../activitypub/process/process-dislike.ts    | 20 ++--
 .../lib/activitypub/process/process-like.ts   | 21 ++--
 .../lib/activitypub/process/process-undo.ts   | 61 ++++++------
 .../lib/activitypub/process/process-update.ts |  2 +-
 .../lib/activitypub/process/process-view.ts   |  2 +-
 server/lib/activitypub/send/send-accept.ts    |  4 +-
 server/lib/activitypub/send/send-announce.ts  |  7 +-
 server/lib/activitypub/send/send-create.ts    | 21 ++--
 server/lib/activitypub/send/send-delete.ts    |  7 +-
 server/lib/activitypub/send/send-dislike.ts   | 10 +-
 server/lib/activitypub/send/send-flag.ts      |  4 +-
 server/lib/activitypub/send/send-follow.ts    |  4 +-
 server/lib/activitypub/send/send-like.ts      | 10 +-
 server/lib/activitypub/send/send-reject.ts    |  4 +-
 server/lib/activitypub/send/send-undo.ts      | 62 ++++++++----
 server/lib/activitypub/send/send-update.ts    | 15 ++-
 server/lib/activitypub/send/send-view.ts      |  4 +-
 .../activitypub/send/shared/audience-utils.ts | 74 +++++++++++++++
 server/lib/activitypub/send/shared/index.ts   |  2 +
 .../send/{utils.ts => shared/send-utils.ts}   | 54 +++++++----
 server/lib/activitypub/video-comments.ts      |  2 +-
 server/lib/activitypub/video-rates.ts         | 95 +++++++------------
 server/lib/activitypub/videos/get.ts          |  2 +-
 .../videos/shared/video-sync-attributes.ts    | 56 ++++++-----
 server/lib/activitypub/videos/updater.ts      |  4 +-
 .../job-queue/handlers/activitypub-cleaner.ts |  2 +-
 .../handlers/activitypub-http-fetcher.ts      |  6 --
 .../handlers/activitypub-refresher.ts         |  2 +-
 .../schedulers/videos-redundancy-scheduler.ts |  2 +-
 server/models/account/account-video-rate.ts   | 24 +----
 server/models/video/video.ts                  | 16 +++-
 server/tests/api/videos/multiple-servers.ts   |  4 +-
 shared/models/activitypub/index.ts            |  3 +-
 shared/models/server/job.model.ts             |  2 +-
 44 files changed, 402 insertions(+), 338 deletions(-)
 create mode 100644 server/initializers/migrations/0695-remove-remote-rates.ts
 create mode 100644 server/lib/activitypub/send/shared/audience-utils.ts
 create mode 100644 server/lib/activitypub/send/shared/index.ts
 rename server/lib/activitypub/send/{utils.ts => shared/send-utils.ts} (78%)

diff --git a/scripts/ci.sh b/scripts/ci.sh
index 3d23f0297..a45f91a6b 100755
--- a/scripts/ci.sh
+++ b/scripts/ci.sh
@@ -8,6 +8,7 @@ if [ $# -eq 0 ]; then
 fi
 
 retries=3
+speedFactor="${2:-1}"
 
 runTest () {
     jobname=$1
@@ -53,7 +54,7 @@ elif [ "$1" = "client" ]; then
     # Not in their own task, they need an index.html
     pluginFiles="./dist/server/tests/plugins/html-injection.js ./dist/server/tests/api/server/plugins.js"
 
-    MOCHA_PARALLEL=true runTest "$1" 2 $feedsFiles $helperFiles $miscFiles $pluginFiles $libFiles
+    MOCHA_PARALLEL=true runTest "$1" $((2*$speedFactor)) $feedsFiles $helperFiles $miscFiles $pluginFiles $libFiles
 elif [ "$1" = "cli-plugin" ]; then
     npm run build:server
     npm run setup:cli
@@ -61,7 +62,7 @@ elif [ "$1" = "cli-plugin" ]; then
     pluginsFiles=$(findTestFiles ./dist/server/tests/plugins html-injection.js)
     cliFiles=$(findTestFiles ./dist/server/tests/cli)
 
-    MOCHA_PARALLEL=true runTest "$1" 2 $pluginsFiles
+    MOCHA_PARALLEL=true runTest "$1" $((2*$speedFactor)) $pluginsFiles
     runTest "$1" 1 $cliFiles
 elif [ "$1" = "api-1" ]; then
     npm run build:server
@@ -70,7 +71,7 @@ elif [ "$1" = "api-1" ]; then
     notificationsFiles=$(findTestFiles ./dist/server/tests/api/notifications)
     searchFiles=$(findTestFiles ./dist/server/tests/api/search)
 
-    MOCHA_PARALLEL=true runTest "$1" 3 $notificationsFiles $searchFiles $checkParamFiles
+    MOCHA_PARALLEL=true runTest "$1" $((3*$speedFactor)) $notificationsFiles $searchFiles $checkParamFiles
 elif [ "$1" = "api-2" ]; then
     npm run build:server
 
@@ -78,13 +79,13 @@ elif [ "$1" = "api-2" ]; then
     serverFiles=$(findTestFiles ./dist/server/tests/api/server plugins.js)
     usersFiles=$(findTestFiles ./dist/server/tests/api/users)
 
-    MOCHA_PARALLEL=true runTest "$1" 3 $liveFiles $serverFiles $usersFiles
+    MOCHA_PARALLEL=true runTest "$1" $((3*$speedFactor)) $liveFiles $serverFiles $usersFiles
 elif [ "$1" = "api-3" ]; then
     npm run build:server
 
     videosFiles=$(findTestFiles ./dist/server/tests/api/videos)
 
-    MOCHA_PARALLEL=true runTest "$1" 3 $videosFiles
+    MOCHA_PARALLEL=true runTest "$1" $((3*$speedFactor)) $videosFiles
 elif [ "$1" = "api-4" ]; then
     npm run build:server
 
@@ -93,13 +94,13 @@ elif [ "$1" = "api-4" ]; then
     objectStorageFiles=$(findTestFiles ./dist/server/tests/api/object-storage)
     activitypubFiles=$(findTestFiles ./dist/server/tests/api/activitypub)
 
-    MOCHA_PARALLEL=true runTest "$1" 2 $moderationFiles $redundancyFiles $activitypubFiles $objectStorageFiles
+    MOCHA_PARALLEL=true runTest "$1" $((2*$speedFactor)) $moderationFiles $redundancyFiles $activitypubFiles $objectStorageFiles
 elif [ "$1" = "api-5" ]; then
     npm run build:server
 
     transcodingFiles=$(findTestFiles ./dist/server/tests/api/transcoding)
 
-    MOCHA_PARALLEL=true runTest "$1" 2 $transcodingFiles
+    MOCHA_PARALLEL=true runTest "$1" $((2*$speedFactor)) $transcodingFiles
 elif [ "$1" = "external-plugins" ]; then
     npm run build:server
 
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',
 )
 activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
   executeIfActivityPub,
+  cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
   asyncMiddleware(getAccountVideoRateValidatorFactory('like')),
   getAccountVideoRateFactory('like')
 )
 activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
   executeIfActivityPub,
+  cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
   asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')),
   getAccountVideoRateFactory('dislike')
 )
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) {
   if (isUserAbleToSearchRemoteURI(res)) {
     try {
       const syncParam = {
-        likes: false,
-        dislikes: false,
+        rates: false,
         shares: false,
         comments: false,
         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 (
       id: baseUrl,
       type: 'OrderedCollectionPage',
       totalItems: result.total,
-      first: baseUrl + '?page=1'
+      first: result.data.length === 0
+        ? undefined
+        : baseUrl + '?page=1'
     }
   }
 
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'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 690
+const LAST_MIGRATION_VERSION = 695
 
 // ---------------------------------------------------------------------------
 
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 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction
+  queryInterface: Sequelize.QueryInterface
+  sequelize: Sequelize.Sequelize
+  db: any
+}): Promise<void> {
+  const query = 'DELETE FROM "accountVideoRate" ' +
+    'WHERE "accountVideoRate".id IN (' +
+      'SELECT "accountVideoRate".id FROM "accountVideoRate" ' +
+      'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' +
+      'INNER JOIN actor ON actor.id = account."actorId" ' +
+      'INNER JOIN video ON video.id = "accountVideoRate"."videoId" ' +
+      'WHERE actor."serverId" IS NOT NULL AND video.remote IS TRUE' +
+    ')'
+
+  await utils.sequelize.query(query, { type: Sequelize.QueryTypes.BULKDELETE, transaction: utils.transaction })
+}
+
+function down () {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
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 @@
-import { Transaction } from 'sequelize'
 import { ActivityAudience } from '../../../shared/models/activitypub'
 import { ACTIVITY_PUB } from '../../initializers/constants'
-import { ActorModel } from '../../models/actor/actor'
-import { VideoModel } from '../../models/video/video'
-import { VideoShareModel } from '../../models/video/video-share'
-import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../types/models'
-
-function getRemoteVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience {
-  return {
-    to: [ accountActor.url ],
-    cc: actorsInvolvedInVideo.map(a => a.followersUrl)
-  }
-}
-
-function getVideoCommentAudience (
-  videoComment: MCommentOwnerVideo,
-  threadParentComments: MCommentOwner[],
-  actorsInvolvedInVideo: MActorFollowersUrl[],
-  isOrigin = false
-): ActivityAudience {
-  const to = [ ACTIVITY_PUB.PUBLIC ]
-  const cc: string[] = []
-
-  // Owner of the video we comment
-  if (isOrigin === false) {
-    cc.push(videoComment.Video.VideoChannel.Account.Actor.url)
-  }
-
-  // Followers of the poster
-  cc.push(videoComment.Account.Actor.followersUrl)
-
-  // Send to actors we reply to
-  for (const parentComment of threadParentComments) {
-    if (parentComment.isDeleted()) continue
-
-    cc.push(parentComment.Account.Actor.url)
-  }
-
-  return {
-    to,
-    cc: cc.concat(actorsInvolvedInVideo.map(a => a.followersUrl))
-  }
-}
-
-function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
-  return {
-    to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
-    cc: []
-  }
-}
-
-async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
-  const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
-
-  const videoAll = video as VideoModel
-
-  const videoActor = videoAll.VideoChannel?.Account
-    ? videoAll.VideoChannel.Account.Actor
-    : await ActorModel.loadFromAccountByVideoId(video.id, t)
-
-  actors.push(videoActor)
-
-  return actors
-}
+import { MActorFollowersUrl } from '../../types/models'
 
 function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
   return buildAudience([ actorSender.followersUrl ], isPublic)
@@ -92,9 +30,5 @@ function audiencify<T> (object: T, audience: ActivityAudience) {
 export {
   buildAudience,
   getAudience,
-  getRemoteVideoAudience,
-  getActorsInvolvedInVideo,
-  getAudienceFromFollowersOf,
-  audiencify,
-  getVideoCommentAudience
+  audiencify
 }
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'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { VideoShareModel } from '../../../models/video/video-share'
-import { forwardVideoRelatedActivity } from '../send/utils'
+import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
 import { getOrCreateAPVideo } from '../videos'
 import { Notifier } from '../../notifier'
 import { 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
 import { Notifier } from '../../notifier'
 import { createOrUpdateCacheFile } from '../cache-file'
 import { createOrUpdateVideoPlaylist } from '../playlists'
-import { forwardVideoRelatedActivity } from '../send/utils'
+import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
 import { resolveThread } from '../video-comments'
 import { getOrCreateAPVideo } from '../videos'
 
@@ -55,7 +55,7 @@ export {
 async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
   const videoToCreateData = activity.object as VideoObject
 
-  const syncParam = { likes: false, dislikes: false, shares: false, comments: false, thumbnail: true, refreshVideo: false }
+  const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false }
   const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam })
 
   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 {
   MChannelActor,
   MCommentOwnerVideo
 } from '../../../types/models'
-import { forwardVideoRelatedActivity } from '../send/utils'
+import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
 
 async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) {
   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 @@
+import { VideoModel } from '@server/models/video/video'
 import { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
 import { APProcessorOptions } from '../../../types/activitypub-processor.model'
 import { MActorSignature } from '../../../types/models'
-import { forwardVideoRelatedActivity } from '../send/utils'
-import { getOrCreateAPVideo } from '../videos'
+import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
 
 async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) {
   const { activity, byActor } = options
@@ -29,16 +29,23 @@ async function processDislike (activity: ActivityCreate | ActivityDislike, byAct
 
   if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
 
-  const { video } = await getOrCreateAPVideo({ videoObject: dislikeObject })
+  const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeObject, fetchType: 'only-video' })
+
+  // We don't care about dislikes of remote videos
+  if (!onlyVideo.isOwned()) return
 
   return sequelizeTypescript.transaction(async t => {
+    const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
+
     const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
     if (existingRate && existingRate.type === 'dislike') return
 
     await video.increment('dislikes', { transaction: t })
+    video.dislikes++
 
     if (existingRate && existingRate.type === 'like') {
       await video.decrement('likes', { transaction: t })
+      video.likes--
     }
 
     const rate = existingRate || new AccountVideoRateModel()
@@ -49,11 +56,6 @@ async function processDislike (activity: ActivityCreate | ActivityDislike, byAct
 
     await rate.save({ transaction: t })
 
-    if (video.isOwned()) {
-      // Don't resend the activity to the sender
-      const exceptions = [ byActor ]
-
-      await forwardVideoRelatedActivity(activity, t, exceptions, video)
-    }
+    await federateVideoIfNeeded(video, false, t)
   })
 }
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 @@
+import { VideoModel } from '@server/models/video/video'
 import { ActivityLike } from '../../../../shared/models/activitypub'
 import { getAPId } from '../../../helpers/activitypub'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
@@ -5,11 +6,11 @@ import { sequelizeTypescript } from '../../../initializers/database'
 import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
 import { APProcessorOptions } from '../../../types/activitypub-processor.model'
 import { MActorSignature } from '../../../types/models'
-import { forwardVideoRelatedActivity } from '../send/utils'
-import { getOrCreateAPVideo } from '../videos'
+import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
 
 async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
   const { activity, byActor } = options
+
   return retryTransactionWrapper(processLikeVideo, byActor, activity)
 }
 
@@ -27,17 +28,24 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
   const byAccount = byActor.Account
   if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
 
-  const { video } = await getOrCreateAPVideo({ videoObject: videoUrl })
+  const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video' })
+
+  // We don't care about likes of remote videos
+  if (!onlyVideo.isOwned()) return
 
   return sequelizeTypescript.transaction(async t => {
+    const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
+
     const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
     if (existingRate && existingRate.type === 'like') return
 
     if (existingRate && existingRate.type === 'dislike') {
       await video.decrement('dislikes', { transaction: t })
+      video.dislikes--
     }
 
     await video.increment('likes', { transaction: t })
+    video.likes++
 
     const rate = existingRate || new AccountVideoRateModel()
     rate.type = 'like'
@@ -47,11 +55,6 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
 
     await rate.save({ transaction: t })
 
-    if (video.isOwned()) {
-      // Don't resend the activity to the sender
-      const exceptions = [ byActor ]
-
-      await forwardVideoRelatedActivity(activity, t, exceptions, video)
-    }
+    await federateVideoIfNeeded(video, false, t)
   })
 }
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 @@
+import { VideoModel } from '@server/models/video/video'
 import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub'
 import { DislikeObject } from '../../../../shared/models/activitypub/objects'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
@@ -10,8 +11,8 @@ import { VideoRedundancyModel } from '../../../models/redundancy/video-redundanc
 import { VideoShareModel } from '../../../models/video/video-share'
 import { APProcessorOptions } from '../../../types/activitypub-processor.model'
 import { MActorSignature } from '../../../types/models'
-import { forwardVideoRelatedActivity } from '../send/utils'
-import { getOrCreateAPVideo } from '../videos'
+import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
+import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
 
 async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) {
   const { activity, byActor } = options
@@ -55,23 +56,22 @@ export {
 async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) {
   const likeActivity = activity.object as ActivityLike
 
-  const { video } = await getOrCreateAPVideo({ videoObject: likeActivity.object })
+  const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object })
+  // We don't care about likes of remote videos
+  if (!onlyVideo.isOwned()) return
 
   return sequelizeTypescript.transaction(async t => {
     if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
 
+    const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
     const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, likeActivity.id, t)
     if (!rate || rate.type !== 'like') throw new Error(`Unknown like by account ${byActor.Account.id} for video ${video.id}.`)
 
     await rate.destroy({ transaction: t })
     await video.decrement('likes', { transaction: t })
 
-    if (video.isOwned()) {
-      // Don't resend the activity to the sender
-      const exceptions = [ byActor ]
-
-      await forwardVideoRelatedActivity(activity, t, exceptions, video)
-    }
+    video.likes--
+    await federateVideoIfNeeded(video, false, t)
   })
 }
 
@@ -80,26 +80,27 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
     ? activity.object
     : activity.object.object as DislikeObject
 
-  const { video } = await getOrCreateAPVideo({ videoObject: dislike.object })
+  const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislike.object })
+  // We don't care about likes of remote videos
+  if (!onlyVideo.isOwned()) return
 
   return sequelizeTypescript.transaction(async t => {
     if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
 
+    const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(onlyVideo.id, t)
     const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislike.id, t)
     if (!rate || rate.type !== 'dislike') throw new Error(`Unknown dislike by account ${byActor.Account.id} for video ${video.id}.`)
 
     await rate.destroy({ transaction: t })
     await video.decrement('dislikes', { transaction: t })
+    video.dislikes--
 
-    if (video.isOwned()) {
-      // Don't resend the activity to the sender
-      const exceptions = [ byActor ]
-
-      await forwardVideoRelatedActivity(activity, t, exceptions, video)
-    }
+    await federateVideoIfNeeded(video, false, t)
   })
 }
 
+// ---------------------------------------------------------------------------
+
 async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) {
   const cacheFileObject = activity.object.object as CacheFileObject
 
@@ -125,19 +126,6 @@ async function processUndoCacheFile (byActor: MActorSignature, activity: Activit
   })
 }
 
-function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) {
-  return sequelizeTypescript.transaction(async t => {
-    const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t)
-    const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
-
-    if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${following.id}.`)
-
-    await actorFollow.destroy({ transaction: t })
-
-    return undefined
-  })
-}
-
 function processUndoAnnounce (byActor: MActorSignature, announceActivity: ActivityAnnounce) {
   return sequelizeTypescript.transaction(async t => {
     const share = await VideoShareModel.loadByUrl(announceActivity.id, t)
@@ -155,3 +143,18 @@ function processUndoAnnounce (byActor: MActorSignature, announceActivity: Activi
     }
   })
 }
+
+// ---------------------------------------------------------------------------
+
+function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) {
+  return sequelizeTypescript.transaction(async t => {
+    const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t)
+    const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
+
+    if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${following.id}.`)
+
+    await actorFollow.destroy({ transaction: t })
+
+    return undefined
+  })
+}
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'
 import { APActorUpdater } from '../actors/updater'
 import { createOrUpdateCacheFile } from '../cache-file'
 import { createOrUpdateVideoPlaylist } from '../playlists'
-import { forwardVideoRelatedActivity } from '../send/utils'
+import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
 import { APVideoUpdater, getOrCreateAPVideo } from '../videos'
 
 async 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'
 import { ActivityView } from '../../../../shared/models/activitypub'
 import { APProcessorOptions } from '../../../types/activitypub-processor.model'
 import { MActorSignature } from '../../../types/models'
-import { forwardVideoRelatedActivity } from '../send/utils'
+import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
 import { getOrCreateAPVideo } from '../videos'
 
 async 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 @@
-import { ActivityAccept, ActivityFollow } from '../../../../shared/models/activitypub'
+import { ActivityAccept, ActivityFollow } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { MActor, MActorFollowActors } from '../../../types/models'
 import { getLocalActorFollowAcceptActivityPubUrl } from '../url'
 import { buildFollowActivity } from './send-follow'
-import { unicastTo } from './utils'
+import { unicastTo } from './shared/send-utils'
 
 function sendAccept (actorFollow: MActorFollowActors) {
   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 @@
 import { Transaction } from 'sequelize'
-import { ActivityAnnounce, ActivityAudience } from '../../../../shared/models/activitypub'
-import { broadcastToFollowers } from './utils'
-import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf } from '../audience'
+import { ActivityAnnounce, ActivityAudience } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { MActorLight, MVideo } from '../../../types/models'
 import { MVideoShare } from '../../../types/models/video'
+import { audiencify, getAudience } from '../audience'
+import { getActorsInvolvedInVideo, getAudienceFromFollowersOf } from './shared'
+import { broadcastToFollowers } from './shared/send-utils'
 
 async function buildAnnounceWithVideoAudience (
   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 @@
 import { Transaction } from 'sequelize'
-import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub'
-import { VideoPrivacy } from '../../../../shared/models/videos'
-import { VideoCommentModel } from '../../../models/video/video-comment'
-import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
-import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
+import { getServerActor } from '@server/models/application/application'
+import { ActivityAudience, ActivityCreate, ContextType, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
 import { logger, loggerTagsFactory } from '../../../helpers/logger'
-import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
+import { VideoCommentModel } from '../../../models/video/video-comment'
 import {
   MActorLight,
   MCommentOwnerVideo,
@@ -15,8 +12,16 @@ import {
   MVideoRedundancyFileVideo,
   MVideoRedundancyStreamingPlaylistVideo
 } from '../../../types/models'
-import { getServerActor } from '@server/models/application/application'
-import { ContextType } from '@shared/models/activitypub/context'
+import { audiencify, getAudience } from '../audience'
+import {
+  broadcastToActors,
+  broadcastToFollowers,
+  getActorsInvolvedInVideo,
+  getAudienceFromFollowersOf,
+  getVideoCommentAudience,
+  sendVideoRelatedActivity,
+  unicastTo
+} from './shared'
 
 const lTags = loggerTagsFactory('ap', 'create')
 
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 @@
 import { Transaction } from 'sequelize'
 import { getServerActor } from '@server/models/application/application'
-import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
+import { ActivityAudience, ActivityDelete } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { ActorModel } from '../../../models/actor/actor'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { VideoShareModel } from '../../../models/video/video-share'
 import { MActorUrl } from '../../../types/models'
 import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../types/models/video'
-import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
+import { audiencify } from '../audience'
 import { getDeleteActivityPubUrl } from '../url'
-import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
+import { getActorsInvolvedInVideo, getVideoCommentAudience } from './shared'
+import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './shared/send-utils'
 
 async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
   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 @@
 import { Transaction } from 'sequelize'
-import { getVideoDislikeActivityPubUrlByLocalActor } from '../url'
+import { ActivityAudience, ActivityDislike } from '@shared/models'
 import { logger } from '../../../helpers/logger'
-import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub'
-import { sendVideoRelatedActivity } from './utils'
-import { audiencify, getAudience } from '../audience'
 import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models'
+import { audiencify, getAudience } from '../audience'
+import { getVideoDislikeActivityPubUrlByLocalActor } from '../url'
+import { sendVideoActivityToOrigin } from './shared/send-utils'
 
 function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
   logger.info('Creating job to dislike %s.', video.url)
@@ -15,7 +15,7 @@ function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction
     return buildDislikeActivity(url, byActor, video, audience)
   }
 
-  return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
+  return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction: t })
 }
 
 function 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 @@
 import { Transaction } from 'sequelize'
-import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
+import { ActivityAudience, ActivityFlag } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { MAbuseAP, MAccountLight, MActor } from '../../../types/models'
 import { audiencify, getAudience } from '../audience'
 import { getLocalAbuseActivityPubUrl } from '../url'
-import { unicastTo } from './utils'
+import { unicastTo } from './shared/send-utils'
 
 function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) {
   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 @@
 import { Transaction } from 'sequelize'
-import { ActivityFollow } from '../../../../shared/models/activitypub'
+import { ActivityFollow } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { MActor, MActorFollowActors } from '../../../types/models'
-import { unicastTo } from './utils'
+import { unicastTo } from './shared/send-utils'
 
 function sendFollow (actorFollow: MActorFollowActors, t: Transaction) {
   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 @@
 import { Transaction } from 'sequelize'
-import { ActivityAudience, ActivityLike } from '../../../../shared/models/activitypub'
-import { getVideoLikeActivityPubUrlByLocalActor } from '../url'
-import { sendVideoRelatedActivity } from './utils'
-import { audiencify, getAudience } from '../audience'
+import { ActivityAudience, ActivityLike } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models'
+import { audiencify, getAudience } from '../audience'
+import { getVideoLikeActivityPubUrlByLocalActor } from '../url'
+import { sendVideoActivityToOrigin } from './shared/send-utils'
 
 function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
   logger.info('Creating job to like %s.', video.url)
@@ -15,7 +15,7 @@ function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
     return buildLikeActivity(url, byActor, video, audience)
   }
 
-  return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
+  return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction: t })
 }
 
 function 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 @@
-import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub'
+import { ActivityFollow, ActivityReject } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { MActor } from '../../../types/models'
 import { getLocalActorFollowRejectActivityPubUrl } from '../url'
 import { buildFollowActivity } from './send-follow'
-import { unicastTo } from './utils'
+import { unicastTo } from './shared/send-utils'
 
 function sendReject (followUrl: string, follower: MActor, following: MActor) {
   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 {
   ActivityFollow,
   ActivityLike,
   ActivityUndo
-} from '../../../../shared/models/activitypub'
+} from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { VideoModel } from '../../../models/video/video'
 import {
@@ -27,7 +27,7 @@ import { buildCreateActivity } from './send-create'
 import { buildDislikeActivity } from './send-dislike'
 import { buildFollowActivity } from './send-follow'
 import { buildLikeActivity } from './send-like'
-import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
+import { broadcastToFollowers, sendVideoActivityToOrigin, sendVideoRelatedActivity, unicastTo } from './shared/send-utils'
 
 function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
   const me = actorFollow.ActorFollower
@@ -46,6 +46,8 @@ function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
   t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl))
 }
 
+// ---------------------------------------------------------------------------
+
 async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) {
   logger.info('Creating job to undo announce %s.', videoShare.url)
 
@@ -58,24 +60,6 @@ async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare,
   return broadcastToFollowers(undoActivity, byActor, actorsInvolvedInVideo, t, followersException)
 }
 
-async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
-  logger.info('Creating job to undo a like of video %s.', video.url)
-
-  const likeUrl = getVideoLikeActivityPubUrlByLocalActor(byActor, video)
-  const likeActivity = buildLikeActivity(likeUrl, byActor, video)
-
-  return sendUndoVideoRelatedActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t })
-}
-
-async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
-  logger.info('Creating job to undo a dislike of video %s.', video.url)
-
-  const dislikeUrl = getVideoDislikeActivityPubUrlByLocalActor(byActor, video)
-  const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
-
-  return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t })
-}
-
 async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, t: Transaction) {
   logger.info('Creating job to undo cache file %s.', redundancyModel.url)
 
@@ -93,6 +77,26 @@ async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedund
 
 // ---------------------------------------------------------------------------
 
+async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
+  logger.info('Creating job to undo a like of video %s.', video.url)
+
+  const likeUrl = getVideoLikeActivityPubUrlByLocalActor(byActor, video)
+  const likeActivity = buildLikeActivity(likeUrl, byActor, video)
+
+  return sendUndoVideoToOriginActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t })
+}
+
+async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
+  logger.info('Creating job to undo a dislike of video %s.', video.url)
+
+  const dislikeUrl = getVideoDislikeActivityPubUrlByLocalActor(byActor, video)
+  const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
+
+  return sendUndoVideoToOriginActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t })
+}
+
+// ---------------------------------------------------------------------------
+
 export {
   sendUndoFollow,
   sendUndoLike,
@@ -126,7 +130,7 @@ async function sendUndoVideoRelatedActivity (options: {
   byActor: MActor
   video: MVideoAccountLight
   url: string
-  activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce
+  activity: ActivityFollow | ActivityCreate | ActivityAnnounce
   transaction: Transaction
 }) {
   const activityBuilder = (audience: ActivityAudience) => {
@@ -137,3 +141,19 @@ async function sendUndoVideoRelatedActivity (options: {
 
   return sendVideoRelatedActivity(activityBuilder, options)
 }
+
+async function sendUndoVideoToOriginActivity (options: {
+  byActor: MActor
+  video: MVideoAccountLight
+  url: string
+  activity: ActivityLike | ActivityDislike
+  transaction: Transaction
+}) {
+  const activityBuilder = (audience: ActivityAudience) => {
+    const undoUrl = getUndoActivityPubUrl(options.url)
+
+    return undoActivityData(undoUrl, options.byActor, options.activity, audience)
+  }
+
+  return sendVideoActivityToOrigin(activityBuilder, options)
+}
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 @@
 import { Transaction } from 'sequelize'
-import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub'
-import { VideoPrivacy } from '../../../../shared/models/videos'
+import { getServerActor } from '@server/models/application/application'
+import { ActivityAudience, ActivityUpdate, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
+import { logger } from '../../../helpers/logger'
 import { AccountModel } from '../../../models/account/account'
 import { VideoModel } from '../../../models/video/video'
 import { VideoShareModel } from '../../../models/video/video-share'
-import { getUpdateActivityPubUrl } from '../url'
-import { broadcastToFollowers, sendVideoRelatedActivity } from './utils'
-import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience'
-import { logger } from '../../../helpers/logger'
-import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
 import {
   MAccountDefault,
   MActor,
@@ -19,7 +15,10 @@ import {
   MVideoPlaylistFull,
   MVideoRedundancyVideo
 } from '../../../types/models'
-import { getServerActor } from '@server/models/application/application'
+import { audiencify, getAudience } from '../audience'
+import { getUpdateActivityPubUrl } from '../url'
+import { getActorsInvolvedInVideo } from './shared'
+import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils'
 
 async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
   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 @@
 import { Transaction } from 'sequelize'
 import { VideoViews } from '@server/lib/video-views'
 import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models'
-import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub'
+import { ActivityAudience, ActivityView } from '@shared/models'
 import { logger } from '../../../helpers/logger'
 import { ActorModel } from '../../../models/actor/actor'
 import { audiencify, getAudience } from '../audience'
 import { getLocalVideoViewActivityPubUrl } from '../url'
-import { sendVideoRelatedActivity } from './utils'
+import { sendVideoRelatedActivity } from './shared/send-utils'
 
 async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) {
   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 @@
+import { Transaction } from 'sequelize/dist'
+import { ACTIVITY_PUB } from '@server/initializers/constants'
+import { ActorModel } from '@server/models/actor/actor'
+import { VideoModel } from '@server/models/video/video'
+import { VideoShareModel } from '@server/models/video/video-share'
+import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '@server/types/models'
+import { ActivityAudience } from '@shared/models'
+
+function getOriginVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[] = []): ActivityAudience {
+  return {
+    to: [ accountActor.url ],
+    cc: actorsInvolvedInVideo.map(a => a.followersUrl)
+  }
+}
+
+function getVideoCommentAudience (
+  videoComment: MCommentOwnerVideo,
+  threadParentComments: MCommentOwner[],
+  actorsInvolvedInVideo: MActorFollowersUrl[],
+  isOrigin = false
+): ActivityAudience {
+  const to = [ ACTIVITY_PUB.PUBLIC ]
+  const cc: string[] = []
+
+  // Owner of the video we comment
+  if (isOrigin === false) {
+    cc.push(videoComment.Video.VideoChannel.Account.Actor.url)
+  }
+
+  // Followers of the poster
+  cc.push(videoComment.Account.Actor.followersUrl)
+
+  // Send to actors we reply to
+  for (const parentComment of threadParentComments) {
+    if (parentComment.isDeleted()) continue
+
+    cc.push(parentComment.Account.Actor.url)
+  }
+
+  return {
+    to,
+    cc: cc.concat(actorsInvolvedInVideo.map(a => a.followersUrl))
+  }
+}
+
+function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
+  return {
+    to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
+    cc: []
+  }
+}
+
+async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
+  const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
+
+  const videoAll = video as VideoModel
+
+  const videoActor = videoAll.VideoChannel?.Account
+    ? videoAll.VideoChannel.Account.Actor
+    : await ActorModel.loadFromAccountByVideoId(video.id, t)
+
+  actors.push(videoActor)
+
+  return actors
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  getOriginVideoAudience,
+  getActorsInvolvedInVideo,
+  getAudienceFromFollowersOf,
+  getVideoCommentAudience
+}
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 @@
+export * from './audience-utils'
+export * from './send-utils'
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/shared/send-utils.ts
similarity index 78%
rename from server/lib/activitypub/send/utils.ts
rename to 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 @@
 import { Transaction } from 'sequelize'
 import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache'
 import { getServerActor } from '@server/models/application/application'
+import { Activity, ActivityAudience } from '@shared/models'
 import { ContextType } from '@shared/models/activitypub/context'
-import { Activity, ActivityAudience } from '../../../../shared/models/activitypub'
-import { afterCommitIfTransaction } from '../../../helpers/database-utils'
-import { logger } from '../../../helpers/logger'
-import { ActorModel } from '../../../models/actor/actor'
-import { ActorFollowModel } from '../../../models/actor/actor-follow'
-import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models'
-import { JobQueue } from '../../job-queue'
-import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
+import { afterCommitIfTransaction } from '../../../../helpers/database-utils'
+import { logger } from '../../../../helpers/logger'
+import { ActorModel } from '../../../../models/actor/actor'
+import { ActorFollowModel } from '../../../../models/actor/actor-follow'
+import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../../types/models'
+import { JobQueue } from '../../../job-queue'
+import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getOriginVideoAudience } from './audience-utils'
 
 async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
   byActor: MActorLight
@@ -23,16 +23,7 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
 
   // Send to origin
   if (video.isOwned() === false) {
-    let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor
-
-    if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)
-
-    const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo)
-    const activity = activityBuilder(audience)
-
-    return afterCommitIfTransaction(transaction, () => {
-      return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
-    })
+    return sendVideoActivityToOrigin(activityBuilder, options)
   }
 
   // Send to followers
@@ -44,6 +35,30 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
   return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType)
 }
 
+async function sendVideoActivityToOrigin (activityBuilder: (audience: ActivityAudience) => Activity, options: {
+  byActor: MActorLight
+  video: MVideoImmutable | MVideoAccountLight
+  actorsInvolvedInVideo?: MActorLight[]
+  transaction?: Transaction
+  contextType?: ContextType
+}) {
+  const { byActor, video, actorsInvolvedInVideo, transaction, contextType } = options
+
+  if (video.isOwned()) throw new Error('Cannot send activity to owned video origin ' + video.url)
+
+  let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor
+  if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)
+
+  const audience = getOriginVideoAudience(accountActor, actorsInvolvedInVideo)
+  const activity = activityBuilder(audience)
+
+  return afterCommitIfTransaction(transaction, () => {
+    return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
+  })
+}
+
+// ---------------------------------------------------------------------------
+
 async function forwardVideoRelatedActivity (
   activity: Activity,
   t: Transaction,
@@ -92,6 +107,8 @@ async function forwardActivity (
   return afterCommitIfTransaction(t, () => JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }))
 }
 
+// ---------------------------------------------------------------------------
+
 async function broadcastToFollowers (
   data: any,
   byActor: MActorId,
@@ -177,6 +194,7 @@ export {
   unicastTo,
   forwardActivity,
   broadcastToActors,
+  sendVideoActivityToOrigin,
   forwardVideoRelatedActivity,
   sendVideoRelatedActivity
 }
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) {
 
   // Maybe it's a reply to a video?
   // If yes, it's done: we resolved all the thread
-  const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false }
+  const syncParam = { rates: true, shares: true, comments: false, thumbnail: true, refreshVideo: false }
   const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
 
   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,36 +1,48 @@
-import { map } from 'bluebird'
 import { Transaction } from 'sequelize'
-import { doJSONRequest } from '@server/helpers/requests'
 import { VideoRateType } from '../../../shared/models/videos'
-import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
-import { logger, loggerTagsFactory } from '../../helpers/logger'
-import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
-import { AccountVideoRateModel } from '../../models/account/account-video-rate'
-import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
-import { getOrCreateAPActor } from './actors'
+import { MAccountActor, MActorUrl, MVideoAccountLight, MVideoFullLight, MVideoId } from '../../types/models'
 import { sendLike, sendUndoDislike, sendUndoLike } from './send'
 import { sendDislike } from './send/send-dislike'
 import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
-
-const lTags = loggerTagsFactory('ap', 'video-rate', 'create')
-
-async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
-  await map(ratesUrl, async rateUrl => {
-    try {
-      await createRate(rateUrl, video, rate)
-    } catch (err) {
-      logger.info('Cannot add rate %s.', rateUrl, { err, ...lTags(rateUrl, video.uuid, video.url) })
-    }
-  }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
-}
+import { federateVideoIfNeeded } from './videos'
 
 async function sendVideoRateChange (
+  account: MAccountActor,
+  video: MVideoFullLight,
+  likes: number,
+  dislikes: number,
+  t: Transaction
+) {
+  if (video.isOwned()) return federateVideoIfNeeded(video, false, t)
+
+  return sendVideoRateChangeToOrigin(account, video, likes, dislikes, t)
+}
+
+function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) {
+  return rateType === 'like'
+    ? getVideoLikeActivityPubUrlByLocalActor(actor, video)
+    : getVideoDislikeActivityPubUrlByLocalActor(actor, video)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  getLocalRateUrl,
+  sendVideoRateChange
+}
+
+// ---------------------------------------------------------------------------
+
+async function sendVideoRateChangeToOrigin (
   account: MAccountActor,
   video: MVideoAccountLight,
   likes: number,
   dislikes: number,
   t: Transaction
 ) {
+  // Local video, we don't need to send like
+  if (video.isOwned()) return
+
   const actor = account.Actor
 
   // Keep the order: first we undo and then we create
@@ -45,46 +57,3 @@ async function sendVideoRateChange (
   // Dislike
   if (dislikes > 0) await sendDislike(actor, video, t)
 }
-
-function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) {
-  return rateType === 'like'
-    ? getVideoLikeActivityPubUrlByLocalActor(actor, video)
-    : getVideoDislikeActivityPubUrlByLocalActor(actor, video)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getLocalRateUrl,
-  createRates,
-  sendVideoRateChange
-}
-
-// ---------------------------------------------------------------------------
-
-async function createRate (rateUrl: string, video: MVideo, rate: VideoRateType) {
-  // Fetch url
-  const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true })
-  if (!body || !body.actor) throw new Error('Body or body actor is invalid')
-
-  const actorUrl = getAPId(body.actor)
-  if (checkUrlsSameHost(actorUrl, rateUrl) !== true) {
-    throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`)
-  }
-
-  if (checkUrlsSameHost(body.id, rateUrl) !== true) {
-    throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`)
-  }
-
-  const actor = await getOrCreateAPActor(actorUrl)
-
-  const entry = {
-    videoId: video.id,
-    accountId: actor.Account.id,
-    type: rate,
-    url: body.id
-  }
-
-  // Video "likes"/"dislikes" will be updated by the caller
-  await AccountVideoRateModel.upsert(entry)
-}
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 (
   options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
 ): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
   // Default params
-  const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
+  const syncParam = options.syncParam || { rates: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
   const fetchType = options.fetchType || 'all'
   const allowRefresh = options.allowRefresh !== false
 
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 @@
+import { runInReadCommittedTransaction } from '@server/helpers/database-utils'
 import { logger, loggerTagsFactory } from '@server/helpers/logger'
+import { doJSONRequest } from '@server/helpers/requests'
 import { JobQueue } from '@server/lib/job-queue'
-import { AccountVideoRateModel } from '@server/models/account/account-video-rate'
+import { VideoModel } from '@server/models/video/video'
 import { VideoCommentModel } from '@server/models/video/video-comment'
 import { VideoShareModel } from '@server/models/video/video-share'
 import { MVideo } from '@server/types/models'
-import { ActivitypubHttpFetcherPayload, VideoObject } from '@shared/models'
+import { ActivitypubHttpFetcherPayload, ActivityPubOrderedCollection, VideoObject } from '@shared/models'
 import { crawlCollectionPage } from '../../crawl'
 import { addVideoShares } from '../../share'
 import { addVideoComments } from '../../video-comments'
-import { createRates } from '../../video-rates'
 
 const lTags = loggerTagsFactory('ap', 'video')
 
 type SyncParam = {
-  likes: boolean
-  dislikes: boolean
+  rates: boolean
   shares: boolean
   comments: boolean
   thumbnail: boolean
@@ -24,45 +24,57 @@ type SyncParam = {
 async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoObject, syncParam: SyncParam) {
   logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
 
-  await syncRates('like', video, fetchedVideo, syncParam.likes)
-  await syncRates('dislike', video, fetchedVideo, syncParam.dislikes)
+  const ratePromise = updateVideoRates(video, fetchedVideo)
+  if (syncParam.rates) await ratePromise
 
   await syncShares(video, fetchedVideo, syncParam.shares)
 
   await syncComments(video, fetchedVideo, syncParam.comments)
 }
 
+async function updateVideoRates (video: MVideo, fetchedVideo: VideoObject) {
+  const [ likes, dislikes ] = await Promise.all([
+    getRatesCount('like', video, fetchedVideo),
+    getRatesCount('dislike', video, fetchedVideo)
+  ])
+
+  return runInReadCommittedTransaction(async t => {
+    await VideoModel.updateRatesOf(video.id, 'like', likes, t)
+    await VideoModel.updateRatesOf(video.id, 'dislike', dislikes, t)
+  })
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   SyncParam,
-  syncVideoExternalAttributes
+  syncVideoExternalAttributes,
+  updateVideoRates
 }
 
 // ---------------------------------------------------------------------------
 
-function createJob (payload: ActivitypubHttpFetcherPayload) {
-  return JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
-}
-
-function syncRates (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {
+async function getRatesCount (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject) {
   const uri = type === 'like'
     ? fetchedVideo.likes
     : fetchedVideo.dislikes
 
-  if (!isSync) {
-    const jobType = type === 'like'
-      ? 'video-likes'
-      : 'video-dislikes'
+  logger.info('Sync %s of video %s', type, video.url)
+  const options = { activityPub: true }
 
-    return createJob({ uri, videoId: video.id, type: jobType })
+  const response = await doJSONRequest<ActivityPubOrderedCollection<any>>(uri, options)
+  const totalItems = response.body.totalItems
+
+  if (isNaN(totalItems)) {
+    logger.error('Cannot sync %s of video %s, totalItems is not a number', type, video.url, { body: response.body })
+    return
   }
 
-  const handler = items => createRates(items, video, type)
-  const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, type, crawlStartDate)
+  return totalItems
+}
 
-  return crawlCollectionPage<string>(uri, handler, cleaner)
-    .catch(err => logger.error('Cannot add rate of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) }))
+function createJob (payload: ActivitypubHttpFetcherPayload) {
+  return JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
 }
 
 function 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'
 import { VideoLiveModel } from '@server/models/video/video-live'
 import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models'
 import { VideoObject, VideoPrivacy } from '@shared/models'
-import { APVideoAbstractBuilder, getVideoAttributesFromObject } from './shared'
+import { APVideoAbstractBuilder, getVideoAttributesFromObject, updateVideoRates } from './shared'
 
 export class APVideoUpdater extends APVideoAbstractBuilder {
   private readonly wasPrivateVideo: boolean
@@ -74,6 +74,8 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
         transaction: undefined
       })
 
+      await updateVideoRates(videoUpdated, this.videoObject)
+
       // Notify our users?
       if (this.wasPrivateVideo || this.wasUnlistedVideo) {
         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) {
       if (result?.status === 'deleted') {
         const { videoId, type } = result.data
 
-        await VideoModel.updateRatesOf(videoId, type, undefined)
+        await VideoModel.syncLocalRates(videoId, type, undefined)
       }
     }, { concurrency: AP_CLEANER.CONCURRENCY })
   }
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 @@
 import { Job } from 'bull'
 import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models'
 import { logger } from '../../../helpers/logger'
-import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
 import { VideoModel } from '../../../models/video/video'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { VideoShareModel } from '../../../models/video/video-share'
@@ -11,7 +10,6 @@ import { createAccountPlaylists } from '../../activitypub/playlists'
 import { processActivities } from '../../activitypub/process'
 import { addVideoShares } from '../../activitypub/share'
 import { addVideoComments } from '../../activitypub/video-comments'
-import { createRates } from '../../activitypub/video-rates'
 
 async function processActivityPubHttpFetcher (job: Job) {
   logger.info('Processing ActivityPub fetcher in job %d.', job.id)
@@ -23,16 +21,12 @@ async function processActivityPubHttpFetcher (job: Job) {
 
   const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
     'activity': items => processActivities(items, { outboxUrl: payload.uri, fromFetch: true }),
-    'video-likes': items => createRates(items, video, 'like'),
-    'video-dislikes': items => createRates(items, video, 'dislike'),
     'video-shares': items => addVideoShares(items, video),
     'video-comments': items => addVideoComments(items),
     'account-playlists': items => createAccountPlaylists(items)
   }
 
   const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Promise<any> } = {
-    'video-likes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate),
-    'video-dislikes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate),
     'video-shares': crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate),
     'video-comments': crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
   }
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 {
 
 async function refreshVideo (videoUrl: string) {
   const fetchType = 'all' as 'all'
-  const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true }
+  const syncParam = { rates: true, shares: true, comments: true, thumbnail: true }
 
   const videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType)
   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 {
     // We need more attributes and check if the video still exists
     const getVideoOptions = {
       videoObject: videoUrl,
-      syncParam: { likes: false, dislikes: false, shares: false, comments: false, thumbnail: false, refreshVideo: true },
+      syncParam: { rates: false, shares: false, comments: false, thumbnail: false, refreshVideo: true },
       fetchType: 'all' as 'all'
     }
     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'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants'
 import { ActorModel } from '../actor/actor'
-import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils'
+import { getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from '../video/video'
 import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
 import { AccountModel } from './account'
@@ -249,28 +249,6 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV
     ]).then(([ total, data ]) => ({ total, data }))
   }
 
-  static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) {
-    return AccountVideoRateModel.sequelize.transaction(async t => {
-      const query = {
-        where: {
-          updatedAt: {
-            [Op.lt]: beforeUpdatedAt
-          },
-          videoId,
-          type,
-          accountId: {
-            [Op.notIn]: buildLocalAccountIdsIn()
-          }
-        },
-        transaction: t
-      }
-
-      await AccountVideoRateModel.destroy(query)
-
-      return VideoModel.updateRatesOf(videoId, type, t)
-    })
-  }
-
   toFormattedJSON (this: MAccountVideoRateFormattable): AccountVideoRate {
     return {
       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>>> {
     })
   }
 
-  static updateRatesOf (videoId: number, type: VideoRateType, t: Transaction) {
+  static updateRatesOf (videoId: number, type: VideoRateType, count: number, t: Transaction) {
+    const field = type === 'like'
+      ? 'likes'
+      : 'dislikes'
+
+    const rawQuery = `UPDATE "video" SET "${field}" = :count WHERE "video"."id" = :videoId`
+
+    return AccountVideoRateModel.sequelize.query(rawQuery, {
+      transaction: t,
+      replacements: { videoId, rateType: type, count },
+      type: QueryTypes.UPDATE
+    })
+  }
+
+  static syncLocalRates (videoId: number, type: VideoRateType, t: Transaction) {
     const field = type === 'like'
       ? 'likes'
       : '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 () {
 
         for (const baseVideo of baseVideos) {
           const sameVideo = data.find(video => video.name === baseVideo.name)
-          expect(baseVideo.likes).to.equal(sameVideo.likes)
-          expect(baseVideo.dislikes).to.equal(sameVideo.dislikes)
+          expect(baseVideo.likes).to.equal(sameVideo.likes, `Likes of ${sameVideo.uuid} do not correspond`)
+          expect(baseVideo.dislikes).to.equal(sameVideo.dislikes, `Dislikes of ${sameVideo.uuid} do not correspond`)
         }
       }
     })
diff --git a/shared/models/activitypub/index.ts b/shared/models/activitypub/index.ts
index 6cacb24d2..fa07b6a64 100644
--- a/shared/models/activitypub/index.ts
+++ b/shared/models/activitypub/index.ts
@@ -1,8 +1,9 @@
+export * from './objects'
 export * from './activity'
 export * from './activitypub-actor'
 export * from './activitypub-collection'
 export * from './activitypub-ordered-collection'
 export * from './activitypub-root'
 export * from './activitypub-signature'
-export * from './objects'
+export * from './context'
 export * from './webfinger'
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts
index 6b07eba69..d81b72696 100644
--- a/shared/models/server/job.model.ts
+++ b/shared/models/server/job.model.ts
@@ -52,7 +52,7 @@ export type ActivitypubFollowPayload = {
   assertIsChannel?: boolean
 }
 
-export type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists'
+export type FetchType = 'activity' | 'video-shares' | 'video-comments' | 'account-playlists'
 export type ActivitypubHttpFetcherPayload = {
   uri: string
   type: FetchType