Correctly forward like/dislikes and undo
authorChocobozzz <florian.bigard@gmail.com>
Fri, 24 Nov 2017 12:41:10 +0000 (13:41 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 18:40:53 +0000 (19:40 +0100)
15 files changed:
server/lib/activitypub/process/process-add.ts
server/lib/activitypub/process/process-create.ts
server/lib/activitypub/process/process-like.ts
server/lib/activitypub/process/process-undo.ts
server/lib/activitypub/send/misc.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/send-like.ts
server/lib/activitypub/send/send-undo.ts
server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts
server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts
server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts
server/models/account/account-interface.ts
server/models/account/account.ts
server/models/video/video.ts
shared/models/activitypub/activity.ts

index 433e68eb6c8a6dadffc01284515e5ebb069fa5ae..98280b9f05fe490a9bcb1abdc1dd5c8cbc853ed0 100644 (file)
@@ -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)
index 147bbd1328b69b2085036cd6c264b4859ac472d1..1f982598bca7af981de7edd9c643e71bfdecbee7 100644 (file)
@@ -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)
index d77b30f2449aa0cee284422b0aa2cc6041d5c477..0347f95be6648d8f3dbb825f0dd9108523bb8110 100644 (file)
@@ -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)
+    }
   })
 }
index 9fe066c01422fb6ddfd20620f9726ddafe6b6f60..cc221045f972c453f2d4a19a46fd7b2fe7f5f86f 100644 (file)
@@ -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)
+    }
   })
 }
 
index 41a039b1924e4540d52bbcc03eafb88b930999f6..fe137464eb57ec587589bf010379ccfa001f4c78 100644 (file)
@@ -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
 }
index 6afe67ee6737e7eb027093094ae6d6dce7503cac..113d892335a667bc3e1cbcfcf3c5fb3d82cf96dd 100644 (file)
@@ -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)
   }
index 70a7d886feb8453f9816790223a8d817a80eee98..8ca775bf3d77066779e28663a39bc26ef1258829 100644 (file)
@@ -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[] }) {
index 8f46a051e192f646e64c071ac9b5419a7e3d6695..79fc113f05e7e459e1f8b8499107c5b0b8a0a45c 100644 (file)
@@ -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
   }
 
index 5b4c65b81480e90cd3704c80c8fd187a56669b33..49d4bf5c6b4cfcfc7db412cd5ef57f7532493fa5 100644 (file)
@@ -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) {
index ccf1099350e13505f23526a3bf956d1d139bec5e..f1fe774cc9d75912799d638b57d93f980914144e 100644 (file)
@@ -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
 }
index f7f3dabbd383be0e852f8483c3672d8885f7aeb8..4c95197c417d89e1ee072af6029e667d468d5b1e 100644 (file)
@@ -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 {
index 2e4b0382d176880487152bba3b3572204d3d128f..6fc98ba45c4b4112dc19d987f23fc4fc15575767 100644 (file)
@@ -12,6 +12,7 @@ export namespace AccountMethods {
   export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird<AccountInstance>
   export type LoadLocalByName = (name: string) => Bluebird<AccountInstance>
   export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance>
+  export type ListByFollowersUrls = (followerUrls: string[], transaction?: Sequelize.Transaction) => Bluebird<AccountInstance[]>
 
   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 {
index f2bd325f3980621ef49b099516622aafec455c7d..fff3ce08799b147062c8f6df2dad8a33ca0da283 100644 (file)
@@ -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<AccountAttributes> = {
+    where: {
+      followersUrl: {
+        [Sequelize.Op.in]: followersUrls
+      }
+    },
+    transaction
+  }
+
+  return Account.findAll(query)
+}
index e5fd925490d48c4608ff01ed5fe44ea7b30dec58..82b95c4894222c47e81087b9d0f14c5dacb721f6 100644 (file)
@@ -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<VideoInstance, VideoAttributes>
 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
     ]
index cbfd6157a3723befa97b79d70dbff6828953bb42..37f5400b90668fdb6bcde341a922e262558a350f 100644 (file)
@@ -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