From 57e4e1c1a95c3a81a967f54ecc2a510d8b0e129c Mon Sep 17 00:00:00 2001 From: Chocobozzz 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 | 60 +++++++++----- 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 | 79 ++++++------------- 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, 393 insertions(+), 329 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 { + 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 (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) { 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) { 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) { 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) { 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) { 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) { 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,13 +60,30 @@ async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, return broadcastToFollowers(undoActivity, byActor, actorsInvolvedInVideo, t, followersException) } +async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, t: Transaction) { + logger.info('Creating job to undo cache file %s.', redundancyModel.url) + + const associatedVideo = redundancyModel.getVideo() + if (!associatedVideo) { + logger.warn('Cannot send undo activity for redundancy %s: no video files associated.', redundancyModel.url) + return + } + + const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(associatedVideo.id) + const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) + + return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) +} + +// --------------------------------------------------------------------------- + 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 }) + return sendUndoVideoToOriginActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t }) } async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { @@ -73,22 +92,7 @@ async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: T 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) - - const associatedVideo = redundancyModel.getVideo() - if (!associatedVideo) { - logger.warn('Cannot send undo activity for redundancy %s: no video files associated.', redundancyModel.url) - return - } - - const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(associatedVideo.id) - const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) - - return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) + return sendUndoVideoToOriginActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t }) } // --------------------------------------------------------------------------- @@ -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,49 +1,21 @@ -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: MVideoAccountLight, + video: MVideoFullLight, likes: number, dislikes: number, t: Transaction ) { - const actor = account.Actor + if (video.isOwned()) return federateVideoIfNeeded(video, false, t) - // Keep the order: first we undo and then we create - - // Undo Like - if (likes < 0) await sendUndoLike(actor, video, t) - // Undo Dislike - if (dislikes < 0) await sendUndoDislike(actor, video, t) - - // Like - if (likes > 0) await sendLike(actor, video, t) - // Dislike - if (dislikes > 0) await sendDislike(actor, video, t) + return sendVideoRateChangeToOrigin(account, video, likes, dislikes, t) } function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) { @@ -56,35 +28,32 @@ function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVid export { getLocalRateUrl, - createRates, sendVideoRateChange } // --------------------------------------------------------------------------- -async function createRate (rateUrl: string, video: MVideo, rate: VideoRateType) { - // Fetch url - const { body } = await doJSONRequest(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}`) - } +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 - if (checkUrlsSameHost(body.id, rateUrl) !== true) { - throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`) - } + const actor = account.Actor - const actor = await getOrCreateAPActor(actorUrl) + // Keep the order: first we undo and then we create - const entry = { - videoId: video.id, - accountId: actor.Account.id, - type: rate, - url: body.id - } + // Undo Like + if (likes < 0) await sendUndoLike(actor, video, t) + // Undo Dislike + if (dislikes < 0) await sendUndoDislike(actor, video, t) - // Video "likes"/"dislikes" will be updated by the caller - await AccountVideoRateModel.upsert(entry) + // Like + if (likes > 0) await sendLike(actor, video, t) + // Dislike + if (dislikes > 0) await sendDislike(actor, video, t) } 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 { // 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 } + + const response = await doJSONRequest>(uri, options) + const totalItems = response.body.totalItems - return createJob({ uri, videoId: video.id, type: jobType }) + 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(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 } = { '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 } = { - '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 ({ 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>> { }) } - 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 -- 2.41.0