From 57e4e1c1a95c3a81a967f54ecc2a510d8b0e129c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 18 Mar 2022 11:17:35 +0100 Subject: Don't store remote rates of remote videos In the future we'll stop to expose all available rates to improve users privacy --- server/controllers/activitypub/client.ts | 2 + server/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 +------ server/lib/activitypub/process/process-announce.ts | 2 +- server/lib/activitypub/process/process-create.ts | 4 +- server/lib/activitypub/process/process-delete.ts | 2 +- server/lib/activitypub/process/process-dislike.ts | 20 +- server/lib/activitypub/process/process-like.ts | 21 +- server/lib/activitypub/process/process-undo.ts | 61 +++--- server/lib/activitypub/process/process-update.ts | 2 +- server/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 +- .../lib/activitypub/send/shared/audience-utils.ts | 74 +++++++ server/lib/activitypub/send/shared/index.ts | 2 + server/lib/activitypub/send/shared/send-utils.ts | 232 +++++++++++++++++++++ server/lib/activitypub/send/utils.ts | 214 ------------------- 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 +- .../lib/job-queue/handlers/activitypub-cleaner.ts | 2 +- .../job-queue/handlers/activitypub-http-fetcher.ts | 6 - .../job-queue/handlers/activitypub-refresher.ts | 2 +- .../lib/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 +- 42 files changed, 578 insertions(+), 516 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 create mode 100644 server/lib/activitypub/send/shared/send-utils.ts delete mode 100644 server/lib/activitypub/send/utils.ts (limited to '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/shared/send-utils.ts b/server/lib/activitypub/send/shared/send-utils.ts new file mode 100644 index 000000000..9e8f12fa8 --- /dev/null +++ b/server/lib/activitypub/send/shared/send-utils.ts @@ -0,0 +1,232 @@ +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 { 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 + video: MVideoImmutable | MVideoAccountLight + transaction?: Transaction + contextType?: ContextType +}) { + const { byActor, video, transaction, contextType } = options + + const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction) + + // Send to origin + if (video.isOwned() === false) { + return sendVideoActivityToOrigin(activityBuilder, options) + } + + // Send to followers + const audience = getAudienceFromFollowersOf(actorsInvolvedInVideo) + const activity = activityBuilder(audience) + + const actorsException = [ byActor ] + + 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, + followersException: MActorWithInboxes[], + video: MVideoId +) { + // Mastodon does not add our announces in audience, so we forward to them manually + const additionalActors = await getActorsInvolvedInVideo(video, t) + const additionalFollowerUrls = additionalActors.map(a => a.followersUrl) + + return forwardActivity(activity, t, followersException, additionalFollowerUrls) +} + +async function forwardActivity ( + activity: Activity, + t: Transaction, + followersException: MActorWithInboxes[] = [], + additionalFollowerUrls: string[] = [] +) { + logger.info('Forwarding activity %s.', activity.id) + + const to = activity.to || [] + const cc = activity.cc || [] + + const followersUrls = additionalFollowerUrls + for (const dest of to.concat(cc)) { + if (dest.endsWith('/followers')) { + followersUrls.push(dest) + } + } + + const toActorFollowers = await ActorModel.listByFollowersUrls(followersUrls, t) + const uris = await computeFollowerUris(toActorFollowers, followersException, t) + + if (uris.length === 0) { + logger.info('0 followers for %s, no forwarding.', toActorFollowers.map(a => a.id).join(', ')) + return undefined + } + + logger.debug('Creating forwarding job.', { uris }) + + const payload = { + uris, + body: activity + } + return afterCommitIfTransaction(t, () => JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })) +} + +// --------------------------------------------------------------------------- + +async function broadcastToFollowers ( + data: any, + byActor: MActorId, + toFollowersOf: MActorId[], + t: Transaction, + actorsException: MActorWithInboxes[] = [], + contextType?: ContextType +) { + const uris = await computeFollowerUris(toFollowersOf, actorsException, t) + + return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType)) +} + +async function broadcastToActors ( + data: any, + byActor: MActorId, + toActors: MActor[], + t?: Transaction, + actorsException: MActorWithInboxes[] = [], + contextType?: ContextType +) { + const uris = await computeUris(toActors, actorsException) + return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType)) +} + +function broadcastTo (uris: string[], data: any, byActor: MActorId, contextType?: ContextType) { + if (uris.length === 0) return undefined + + const broadcastUris: string[] = [] + const unicastUris: string[] = [] + + // Bad URIs could be slow to respond, prefer to process them in a dedicated queue + for (const uri of uris) { + if (ActorFollowHealthCache.Instance.isBadInbox(uri)) { + unicastUris.push(uri) + } else { + broadcastUris.push(uri) + } + } + + logger.debug('Creating broadcast job.', { broadcastUris, unicastUris }) + + if (broadcastUris.length !== 0) { + const payload = { + uris: broadcastUris, + signatureActorId: byActor.id, + body: data, + contextType + } + + JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }) + } + + for (const unicastUri of unicastUris) { + const payload = { + uri: unicastUri, + signatureActorId: byActor.id, + body: data, + contextType + } + + JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload }) + } +} + +function unicastTo (data: any, byActor: MActorId, toActorUrl: string, contextType?: ContextType) { + logger.debug('Creating unicast job.', { uri: toActorUrl }) + + const payload = { + uri: toActorUrl, + signatureActorId: byActor.id, + body: data, + contextType + } + + JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload }) +} + +// --------------------------------------------------------------------------- + +export { + broadcastToFollowers, + unicastTo, + forwardActivity, + broadcastToActors, + sendVideoActivityToOrigin, + forwardVideoRelatedActivity, + sendVideoRelatedActivity +} + +// --------------------------------------------------------------------------- + +async function computeFollowerUris (toFollowersOf: MActorId[], actorsException: MActorWithInboxes[], t: Transaction) { + const toActorFollowerIds = toFollowersOf.map(a => a.id) + + const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) + const sharedInboxesException = await buildSharedInboxesException(actorsException) + + return result.data.filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false) +} + +async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) { + const serverActor = await getServerActor() + const targetUrls = toActors + .filter(a => a.id !== serverActor.id) // Don't send to ourselves + .map(a => a.getSharedInbox()) + + const toActorSharedInboxesSet = new Set(targetUrls) + + const sharedInboxesException = await buildSharedInboxesException(actorsException) + return Array.from(toActorSharedInboxesSet) + .filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false) +} + +async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) { + const serverActor = await getServerActor() + + return actorsException + .map(f => f.getSharedInbox()) + .concat([ serverActor.sharedInboxUrl ]) +} diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts deleted file mode 100644 index 7729703b8..000000000 --- a/server/lib/activitypub/send/utils.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { Transaction } from 'sequelize' -import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache' -import { getServerActor } from '@server/models/application/application' -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' - -async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { - byActor: MActorLight - video: MVideoImmutable | MVideoAccountLight - transaction?: Transaction - contextType?: ContextType -}) { - const { byActor, video, transaction, contextType } = options - - const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction) - - // 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) - }) - } - - // Send to followers - const audience = getAudienceFromFollowersOf(actorsInvolvedInVideo) - const activity = activityBuilder(audience) - - const actorsException = [ byActor ] - - return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType) -} - -async function forwardVideoRelatedActivity ( - activity: Activity, - t: Transaction, - followersException: MActorWithInboxes[], - video: MVideoId -) { - // Mastodon does not add our announces in audience, so we forward to them manually - const additionalActors = await getActorsInvolvedInVideo(video, t) - const additionalFollowerUrls = additionalActors.map(a => a.followersUrl) - - return forwardActivity(activity, t, followersException, additionalFollowerUrls) -} - -async function forwardActivity ( - activity: Activity, - t: Transaction, - followersException: MActorWithInboxes[] = [], - additionalFollowerUrls: string[] = [] -) { - logger.info('Forwarding activity %s.', activity.id) - - const to = activity.to || [] - const cc = activity.cc || [] - - const followersUrls = additionalFollowerUrls - for (const dest of to.concat(cc)) { - if (dest.endsWith('/followers')) { - followersUrls.push(dest) - } - } - - const toActorFollowers = await ActorModel.listByFollowersUrls(followersUrls, t) - const uris = await computeFollowerUris(toActorFollowers, followersException, t) - - if (uris.length === 0) { - logger.info('0 followers for %s, no forwarding.', toActorFollowers.map(a => a.id).join(', ')) - return undefined - } - - logger.debug('Creating forwarding job.', { uris }) - - const payload = { - uris, - body: activity - } - return afterCommitIfTransaction(t, () => JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })) -} - -async function broadcastToFollowers ( - data: any, - byActor: MActorId, - toFollowersOf: MActorId[], - t: Transaction, - actorsException: MActorWithInboxes[] = [], - contextType?: ContextType -) { - const uris = await computeFollowerUris(toFollowersOf, actorsException, t) - - return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType)) -} - -async function broadcastToActors ( - data: any, - byActor: MActorId, - toActors: MActor[], - t?: Transaction, - actorsException: MActorWithInboxes[] = [], - contextType?: ContextType -) { - const uris = await computeUris(toActors, actorsException) - return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType)) -} - -function broadcastTo (uris: string[], data: any, byActor: MActorId, contextType?: ContextType) { - if (uris.length === 0) return undefined - - const broadcastUris: string[] = [] - const unicastUris: string[] = [] - - // Bad URIs could be slow to respond, prefer to process them in a dedicated queue - for (const uri of uris) { - if (ActorFollowHealthCache.Instance.isBadInbox(uri)) { - unicastUris.push(uri) - } else { - broadcastUris.push(uri) - } - } - - logger.debug('Creating broadcast job.', { broadcastUris, unicastUris }) - - if (broadcastUris.length !== 0) { - const payload = { - uris: broadcastUris, - signatureActorId: byActor.id, - body: data, - contextType - } - - JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }) - } - - for (const unicastUri of unicastUris) { - const payload = { - uri: unicastUri, - signatureActorId: byActor.id, - body: data, - contextType - } - - JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload }) - } -} - -function unicastTo (data: any, byActor: MActorId, toActorUrl: string, contextType?: ContextType) { - logger.debug('Creating unicast job.', { uri: toActorUrl }) - - const payload = { - uri: toActorUrl, - signatureActorId: byActor.id, - body: data, - contextType - } - - JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload }) -} - -// --------------------------------------------------------------------------- - -export { - broadcastToFollowers, - unicastTo, - forwardActivity, - broadcastToActors, - forwardVideoRelatedActivity, - sendVideoRelatedActivity -} - -// --------------------------------------------------------------------------- - -async function computeFollowerUris (toFollowersOf: MActorId[], actorsException: MActorWithInboxes[], t: Transaction) { - const toActorFollowerIds = toFollowersOf.map(a => a.id) - - const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) - const sharedInboxesException = await buildSharedInboxesException(actorsException) - - return result.data.filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false) -} - -async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) { - const serverActor = await getServerActor() - const targetUrls = toActors - .filter(a => a.id !== serverActor.id) // Don't send to ourselves - .map(a => a.getSharedInbox()) - - const toActorSharedInboxesSet = new Set(targetUrls) - - const sharedInboxesException = await buildSharedInboxesException(actorsException) - return Array.from(toActorSharedInboxesSet) - .filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false) -} - -async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) { - const serverActor = await getServerActor() - - return actorsException - .map(f => f.getSharedInbox()) - .concat([ serverActor.sharedInboxUrl ]) -} 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`) } } }) -- cgit v1.2.3