From 63c93323ecdeaa4b6183d75dd3f13469e1ef3ebd Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 24 Nov 2017 13:41:10 +0100 Subject: [PATCH] Correctly forward like/dislikes and undo --- server/lib/activitypub/process/process-add.ts | 2 +- .../lib/activitypub/process/process-create.ts | 42 ++++++---- .../lib/activitypub/process/process-like.ts | 25 ++++-- .../lib/activitypub/process/process-undo.ts | 43 ++++++---- server/lib/activitypub/send/misc.ts | 83 +++++++++++++++---- server/lib/activitypub/send/send-create.ts | 26 +++--- server/lib/activitypub/send/send-like.ts | 14 ++-- server/lib/activitypub/send/send-undo.ts | 38 ++++++--- .../activitypub-http-broadcast-handler.ts | 11 +-- .../activitypub-http-job-scheduler.ts | 27 ++++-- .../activitypub-http-unicast-handler.ts | 10 +-- server/models/account/account-interface.ts | 2 + server/models/account/account.ts | 17 +++- server/models/video/video.ts | 35 ++++---- shared/models/activitypub/activity.ts | 5 ++ 15 files changed, 246 insertions(+), 134 deletions(-) diff --git a/server/lib/activitypub/process/process-add.ts b/server/lib/activitypub/process/process-add.ts index 433e68eb6..98280b9f0 100644 --- a/server/lib/activitypub/process/process-add.ts +++ b/server/lib/activitypub/process/process-add.ts @@ -76,7 +76,7 @@ function addRemoteVideo (account: AccountInstance, if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.') const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoToCreateData.uuid, videoToCreateData.id, t) - if (videoFromDatabase) throw new Error('Video with this UUID/Url already exists.') + if (videoFromDatabase) return videoFromDatabase const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, activity.to, activity.cc) const video = db.Video.build(videoData) diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 147bbd132..1f982598b 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -1,14 +1,14 @@ import { ActivityCreate, VideoChannelObject } from '../../../../shared' +import { DislikeObject } from '../../../../shared/models/activitypub/objects/dislike-object' import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects/video-abuse-object' import { ViewObject } from '../../../../shared/models/activitypub/objects/view-object' import { logger, retryTransactionWrapper } from '../../../helpers' import { database as db } from '../../../initializers' import { AccountInstance } from '../../../models/account/account-interface' import { getOrCreateAccountAndServer } from '../account' -import { sendCreateDislikeToVideoFollowers, sendCreateViewToVideoFollowers } from '../send/send-create' +import { forwardActivity } from '../send/misc' import { getVideoChannelActivityPubUrl } from '../url' import { videoChannelActivityObjectToDBAttributes } from './misc' -import { DislikeObject } from '../../../../shared/models/activitypub/objects/dislike-object' async function processCreateActivity (activity: ActivityCreate) { const activityObject = activity.object @@ -16,9 +16,9 @@ async function processCreateActivity (activity: ActivityCreate) { const account = await getOrCreateAccountAndServer(activity.actor) if (activityType === 'View') { - return processCreateView(activityObject as ViewObject) + return processCreateView(account, activity) } else if (activityType === 'Dislike') { - return processCreateDislike(account, activityObject as DislikeObject) + return processCreateDislike(account, activity) } else if (activityType === 'VideoChannel') { return processCreateVideoChannel(account, activityObject as VideoChannelObject) } else if (activityType === 'Flag') { @@ -37,19 +37,20 @@ export { // --------------------------------------------------------------------------- -async function processCreateDislike (byAccount: AccountInstance, dislike: DislikeObject) { +async function processCreateDislike (byAccount: AccountInstance, activity: ActivityCreate) { const options = { - arguments: [ byAccount, dislike ], + arguments: [ byAccount, activity ], errorMessage: 'Cannot dislike the video with many retries.' } return retryTransactionWrapper(createVideoDislike, options) } -function createVideoDislike (byAccount: AccountInstance, dislike: DislikeObject) { - return db.sequelize.transaction(async t => { - const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object) +function createVideoDislike (byAccount: AccountInstance, activity: ActivityCreate) { + const dislike = activity.object as DislikeObject + return db.sequelize.transaction(async t => { + const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object, t) if (!video) throw new Error('Unknown video ' + dislike.object) const rate = { @@ -59,15 +60,22 @@ function createVideoDislike (byAccount: AccountInstance, dislike: DislikeObject) } const [ , created ] = await db.AccountVideoRate.findOrCreate({ where: rate, - defaults: rate + defaults: rate, + transaction: t }) - await video.increment('dislikes') + await video.increment('dislikes', { transaction: t }) - if (video.isOwned() && created === true) await sendCreateDislikeToVideoFollowers(byAccount, video, undefined) + if (video.isOwned() && created === true) { + // Don't resend the activity to the sender + const exceptions = [ byAccount ] + await forwardActivity(activity, t, exceptions) + } }) } -async function processCreateView (view: ViewObject) { +async function processCreateView (byAccount: AccountInstance, activity: ActivityCreate) { + const view = activity.object as ViewObject + const video = await db.Video.loadByUrlAndPopulateAccount(view.object) if (!video) throw new Error('Unknown video ' + view.object) @@ -77,7 +85,11 @@ async function processCreateView (view: ViewObject) { await video.increment('views') - if (video.isOwned()) await sendCreateViewToVideoFollowers(account, video, undefined) + if (video.isOwned()) { + // Don't resend the activity to the sender + const exceptions = [ byAccount ] + await forwardActivity(activity, undefined, exceptions) + } } function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { @@ -94,7 +106,7 @@ function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateDa return db.sequelize.transaction(async t => { let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t) - if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.') + if (videoChannel) return videoChannel const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account) videoChannel = db.VideoChannel.build(videoChannelData) diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts index d77b30f24..0347f95be 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts @@ -1,14 +1,14 @@ import { ActivityLike } from '../../../../shared/models/activitypub/activity' +import { retryTransactionWrapper } from '../../../helpers/database-utils' import { database as db } from '../../../initializers' import { AccountInstance } from '../../../models/account/account-interface' import { getOrCreateAccountAndServer } from '../account' -import { sendLikeToVideoFollowers } from '../send/send-like' -import { retryTransactionWrapper } from '../../../helpers/database-utils' +import { forwardActivity } from '../send/misc' async function processLikeActivity (activity: ActivityLike) { const account = await getOrCreateAccountAndServer(activity.actor) - return processLikeVideo(account, activity.object) + return processLikeVideo(account, activity) } // --------------------------------------------------------------------------- @@ -19,16 +19,18 @@ export { // --------------------------------------------------------------------------- -async function processLikeVideo (byAccount: AccountInstance, videoUrl: string) { +async function processLikeVideo (byAccount: AccountInstance, activity: ActivityLike) { const options = { - arguments: [ byAccount, videoUrl ], + arguments: [ byAccount, activity ], errorMessage: 'Cannot like the video with many retries.' } return retryTransactionWrapper(createVideoLike, options) } -function createVideoLike (byAccount: AccountInstance, videoUrl: string) { +function createVideoLike (byAccount: AccountInstance, activity: ActivityLike) { + const videoUrl = activity.object + return db.sequelize.transaction(async t => { const video = await db.Video.loadByUrlAndPopulateAccount(videoUrl) @@ -41,10 +43,15 @@ function createVideoLike (byAccount: AccountInstance, videoUrl: string) { } const [ , created ] = await db.AccountVideoRate.findOrCreate({ where: rate, - defaults: rate + defaults: rate, + transaction: t }) - await video.increment('likes') + await video.increment('likes', { transaction: t }) - if (video.isOwned() && created === true) await sendLikeToVideoFollowers(byAccount, video, undefined) + if (video.isOwned() && created === true) { + // Don't resend the activity to the sender + const exceptions = [ byAccount ] + await forwardActivity(activity, t, exceptions) + } }) } diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index 9fe066c01..cc221045f 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts @@ -3,16 +3,15 @@ import { DislikeObject } from '../../../../shared/models/activitypub/objects/dis import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { database as db } from '../../../initializers' -import { sendUndoDislikeToVideoFollowers } from '../index' -import { sendUndoLikeToVideoFollowers } from '../send/send-undo' +import { forwardActivity } from '../send/misc' async function processUndoActivity (activity: ActivityUndo) { const activityToUndo = activity.object if (activityToUndo.type === 'Like') { - return processUndoLike(activity.actor, activityToUndo) + return processUndoLike(activity.actor, activity) } else if (activityToUndo.type === 'Create' && activityToUndo.object.type === 'Dislike') { - return processUndoDislike(activity.actor, activityToUndo.object) + return processUndoDislike(activity.actor, activity) } else if (activityToUndo.type === 'Follow') { return processUndoFollow(activity.actor, activityToUndo) } @@ -30,57 +29,69 @@ export { // --------------------------------------------------------------------------- -function processUndoLike (actor: string, likeActivity: ActivityLike) { +function processUndoLike (actor: string, activity: ActivityUndo) { const options = { - arguments: [ actor, likeActivity ], + arguments: [ actor, activity ], errorMessage: 'Cannot undo like with many retries.' } return retryTransactionWrapper(undoLike, options) } -function undoLike (actor: string, likeActivity: ActivityLike) { +function undoLike (actor: string, activity: ActivityUndo) { + const likeActivity = activity.object as ActivityLike + return db.sequelize.transaction(async t => { const byAccount = await db.Account.loadByUrl(actor, t) if (!byAccount) throw new Error('Unknown account ' + actor) - const video = await db.Video.loadByUrlAndPopulateAccount(likeActivity.object) + const video = await db.Video.loadByUrlAndPopulateAccount(likeActivity.object, t) if (!video) throw new Error('Unknown video ' + likeActivity.actor) const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t) if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`) await rate.destroy({ transaction: t }) - await video.decrement('likes') + await video.decrement('likes', { transaction: t }) - if (video.isOwned()) await sendUndoLikeToVideoFollowers(byAccount, video, t) + if (video.isOwned()) { + // Don't resend the activity to the sender + const exceptions = [ byAccount ] + await forwardActivity(activity, t, exceptions) + } }) } -function processUndoDislike (actor: string, dislikeCreateActivity: DislikeObject) { +function processUndoDislike (actor: string, activity: ActivityUndo) { const options = { - arguments: [ actor, dislikeCreateActivity ], + arguments: [ actor, activity ], errorMessage: 'Cannot undo dislike with many retries.' } return retryTransactionWrapper(undoDislike, options) } -function undoDislike (actor: string, dislike: DislikeObject) { +function undoDislike (actor: string, activity: ActivityUndo) { + const dislike = activity.object.object as DislikeObject + return db.sequelize.transaction(async t => { const byAccount = await db.Account.loadByUrl(actor, t) if (!byAccount) throw new Error('Unknown account ' + actor) - const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object) + const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object, t) if (!video) throw new Error('Unknown video ' + dislike.actor) const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t) if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`) await rate.destroy({ transaction: t }) - await video.decrement('dislikes') + await video.decrement('dislikes', { transaction: t }) - if (video.isOwned()) await sendUndoDislikeToVideoFollowers(byAccount, video, t) + if (video.isOwned()) { + // Don't resend the activity to the sender + const exceptions = [ byAccount ] + await forwardActivity(activity, t, exceptions) + } }) } diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts index 41a039b19..fe137464e 100644 --- a/server/lib/activitypub/send/misc.ts +++ b/server/lib/activitypub/send/misc.ts @@ -2,8 +2,45 @@ import { Transaction } from 'sequelize' import { logger } from '../../../helpers/logger' import { ACTIVITY_PUB, database as db } from '../../../initializers' import { AccountInstance } from '../../../models/account/account-interface' -import { activitypubHttpJobScheduler } from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler' +import { + activitypubHttpJobScheduler, + ActivityPubHttpPayload +} from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler' import { VideoInstance } from '../../../models/video/video-interface' +import { Activity } from '../../../../shared/models/activitypub/activity' + +async function forwardActivity ( + activity: Activity, + t: Transaction, + followersException: AccountInstance[] = [] +) { + const to = activity.to || [] + const cc = activity.cc || [] + + const followersUrls: string[] = [] + for (const dest of to.concat(cc)) { + if (dest.endsWith('/followers')) { + followersUrls.push(dest) + } + } + + const toAccountFollowers = await db.Account.listByFollowersUrls(followersUrls) + const uris = await computeFollowerUris(toAccountFollowers, followersException) + + if (uris.length === 0) { + logger.info('0 followers for %s, no forwarding.', toAccountFollowers.map(a => a.id).join(', ')) + return + } + + logger.debug('Creating forwarding job.', { uris }) + + const jobPayload: ActivityPubHttpPayload = { + uris, + body: activity + } + + return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload) +} async function broadcastToFollowers ( data: any, @@ -12,18 +49,15 @@ async function broadcastToFollowers ( t: Transaction, followersException: AccountInstance[] = [] ) { - const toAccountFollowerIds = toAccountFollowers.map(a => a.id) - - const result = await db.AccountFollow.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds) - if (result.data.length === 0) { - logger.info('Not broadcast because of 0 followers for %s.', toAccountFollowerIds.join(', ')) - return undefined + const uris = await computeFollowerUris(toAccountFollowers, followersException) + if (uris.length === 0) { + logger.info('0 followers for %s, no broadcasting.', toAccountFollowers.map(a => a.id).join(', ')) + return } - const followersSharedInboxException = followersException.map(f => f.sharedInboxUrl) - const uris = result.data.filter(sharedInbox => followersSharedInboxException.indexOf(sharedInbox) === -1) + logger.debug('Creating broadcast job.', { uris }) - const jobPayload = { + const jobPayload: ActivityPubHttpPayload = { uris, signatureAccountId: byAccount.id, body: data @@ -33,7 +67,9 @@ async function broadcastToFollowers ( } async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: string, t: Transaction) { - const jobPayload = { + logger.debug('Creating unicast job.', { uri: toAccountUrl }) + + const jobPayload: ActivityPubHttpPayload = { uris: [ toAccountUrl ], signatureAccountId: byAccount.id, body: data @@ -42,21 +78,21 @@ async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: s return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload) } -function getOriginVideoAudience (video: VideoInstance) { +function getOriginVideoAudience (video: VideoInstance, accountsInvolvedInVideo: AccountInstance[]) { return { to: [ video.VideoChannel.Account.url ], - cc: [ video.VideoChannel.Account.url + '/followers' ] + cc: accountsInvolvedInVideo.map(a => a.followersUrl) } } -function getVideoFollowersAudience (video: VideoInstance) { +function getVideoFollowersAudience (accountsInvolvedInVideo: AccountInstance[]) { return { - to: [ video.VideoChannel.Account.url + '/followers' ], + to: accountsInvolvedInVideo.map(a => a.followersUrl), cc: [] } } -async function getAccountsToForwardVideoAction (byAccount: AccountInstance, video: VideoInstance) { +async function getAccountsInvolvedInVideo (video: VideoInstance) { const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id) accountsToForwardView.push(video.VideoChannel.Account) @@ -81,6 +117,16 @@ async function getAudience (accountSender: AccountInstance, isPublic = true) { return { to, cc } } +async function computeFollowerUris (toAccountFollower: AccountInstance[], followersException: AccountInstance[]) { + const toAccountFollowerIds = toAccountFollower.map(a => a.id) + + const result = await db.AccountFollow.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds) + const followersSharedInboxException = followersException.map(f => f.sharedInboxUrl) + const uris = result.data.filter(sharedInbox => followersSharedInboxException.indexOf(sharedInbox) === -1) + + return uris +} + // --------------------------------------------------------------------------- export { @@ -88,6 +134,7 @@ export { unicastTo, getAudience, getOriginVideoAudience, - getAccountsToForwardVideoAction, - getVideoFollowersAudience + getAccountsInvolvedInVideo, + getVideoFollowersAudience, + forwardActivity } diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 6afe67ee6..113d89233 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -1,12 +1,12 @@ import { Transaction } from 'sequelize' -import { ActivityCreate } from '../../../../shared/models/activitypub/activity' +import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub/activity' import { getServerAccount } from '../../../helpers/utils' import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface' import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' import { broadcastToFollowers, - getAccountsToForwardVideoAction, + getAccountsInvolvedInVideo, getAudience, getOriginVideoAudience, getVideoFollowersAudience, @@ -35,7 +35,8 @@ async function sendCreateViewToOrigin (byAccount: AccountInstance, video: VideoI const url = getVideoViewActivityPubUrl(byAccount, video) const viewActivity = createViewActivityData(byAccount, video) - const audience = getOriginVideoAudience(video) + const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video) + const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) const data = await createActivityData(url, byAccount, viewActivity, audience) return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) @@ -45,12 +46,12 @@ async function sendCreateViewToVideoFollowers (byAccount: AccountInstance, video const url = getVideoViewActivityPubUrl(byAccount, video) const viewActivity = createViewActivityData(byAccount, video) - const audience = getVideoFollowersAudience(video) + const accountsToForwardView = await getAccountsInvolvedInVideo(video) + const audience = getVideoFollowersAudience(accountsToForwardView) const data = await createActivityData(url, byAccount, viewActivity, audience) + // Use the server account to send the view, because it could be an unregistered account const serverAccount = await getServerAccount() - const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video) - const followersException = [ byAccount ] return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) } @@ -59,7 +60,8 @@ async function sendCreateDislikeToOrigin (byAccount: AccountInstance, video: Vid const url = getVideoDislikeActivityPubUrl(byAccount, video) const dislikeActivity = createDislikeActivityData(byAccount, video) - const audience = getOriginVideoAudience(video) + const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video) + const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) const data = await createActivityData(url, byAccount, dislikeActivity, audience) return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) @@ -69,17 +71,15 @@ async function sendCreateDislikeToVideoFollowers (byAccount: AccountInstance, vi const url = getVideoDislikeActivityPubUrl(byAccount, video) const dislikeActivity = createDislikeActivityData(byAccount, video) - const audience = getVideoFollowersAudience(video) + const accountsToForwardView = await getAccountsInvolvedInVideo(video) + const audience = getVideoFollowersAudience(accountsToForwardView) const data = await createActivityData(url, byAccount, dislikeActivity, audience) - const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video) - const serverAccount = await getServerAccount() - const followersException = [ byAccount ] - return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) + return broadcastToFollowers(data, byAccount, accountsToForwardView, t, followersException) } -async function createActivityData (url: string, byAccount: AccountInstance, object: any, audience?: { to: string[], cc: string[] }) { +async function createActivityData (url: string, byAccount: AccountInstance, object: any, audience?: ActivityAudience) { if (!audience) { audience = await getAudience(byAccount) } diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index 70a7d886f..8ca775bf3 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts @@ -1,11 +1,10 @@ import { Transaction } from 'sequelize' import { ActivityLike } from '../../../../shared/models/activitypub/activity' -import { getServerAccount } from '../../../helpers/utils' import { AccountInstance, VideoInstance } from '../../../models' import { getVideoLikeActivityPubUrl } from '../url' import { broadcastToFollowers, - getAccountsToForwardVideoAction, + getAccountsInvolvedInVideo, getAudience, getOriginVideoAudience, getVideoFollowersAudience, @@ -15,7 +14,8 @@ import { async function sendLikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { const url = getVideoLikeActivityPubUrl(byAccount, video) - const audience = getOriginVideoAudience(video) + const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video) + const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) const data = await likeActivityData(url, byAccount, video, audience) return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) @@ -24,14 +24,14 @@ async function sendLikeToOrigin (byAccount: AccountInstance, video: VideoInstanc async function sendLikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { const url = getVideoLikeActivityPubUrl(byAccount, video) - const audience = getVideoFollowersAudience(video) + const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video) + const audience = getVideoFollowersAudience(accountsInvolvedInVideo) const data = await likeActivityData(url, byAccount, video, audience) - const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video) - const serverAccount = await getServerAccount() + const toAccountsFollowers = await getAccountsInvolvedInVideo(video) const followersException = [ byAccount ] - return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) + return broadcastToFollowers(data, byAccount, toAccountsFollowers, t, followersException) } async function likeActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, audience?: { to: string[], cc: string[] }) { diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 8f46a051e..79fc113f0 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts @@ -1,11 +1,16 @@ import { Transaction } from 'sequelize' -import { ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub/activity' -import { getServerAccount } from '../../../helpers/utils' +import { + ActivityAudience, + ActivityCreate, + ActivityFollow, + ActivityLike, + ActivityUndo +} from '../../../../shared/models/activitypub/activity' import { AccountInstance } from '../../../models' import { AccountFollowInstance } from '../../../models/account/account-follow-interface' import { VideoInstance } from '../../../models/video/video-interface' import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' -import { broadcastToFollowers, getAccountsToForwardVideoAction, unicastTo } from './misc' +import { broadcastToFollowers, getAccountsInvolvedInVideo, getAudience, getVideoFollowersAudience, unicastTo } from './misc' import { createActivityData, createDislikeActivityData } from './send-create' import { followActivityData } from './send-follow' import { likeActivityData } from './send-like' @@ -37,14 +42,13 @@ async function sendUndoLikeToVideoFollowers (byAccount: AccountInstance, video: const likeUrl = getVideoLikeActivityPubUrl(byAccount, video) const undoUrl = getUndoActivityPubUrl(likeUrl) + const toAccountsFollowers = await getAccountsInvolvedInVideo(video) + const audience = getVideoFollowersAudience(toAccountsFollowers) const object = await likeActivityData(likeUrl, byAccount, video) - const data = await undoActivityData(undoUrl, byAccount, object) - - const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video) - const serverAccount = await getServerAccount() + const data = await undoActivityData(undoUrl, byAccount, object, audience) const followersException = [ byAccount ] - return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) + return broadcastToFollowers(data, byAccount, toAccountsFollowers, t, followersException) } async function sendUndoDislikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { @@ -68,11 +72,10 @@ async function sendUndoDislikeToVideoFollowers (byAccount: AccountInstance, vide const data = await undoActivityData(undoUrl, byAccount, object) - const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video) - const serverAccount = await getServerAccount() + const toAccountsFollowers = await getAccountsInvolvedInVideo(video) const followersException = [ byAccount ] - return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) + return broadcastToFollowers(data, byAccount, toAccountsFollowers, t, followersException) } // --------------------------------------------------------------------------- @@ -87,11 +90,22 @@ export { // --------------------------------------------------------------------------- -async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow | ActivityLike | ActivityCreate) { +async function undoActivityData ( + url: string, + byAccount: AccountInstance, + object: ActivityFollow | ActivityLike | ActivityCreate, + audience?: ActivityAudience +) { + if (!audience) { + audience = await getAudience(byAccount) + } + const activity: ActivityUndo = { type: 'Undo', id: url, actor: byAccount.url, + to: audience.to, + cc: audience.cc, object } diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts index 5b4c65b81..49d4bf5c6 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts @@ -1,21 +1,16 @@ import { logger } from '../../../helpers' -import { buildSignedActivity } from '../../../helpers/activitypub' import { doRequest } from '../../../helpers/requests' -import { database as db } from '../../../initializers' -import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler' +import { ActivityPubHttpPayload, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler' async function process (payload: ActivityPubHttpPayload, jobId: number) { logger.info('Processing ActivityPub broadcast in job %d.', jobId) - const accountSignature = await db.Account.load(payload.signatureAccountId) - if (!accountSignature) throw new Error('Unknown signature account id.') - - const signedBody = await buildSignedActivity(accountSignature, payload.body) + const body = await computeBody(payload) const options = { method: 'POST', uri: '', - json: signedBody + json: body } for (const uri of payload.uris) { diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts index ccf109935..f1fe774cc 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts @@ -1,11 +1,13 @@ -import { JobScheduler, JobHandler } from '../job-scheduler' +import { JobCategory } from '../../../../shared' +import { buildSignedActivity } from '../../../helpers/activitypub' +import { logger } from '../../../helpers/logger' +import { ACTIVITY_PUB } from '../../../initializers/constants' +import { database as db } from '../../../initializers/database' +import { JobHandler, JobScheduler } from '../job-scheduler' import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-handler' -import * as activitypubHttpUnicastHandler from './activitypub-http-unicast-handler' import * as activitypubHttpFetcherHandler from './activitypub-http-fetcher-handler' -import { JobCategory } from '../../../../shared' -import { ACTIVITY_PUB } from '../../../initializers/constants' -import { logger } from '../../../helpers/logger' +import * as activitypubHttpUnicastHandler from './activitypub-http-unicast-handler' type ActivityPubHttpPayload = { uris: string[] @@ -40,8 +42,21 @@ function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, ur } } +async function computeBody (payload: ActivityPubHttpPayload) { + let body = payload.body + + if (payload.signatureAccountId) { + const accountSignature = await db.Account.load(payload.signatureAccountId) + if (!accountSignature) throw new Error('Unknown signature account id.') + body = await buildSignedActivity(accountSignature, payload.body) + } + + return body +} + export { ActivityPubHttpPayload, activitypubHttpJobScheduler, - maybeRetryRequestLater + maybeRetryRequestLater, + computeBody } diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts index f7f3dabbd..4c95197c4 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts @@ -1,21 +1,17 @@ import { logger } from '../../../helpers' import { doRequest } from '../../../helpers/requests' -import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler' -import { database as db } from '../../../initializers/database' -import { buildSignedActivity } from '../../../helpers/activitypub' +import { ActivityPubHttpPayload, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler' async function process (payload: ActivityPubHttpPayload, jobId: number) { logger.info('Processing ActivityPub unicast in job %d.', jobId) - const accountSignature = await db.Account.load(payload.signatureAccountId) - if (!accountSignature) throw new Error('Unknown signature account id.') + const body = await computeBody(payload) - const signedBody = await buildSignedActivity(accountSignature, payload.body) const uri = payload.uris[0] const options = { method: 'POST', uri, - json: signedBody + json: body } try { diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts index 2e4b0382d..6fc98ba45 100644 --- a/server/models/account/account-interface.ts +++ b/server/models/account/account-interface.ts @@ -12,6 +12,7 @@ export namespace AccountMethods { export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird export type LoadLocalByName = (name: string) => Bluebird export type LoadByNameAndHost = (name: string, host: string) => Bluebird + export type ListByFollowersUrls = (followerUrls: string[], transaction?: Sequelize.Transaction) => Bluebird export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount @@ -29,6 +30,7 @@ export interface AccountClass { loadByUrl: AccountMethods.LoadByUrl loadLocalByName: AccountMethods.LoadLocalByName loadByNameAndHost: AccountMethods.LoadByNameAndHost + listByFollowersUrls: AccountMethods.ListByFollowersUrls } export interface AccountAttributes { diff --git a/server/models/account/account.ts b/server/models/account/account.ts index f2bd325f3..fff3ce087 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -26,6 +26,7 @@ let loadByUUID: AccountMethods.LoadByUUID let loadByUrl: AccountMethods.LoadByUrl let loadLocalByName: AccountMethods.LoadLocalByName let loadByNameAndHost: AccountMethods.LoadByNameAndHost +let listByFollowersUrls: AccountMethods.ListByFollowersUrls let isOwned: AccountMethods.IsOwned let toActivityPubObject: AccountMethods.ToActivityPubObject let toFormattedJSON: AccountMethods.ToFormattedJSON @@ -188,7 +189,8 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes loadByUUID, loadByUrl, loadLocalByName, - loadByNameAndHost + loadByNameAndHost, + listByFollowersUrls ] const instanceMethods = [ isOwned, @@ -427,3 +429,16 @@ loadByUrl = function (url: string, transaction?: Sequelize.Transaction) { return Account.findOne(query) } + +listByFollowersUrls = function (followersUrls: string[], transaction?: Sequelize.Transaction) { + const query: Sequelize.FindOptions = { + where: { + followersUrl: { + [Sequelize.Op.in]: followersUrls + } + }, + transaction + } + + return Account.findAll(query) +} diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e5fd92549..82b95c489 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -6,27 +6,8 @@ import { join } from 'path' import * as Sequelize from 'sequelize' import { VideoPrivacy, VideoResolution } from '../../../shared' import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' -import { - createTorrentPromise, - generateImageFromVideoFile, - getVideoFileHeight, - isVideoCategoryValid, - isVideoDescriptionValid, - isVideoDurationValid, - isVideoLanguageValid, - isVideoLicenceValid, - isVideoNameValid, - isVideoNSFWValid, - isVideoPrivacyValid, - logger, - renamePromise, - statPromise, - transcode, - unlinkPromise, - writeFilePromise -} from '../../helpers' import { activityPubCollection } from '../../helpers/activitypub' -import { isVideoUrlValid } from '../../helpers/custom-validators/videos' +import { isVideoCategoryValid, isVideoLanguageValid, isVideoPrivacyValid, isVideoUrlValid } from '../../helpers/custom-validators/videos' import { API_VERSION, CONFIG, @@ -39,7 +20,7 @@ import { VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES -} from '../../initializers' +} from '../../initializers/constants' import { sendDeleteVideo } from '../../lib/index' import { addMethodsToModel, getSort } from '../utils' @@ -47,6 +28,10 @@ import { addMethodsToModel, getSort } from '../utils' import { TagInstance } from './tag-interface' import { VideoFileInstance, VideoFileModel } from './video-file-interface' import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface' +import { isVideoNameValid, isVideoLicenceValid, isVideoNSFWValid, isVideoDescriptionValid, isVideoDurationValid } from '../../helpers/index' +import { logger } from '../../helpers/logger' +import { generateImageFromVideoFile, transcode, getVideoFileHeight } from '../../helpers/ffmpeg-utils' +import { createTorrentPromise, writeFilePromise, unlinkPromise, renamePromise, statPromise } from '../../helpers/core-utils' let Video: Sequelize.Model let getOriginalFile: VideoMethods.GetOriginalFile @@ -1013,6 +998,10 @@ loadAndPopulateAccountAndServerAndTags = function (id: number) { model: Video['sequelize'].models.AccountVideoRate, include: [ Video['sequelize'].models.Account ] }, + { + model: Video['sequelize'].models.VideoShare, + include: [ Video['sequelize'].models.Account ] + }, Video['sequelize'].models.Tag, Video['sequelize'].models.VideoFile ] @@ -1040,6 +1029,10 @@ loadByUUIDAndPopulateAccountAndServerAndTags = function (uuid: string) { model: Video['sequelize'].models.AccountVideoRate, include: [ Video['sequelize'].models.Account ] }, + { + model: Video['sequelize'].models.VideoShare, + include: [ Video['sequelize'].models.Account ] + }, Video['sequelize'].models.Tag, Video['sequelize'].models.VideoFile ] diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index cbfd6157a..37f5400b9 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -10,6 +10,11 @@ export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like' +export interface ActivityAudience { + to: string[] + cc: string[] +} + export interface BaseActivity { '@context'?: any[] id: string