]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Federate likes/dislikes
authorChocobozzz <florian.bigard@gmail.com>
Thu, 23 Nov 2017 13:19:55 +0000 (14:19 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 18:40:53 +0000 (19:40 +0100)
27 files changed:
server/controllers/api/videos/rate.ts
server/helpers/custom-validators/activitypub/activity.ts
server/helpers/custom-validators/activitypub/announce.ts
server/helpers/custom-validators/activitypub/rate.ts [new file with mode: 0644]
server/helpers/custom-validators/activitypub/undo.ts
server/initializers/constants.ts
server/lib/activitypub/process/index.ts
server/lib/activitypub/process/process-create.ts
server/lib/activitypub/process/process-like.ts [new file with mode: 0644]
server/lib/activitypub/process/process-undo.ts
server/lib/activitypub/process/process.ts
server/lib/activitypub/send/index.ts
server/lib/activitypub/send/misc.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/send-like.ts [new file with mode: 0644]
server/lib/activitypub/send/send-undo.ts
server/lib/activitypub/url.ts
server/lib/activitypub/videos.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-follow-interface.ts
server/models/account/account-follow.ts
server/tests/api/index-slow.ts
shared/models/activitypub/activity.ts
shared/models/activitypub/objects/dislike-object.ts [new file with mode: 0644]
shared/models/activitypub/objects/index.ts

index 8216dffd297c7f1dcd8d244247e64f524aa148eb..134284df7d7cf168bdb80523f9b6e07793551e9a 100644 (file)
@@ -3,6 +3,7 @@ import { UserVideoRateUpdate } from '../../../../shared'
 import { logger, retryTransactionWrapper } from '../../../helpers'
 import { VIDEO_RATE_TYPES } from '../../../initializers'
 import { database as db } from '../../../initializers/database'
+import { sendVideoRateChangeToFollowers, sendVideoRateChangeToOrigin } from '../../../lib/activitypub/videos'
 import { asyncMiddleware, authenticate, videoRateValidator } from '../../../middlewares'
 import { AccountInstance } from '../../../models/account/account-interface'
 import { VideoInstance } from '../../../models/video/video-interface'
@@ -82,10 +83,10 @@ async function rateVideo (req: express.Request, res: express.Response) {
     // It is useful for the user to have a feedback
     await videoInstance.increment(incrementQuery, sequelizeOptions)
 
-    if (videoInstance.isOwned() === false) {
-      // TODO: Send a event to original server
+    if (videoInstance.isOwned()) {
+      await sendVideoRateChangeToFollowers(accountInstance, videoInstance, likesToIncrement, dislikesToIncrement, t)
     } else {
-      // TODO: Send update to followers
+      await sendVideoRateChangeToOrigin(accountInstance, videoInstance, likesToIncrement, dislikesToIncrement, t)
     }
   })
 
index 66e557d39cbf164641d3f2a05e4c05bcccf86926..3a0e8197c958ce616d274dd89dfc03358342277d 100644 (file)
@@ -1,9 +1,9 @@
 import * as validator from 'validator'
 import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity'
 import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account'
-import { isAnnounceValid } from './announce'
+import { isAnnounceActivityValid } from './announce'
 import { isActivityPubUrlValid } from './misc'
-import { isUndoValid } from './undo'
+import { isUndoActivityValid } from './undo'
 import { isVideoChannelCreateActivityValid, isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels'
 import {
   isVideoFlagValid,
@@ -12,6 +12,7 @@ import {
   isVideoTorrentUpdateActivityValid
 } from './videos'
 import { isViewActivityValid } from './view'
+import { isDislikeActivityValid, isLikeActivityValid } from './rate'
 
 function isRootActivityValid (activity: any) {
   return Array.isArray(activity['@context']) &&
@@ -34,7 +35,8 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean
   Follow: checkFollowActivity,
   Accept: checkAcceptActivity,
   Announce: checkAnnounceActivity,
-  Undo: checkUndoActivity
+  Undo: checkUndoActivity,
+  Like: checkLikeActivity
 }
 
 function isActivityValid (activity: any) {
@@ -55,9 +57,10 @@ export {
 // ---------------------------------------------------------------------------
 
 function checkCreateActivity (activity: any) {
-  return isVideoChannelCreateActivityValid(activity) ||
-    isVideoFlagValid(activity) ||
-    isViewActivityValid(activity)
+  return isViewActivityValid(activity) ||
+    isDislikeActivityValid(activity) ||
+    isVideoChannelCreateActivityValid(activity) ||
+    isVideoFlagValid(activity)
 }
 
 function checkAddActivity (activity: any) {
@@ -84,9 +87,13 @@ function checkAcceptActivity (activity: any) {
 }
 
 function checkAnnounceActivity (activity: any) {
-  return isAnnounceValid(activity)
+  return isAnnounceActivityValid(activity)
 }
 
 function checkUndoActivity (activity: any) {
-  return isUndoValid(activity)
+  return isUndoActivityValid(activity)
+}
+
+function checkLikeActivity (activity: any) {
+  return isLikeActivityValid(activity)
 }
index 4ba99d1ea366e812a084083f7a371d38a2d31abf..45f6b05a08d73c7155a2dfc889ee7ee5c3fc9147 100644 (file)
@@ -2,7 +2,7 @@ import { isBaseActivityValid } from './misc'
 import { isVideoTorrentAddActivityValid } from './videos'
 import { isVideoChannelCreateActivityValid } from './video-channels'
 
-function isAnnounceValid (activity: any) {
+function isAnnounceActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Announce') &&
     (
       isVideoChannelCreateActivityValid(activity.object) ||
@@ -11,5 +11,5 @@ function isAnnounceValid (activity: any) {
 }
 
 export {
-  isAnnounceValid
+  isAnnounceActivityValid
 }
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts
new file mode 100644 (file)
index 0000000..e70bd94
--- /dev/null
@@ -0,0 +1,20 @@
+import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
+
+function isLikeActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Like') &&
+    isActivityPubUrlValid(activity.object)
+}
+
+function isDislikeActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Create') &&
+    activity.object.type === 'Dislike' &&
+    isActivityPubUrlValid(activity.object.actor) &&
+    isActivityPubUrlValid(activity.object.object)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  isLikeActivityValid,
+  isDislikeActivityValid
+}
index a9a2a3a41d38c1ce1607930aaa3c0a446b19aaaf..58043f8a1e65a75b7100caf51fe8a0adb6aed2f0 100644 (file)
@@ -1,13 +1,16 @@
 import { isAccountFollowActivityValid } from './account'
 import { isBaseActivityValid } from './misc'
+import { isDislikeActivityValid, isLikeActivityValid } from './rate'
 
-function isUndoValid (activity: any) {
+function isUndoActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Undo') &&
     (
-      isAccountFollowActivityValid(activity.object)
+      isAccountFollowActivityValid(activity.object) ||
+      isLikeActivityValid(activity.object) ||
+      isDislikeActivityValid(activity.object)
     )
 }
 
 export {
-  isUndoValid
+  isUndoActivityValid
 }
index 9e61f01aa0739adb59886b0de34b78997489e877..e7f668ee47a01aaeeaf918fed64891ecf2fa7b33 100644 (file)
@@ -229,6 +229,7 @@ const ACTIVITY_PUB = {
   PUBLIC: 'https://www.w3.org/ns/activitystreams#Public',
   COLLECTION_ITEMS_PER_PAGE: 10,
   FETCH_PAGE_LIMIT: 100,
+  MAX_HTTP_ATTEMPT: 5,
   URL_MIME_TYPES: {
     VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT
     TORRENT: [ 'application/x-bittorrent' ],
index c68312053a0d4026eb2363b06c786dbbf1f346a0..e25c261cc0373ec912894ca6fed4c3f9245e086d 100644 (file)
@@ -5,5 +5,6 @@ export * from './process-announce'
 export * from './process-create'
 export * from './process-delete'
 export * from './process-follow'
+export * from './process-like'
 export * from './process-undo'
 export * from './process-update'
index 1777733a07dc698e379f9a39e4519a27d315d116..147bbd1328b69b2085036cd6c264b4859ac472d1 100644 (file)
@@ -5,9 +5,10 @@ import { logger, retryTransactionWrapper } from '../../../helpers'
 import { database as db } from '../../../initializers'
 import { AccountInstance } from '../../../models/account/account-interface'
 import { getOrCreateAccountAndServer } from '../account'
-import { sendCreateViewToVideoFollowers } from '../send/send-create'
+import { sendCreateDislikeToVideoFollowers, sendCreateViewToVideoFollowers } from '../send/send-create'
 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,6 +17,8 @@ async function processCreateActivity (activity: ActivityCreate) {
 
   if (activityType === 'View') {
     return processCreateView(activityObject as ViewObject)
+  } else if (activityType === 'Dislike') {
+    return processCreateDislike(account, activityObject as DislikeObject)
   } else if (activityType === 'VideoChannel') {
     return processCreateVideoChannel(account, activityObject as VideoChannelObject)
   } else if (activityType === 'Flag') {
@@ -34,6 +37,36 @@ export {
 
 // ---------------------------------------------------------------------------
 
+async function processCreateDislike (byAccount: AccountInstance, dislike: DislikeObject) {
+  const options = {
+    arguments: [ byAccount, dislike ],
+    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)
+
+    if (!video) throw new Error('Unknown video ' + dislike.object)
+
+    const rate = {
+      type: 'dislike' as 'dislike',
+      videoId: video.id,
+      accountId: byAccount.id
+    }
+    const [ , created ] = await db.AccountVideoRate.findOrCreate({
+      where: rate,
+      defaults: rate
+    })
+    await video.increment('dislikes')
+
+    if (video.isOwned() && created === true) await sendCreateDislikeToVideoFollowers(byAccount, video, undefined)
+  })
+}
+
 async function processCreateView (view: ViewObject) {
   const video = await db.Video.loadByUrlAndPopulateAccount(view.object)
 
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
new file mode 100644 (file)
index 0000000..d77b30f
--- /dev/null
@@ -0,0 +1,50 @@
+import { ActivityLike } from '../../../../shared/models/activitypub/activity'
+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'
+
+async function processLikeActivity (activity: ActivityLike) {
+  const account = await getOrCreateAccountAndServer(activity.actor)
+
+  return processLikeVideo(account, activity.object)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processLikeActivity
+}
+
+// ---------------------------------------------------------------------------
+
+async function processLikeVideo (byAccount: AccountInstance, videoUrl: string) {
+  const options = {
+    arguments: [ byAccount, videoUrl ],
+    errorMessage: 'Cannot like the video with many retries.'
+  }
+
+  return retryTransactionWrapper(createVideoLike, options)
+}
+
+function createVideoLike (byAccount: AccountInstance, videoUrl: string) {
+  return db.sequelize.transaction(async t => {
+    const video = await db.Video.loadByUrlAndPopulateAccount(videoUrl)
+
+    if (!video) throw new Error('Unknown video ' + videoUrl)
+
+    const rate = {
+      type: 'like' as 'like',
+      videoId: video.id,
+      accountId: byAccount.id
+    }
+    const [ , created ] = await db.AccountVideoRate.findOrCreate({
+      where: rate,
+      defaults: rate
+    })
+    await video.increment('likes')
+
+    if (video.isOwned() && created === true) await sendLikeToVideoFollowers(byAccount, video, undefined)
+  })
+}
index 610b800fbe72b4f3a7898819fba51f2dd77f8103..caa835714b1f91cdd6913919915d458e715737b7 100644 (file)
@@ -1,20 +1,20 @@
-import { ActivityUndo } from '../../../../shared/models/activitypub/activity'
+import { ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub/activity'
 import { logger } from '../../../helpers/logger'
 import { database as db } from '../../../initializers'
+import { retryTransactionWrapper } from '../../../helpers/database-utils'
+import { DislikeObject } from '../../../../shared/models/activitypub/objects/dislike-object'
+import { sendUndoLikeToVideoFollowers } from '../send/send-undo'
+import { sendUndoDislikeToVideoFollowers } from '../index'
 
 async function processUndoActivity (activity: ActivityUndo) {
   const activityToUndo = activity.object
 
-  if (activityToUndo.type === 'Follow') {
-    const follower = await db.Account.loadByUrl(activity.actor)
-    const following = await db.Account.loadByUrl(activityToUndo.object)
-    const accountFollow = await db.AccountFollow.loadByAccountAndTarget(follower.id, following.id)
-
-    if (!accountFollow) throw new Error(`'Unknown account follow ${follower.id} -> ${following.id}.`)
-
-    await accountFollow.destroy()
-
-    return undefined
+  if (activityToUndo.type === 'Like') {
+    return processUndoLike(activity.actor, activityToUndo)
+  } else if (activityToUndo.type === 'Create' && activityToUndo.object.type === 'Dislike') {
+    return processUndoDislike(activity.actor, activityToUndo.object)
+  } else if (activityToUndo.type === 'Follow') {
+    return processUndoFollow(activity.actor, activityToUndo)
   }
 
   logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id })
@@ -29,3 +29,80 @@ export {
 }
 
 // ---------------------------------------------------------------------------
+
+function processUndoLike (actor: string, likeActivity: ActivityLike) {
+  const options = {
+    arguments: [ actor, likeActivity ],
+    errorMessage: 'Cannot undo like with many retries.'
+  }
+
+  return retryTransactionWrapper(undoLike, options)
+}
+
+function undoLike (actor: string, likeActivity: 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)
+    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')
+
+    if (video.isOwned()) await sendUndoLikeToVideoFollowers(byAccount, video, t)
+  })
+}
+
+function processUndoDislike (actor: string, dislikeCreateActivity: DislikeObject) {
+  const options = {
+    arguments: [ actor, dislikeCreateActivity ],
+    errorMessage: 'Cannot undo dislike with many retries.'
+  }
+
+  return retryTransactionWrapper(undoDislike, options)
+}
+
+function undoDislike (actor: string, dislike: 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)
+    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')
+
+    if (video.isOwned()) await sendUndoDislikeToVideoFollowers(byAccount, video, t)
+  })
+}
+
+function processUndoFollow (actor: string, followActivity: ActivityFollow) {
+  const options = {
+    arguments: [ actor, followActivity ],
+    errorMessage: 'Cannot undo follow with many retries.'
+  }
+
+  return retryTransactionWrapper(undoFollow, options)
+}
+
+function undoFollow (actor: string, followActivity: ActivityFollow) {
+  return db.sequelize.transaction(async t => {
+    const follower = await db.Account.loadByUrl(actor, t)
+    const following = await db.Account.loadByUrl(followActivity.object, t)
+    const accountFollow = await db.AccountFollow.loadByAccountAndTarget(follower.id, following.id, t)
+
+    if (!accountFollow) throw new Error(`'Unknown account follow ${follower.id} -> ${following.id}.`)
+
+    await accountFollow.destroy({ transaction: t })
+
+    return undefined
+  })
+}
index 6135973411c7f373700f9b0d046fdea7805ba0b4..942bce0e6245e9c41e8d94e5e465ca390e45c999 100644 (file)
@@ -1,4 +1,5 @@
 import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity'
+import { logger } from '../../../helpers/logger'
 import { AccountInstance } from '../../../models/account/account-interface'
 import { processAcceptActivity } from './process-accept'
 import { processAddActivity } from './process-add'
@@ -6,9 +7,9 @@ import { processAnnounceActivity } from './process-announce'
 import { processCreateActivity } from './process-create'
 import { processDeleteActivity } from './process-delete'
 import { processFollowActivity } from './process-follow'
+import { processLikeActivity } from './process-like'
 import { processUndoActivity } from './process-undo'
 import { processUpdateActivity } from './process-update'
-import { logger } from '../../../helpers/logger'
 
 const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccount?: AccountInstance) => Promise<any> } = {
   Create: processCreateActivity,
@@ -18,7 +19,8 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccoun
   Follow: processFollowActivity,
   Accept: processAcceptActivity,
   Announce: processAnnounceActivity,
-  Undo: processUndoActivity
+  Undo: processUndoActivity,
+  Like: processLikeActivity
 }
 
 async function processActivities (activities: Activity[], inboxAccount?: AccountInstance) {
index 5f15dd4b58b59a1abf12dc2fd058529ca7a19b96..ee8f3ad7e069fb5e3f208b2415d562c2dd36094a 100644 (file)
@@ -4,4 +4,6 @@ export * from './send-announce'
 export * from './send-create'
 export * from './send-delete'
 export * from './send-follow'
+export * from './send-like'
+export * from './send-undo'
 export * from './send-update'
index f3dc5c148e59dc94fffe76f554b006d0ed215711..41a039b1924e4540d52bbcc03eafb88b930999f6 100644 (file)
@@ -3,6 +3,7 @@ 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 { VideoInstance } from '../../../models/video/video-interface'
 
 async function broadcastToFollowers (
   data: any,
@@ -41,6 +42,27 @@ async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: s
   return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload)
 }
 
+function getOriginVideoAudience (video: VideoInstance) {
+  return {
+    to: [ video.VideoChannel.Account.url ],
+    cc: [ video.VideoChannel.Account.url + '/followers' ]
+  }
+}
+
+function getVideoFollowersAudience (video: VideoInstance) {
+  return {
+    to: [ video.VideoChannel.Account.url + '/followers' ],
+    cc: []
+  }
+}
+
+async function getAccountsToForwardVideoAction (byAccount: AccountInstance, video: VideoInstance) {
+  const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id)
+  accountsToForwardView.push(video.VideoChannel.Account)
+
+  return accountsToForwardView
+}
+
 async function getAudience (accountSender: AccountInstance, isPublic = true) {
   const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls()
 
@@ -64,5 +86,8 @@ async function getAudience (accountSender: AccountInstance, isPublic = true) {
 export {
   broadcastToFollowers,
   unicastTo,
-  getAudience
+  getAudience,
+  getOriginVideoAudience,
+  getAccountsToForwardVideoAction,
+  getVideoFollowersAudience
 }
index e5fb212b741f0e4bba0a1b815186b02d18f274d0..6afe67ee6737e7eb027093094ae6d6dce7503cac 100644 (file)
@@ -1,11 +1,17 @@
 import { Transaction } from 'sequelize'
 import { 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 { broadcastToFollowers, getAudience, unicastTo } from './misc'
-import { getVideoAbuseActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
-import { getServerAccount } from '../../../helpers/utils'
-import { database as db } from '../../../initializers'
+import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
+import {
+  broadcastToFollowers,
+  getAccountsToForwardVideoAction,
+  getAudience,
+  getOriginVideoAudience,
+  getVideoFollowersAudience,
+  unicastTo
+} from './misc'
 
 async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) {
   const byAccount = videoChannel.Account
@@ -29,7 +35,7 @@ async function sendCreateViewToOrigin (byAccount: AccountInstance, video: VideoI
   const url = getVideoViewActivityPubUrl(byAccount, video)
   const viewActivity = createViewActivityData(byAccount, video)
 
-  const audience = { to: [ video.VideoChannel.Account.url ], cc: [ video.VideoChannel.Account.url + '/followers' ] }
+  const audience = getOriginVideoAudience(video)
   const data = await createActivityData(url, byAccount, viewActivity, audience)
 
   return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
@@ -39,16 +45,35 @@ async function sendCreateViewToVideoFollowers (byAccount: AccountInstance, video
   const url = getVideoViewActivityPubUrl(byAccount, video)
   const viewActivity = createViewActivityData(byAccount, video)
 
-  const audience = { to: [ video.VideoChannel.Account.url + '/followers' ], cc: [] }
+  const audience = getVideoFollowersAudience(video)
   const data = await createActivityData(url, byAccount, viewActivity, audience)
 
   const serverAccount = await getServerAccount()
-  const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id)
-  accountsToForwardView.push(video.VideoChannel.Account)
+  const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
+
+  const followersException = [ byAccount ]
+  return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
+}
+
+async function sendCreateDislikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const url = getVideoDislikeActivityPubUrl(byAccount, video)
+  const dislikeActivity = createDislikeActivityData(byAccount, video)
+
+  const audience = getOriginVideoAudience(video)
+  const data = await createActivityData(url, byAccount, dislikeActivity, audience)
+
+  return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
+}
 
-  // Don't forward view to server that sent it to us
-  const index = accountsToForwardView.findIndex(a => a.id === byAccount.id)
-  if (index) accountsToForwardView.splice(index, 1)
+async function sendCreateDislikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const url = getVideoDislikeActivityPubUrl(byAccount, video)
+  const dislikeActivity = createDislikeActivityData(byAccount, video)
+
+  const audience = getVideoFollowersAudience(video)
+  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)
@@ -71,6 +96,16 @@ async function createActivityData (url: string, byAccount: AccountInstance, obje
   return activity
 }
 
+function createDislikeActivityData (byAccount: AccountInstance, video: VideoInstance) {
+  const obj = {
+    type: 'Dislike',
+    actor: byAccount.url,
+    object: video.url
+  }
+
+  return obj
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -78,7 +113,10 @@ export {
   sendVideoAbuse,
   createActivityData,
   sendCreateViewToOrigin,
-  sendCreateViewToVideoFollowers
+  sendCreateViewToVideoFollowers,
+  sendCreateDislikeToOrigin,
+  sendCreateDislikeToVideoFollowers,
+  createDislikeActivityData
 }
 
 // ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
new file mode 100644 (file)
index 0000000..70a7d88
--- /dev/null
@@ -0,0 +1,60 @@
+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,
+  getAudience,
+  getOriginVideoAudience,
+  getVideoFollowersAudience,
+  unicastTo
+} from './misc'
+
+async function sendLikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const url = getVideoLikeActivityPubUrl(byAccount, video)
+
+  const audience = getOriginVideoAudience(video)
+  const data = await likeActivityData(url, byAccount, video, audience)
+
+  return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
+}
+
+async function sendLikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const url = getVideoLikeActivityPubUrl(byAccount, video)
+
+  const audience = getVideoFollowersAudience(video)
+  const data = await likeActivityData(url, byAccount, video, audience)
+
+  const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
+  const serverAccount = await getServerAccount()
+
+  const followersException = [ byAccount ]
+  return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
+}
+
+async function likeActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, audience?: { to: string[], cc: string[] }) {
+  if (!audience) {
+    audience = await getAudience(byAccount)
+  }
+
+  const activity: ActivityLike = {
+    type: 'Like',
+    id: url,
+    actor: byAccount.url,
+    to: audience.to,
+    cc: audience.cc,
+    object: video.url
+  }
+
+  return activity
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendLikeToOrigin,
+  sendLikeToVideoFollowers,
+  likeActivityData
+}
index 77bee663957d034680cb6119bf46ff5fd184adb2..53fddd0cbfb4b8be0d2f161455b2cc6c363e04f7 100644 (file)
@@ -1,10 +1,14 @@
 import { Transaction } from 'sequelize'
-import { ActivityFollow, ActivityUndo } from '../../../../shared/models/activitypub/activity'
+import { ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub/activity'
 import { AccountInstance } from '../../../models'
 import { AccountFollowInstance } from '../../../models/account/account-follow-interface'
-import { unicastTo } from './misc'
+import { broadcastToFollowers, getAccountsToForwardVideoAction, unicastTo } from './misc'
 import { followActivityData } from './send-follow'
-import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl } from '../url'
+import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
+import { VideoInstance } from '../../../models/video/video-interface'
+import { likeActivityData } from './send-like'
+import { createActivityData, createDislikeActivityData } from './send-create'
+import { getServerAccount } from '../../../helpers/utils'
 
 async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transaction) {
   const me = accountFollow.AccountFollower
@@ -19,15 +23,72 @@ async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transact
   return unicastTo(data, me, following.inboxUrl, t)
 }
 
+async function sendUndoLikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const likeUrl = getVideoLikeActivityPubUrl(byAccount, video)
+  const undoUrl = getUndoActivityPubUrl(likeUrl)
+
+  const object = await likeActivityData(likeUrl, byAccount, video)
+  const data = await undoActivityData(undoUrl, byAccount, object)
+
+  return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
+}
+
+async function sendUndoLikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const likeUrl = getVideoLikeActivityPubUrl(byAccount, video)
+  const undoUrl = getUndoActivityPubUrl(likeUrl)
+
+  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 followersException = [ byAccount ]
+  return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
+}
+
+async function sendUndoDislikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const dislikeUrl = getVideoDislikeActivityPubUrl(byAccount, video)
+  const undoUrl = getUndoActivityPubUrl(dislikeUrl)
+
+  const dislikeActivity = createDislikeActivityData(byAccount, video)
+  const object = await createActivityData(undoUrl, byAccount, dislikeActivity)
+
+  const data = await undoActivityData(undoUrl, byAccount, object)
+
+  return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
+}
+
+async function sendUndoDislikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const dislikeUrl = getVideoDislikeActivityPubUrl(byAccount, video)
+  const undoUrl = getUndoActivityPubUrl(dislikeUrl)
+
+  const dislikeActivity = createDislikeActivityData(byAccount, video)
+  const object = await createActivityData(undoUrl, byAccount, dislikeActivity)
+
+  const data = await undoActivityData(undoUrl, byAccount, object)
+
+  const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
+  const serverAccount = await getServerAccount()
+
+  const followersException = [ byAccount ]
+  return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
+}
+
+
 // ---------------------------------------------------------------------------
 
 export {
-  sendUndoFollow
+  sendUndoFollow,
+  sendUndoLikeToOrigin,
+  sendUndoLikeToVideoFollowers,
+  sendUndoDislikeToOrigin,
+  sendUndoDislikeToVideoFollowers
 }
 
 // ---------------------------------------------------------------------------
 
-async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow) {
+async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow | ActivityLike | ActivityCreate) {
   const activity: ActivityUndo = {
     type: 'Undo',
     id: url,
index d98561e3328874119c325a5ccb26bcc04885a624..17395a99b81291d091877fbc2ad48741a9bd5e57 100644 (file)
@@ -25,6 +25,14 @@ function getVideoViewActivityPubUrl (byAccount: AccountInstance, video: VideoIns
   return video.url + '#views/' + byAccount.uuid + '/' + new Date().toISOString()
 }
 
+function getVideoLikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) {
+  return byAccount.url + '#likes/' + video.id
+}
+
+function getVideoDislikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) {
+  return byAccount.url + '#dislikes/' + video.id
+}
+
 function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) {
   const me = accountFollow.AccountFollower
   const following = accountFollow.AccountFollowing
@@ -61,5 +69,7 @@ export {
   getAnnounceActivityPubUrl,
   getUpdateActivityPubUrl,
   getUndoActivityPubUrl,
-  getVideoViewActivityPubUrl
+  getVideoViewActivityPubUrl,
+  getVideoLikeActivityPubUrl,
+  getVideoDislikeActivityPubUrl
 }
index 9442448932c27e3071de9edf7db2d79a4dcb5889..acee4fe16e38fd1c0f236154b5529a47f42ca948 100644 (file)
@@ -1,9 +1,20 @@
 import { join } from 'path'
 import * as request from 'request'
+import { Transaction } from 'sequelize'
 import { ActivityIconObject } from '../../../shared/index'
 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
 import { CONFIG, REMOTE_SCHEME, STATIC_PATHS } from '../../initializers/constants'
+import { AccountInstance } from '../../models/account/account-interface'
 import { VideoInstance } from '../../models/video/video-interface'
+import { sendLikeToOrigin } from './index'
+import { sendCreateDislikeToOrigin, sendCreateDislikeToVideoFollowers } from './send/send-create'
+import { sendLikeToVideoFollowers } from './send/send-like'
+import {
+  sendUndoDislikeToOrigin,
+  sendUndoDislikeToVideoFollowers,
+  sendUndoLikeToOrigin,
+  sendUndoLikeToVideoFollowers
+} from './send/send-undo'
 
 function fetchRemoteVideoPreview (video: VideoInstance) {
   // FIXME: use url
@@ -37,8 +48,42 @@ function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObjec
   return doRequestAndSaveToFile(options, thumbnailPath)
 }
 
+function sendVideoRateChangeToFollowers (account: AccountInstance, video: VideoInstance, likes: number, dislikes: number, t: Transaction) {
+  const tasks: Promise<any>[] = []
+
+  // Undo Like
+  if (likes < 0) tasks.push(sendUndoLikeToVideoFollowers(account, video, t))
+  // Like
+  if (likes > 0) tasks.push(sendLikeToVideoFollowers(account, video, t))
+
+  // Undo Dislike
+  if (dislikes < 0) tasks.push(sendUndoDislikeToVideoFollowers(account, video, t))
+  // Dislike
+  if (dislikes > 0) tasks.push(sendCreateDislikeToVideoFollowers(account, video, t))
+
+  return Promise.all(tasks)
+}
+
+function sendVideoRateChangeToOrigin (account: AccountInstance, video: VideoInstance, likes: number, dislikes: number, t: Transaction) {
+  const tasks: Promise<any>[] = []
+
+  // Undo Like
+  if (likes < 0) tasks.push(sendUndoLikeToOrigin(account, video, t))
+  // Like
+  if (likes > 0) tasks.push(sendLikeToOrigin(account, video, t))
+
+  // Undo Dislike
+  if (dislikes < 0) tasks.push(sendUndoDislikeToOrigin(account, video, t))
+  // Dislike
+  if (dislikes > 0) tasks.push(sendCreateDislikeToOrigin(account, video, t))
+
+  return Promise.all(tasks)
+}
+
 export {
   fetchRemoteVideoPreview,
   fetchRemoteVideoDescription,
-  generateThumbnailFromUrl
+  generateThumbnailFromUrl,
+  sendVideoRateChangeToFollowers,
+  sendVideoRateChangeToOrigin
 }
index 111fc88a4fa16e24e020e8625d7ed37ca4e06802..5b4c65b81480e90cd3704c80c8fd187a56669b33 100644 (file)
@@ -2,7 +2,7 @@ import { logger } from '../../../helpers'
 import { buildSignedActivity } from '../../../helpers/activitypub'
 import { doRequest } from '../../../helpers/requests'
 import { database as db } from '../../../initializers'
-import { ActivityPubHttpPayload } from './activitypub-http-job-scheduler'
+import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
 
 async function process (payload: ActivityPubHttpPayload, jobId: number) {
   logger.info('Processing ActivityPub broadcast in job %d.', jobId)
@@ -20,7 +20,12 @@ async function process (payload: ActivityPubHttpPayload, jobId: number) {
 
   for (const uri of payload.uris) {
     options.uri = uri
-    await doRequest(options)
+
+    try {
+      await doRequest(options)
+    } catch (err) {
+      await maybeRetryRequestLater(err, payload, uri)
+    }
   }
 }
 
index aef217ce71c0df6766164142ebe69a4b5c89784d..ccf1099350e13505f23526a3bf956d1d139bec5e 100644 (file)
@@ -4,12 +4,16 @@ import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-h
 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'
 
 type ActivityPubHttpPayload = {
   uris: string[]
   signatureAccountId?: number
   body?: any
+  attemptNumber?: number
 }
+
 const jobHandlers: { [ handlerName: string ]: JobHandler<ActivityPubHttpPayload, void> } = {
   activitypubHttpBroadcastHandler,
   activitypubHttpUnicastHandler,
@@ -19,7 +23,25 @@ const jobCategory: JobCategory = 'activitypub-http'
 
 const activitypubHttpJobScheduler = new JobScheduler(jobCategory, jobHandlers)
 
+function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, uri: string) {
+  logger.warn('Cannot make request to %s.', uri, err)
+
+  let attemptNumber = payload.attemptNumber || 1
+  attemptNumber += 1
+
+  if (attemptNumber < ACTIVITY_PUB.MAX_HTTP_ATTEMPT) {
+    logger.debug('Retrying request to %s (attempt %d/%d).', uri, attemptNumber, ACTIVITY_PUB.MAX_HTTP_ATTEMPT, err)
+
+    const newPayload = Object.assign(payload, {
+      uris: [ uri ],
+      attemptNumber
+    })
+    return activitypubHttpJobScheduler.createJob(undefined, 'activitypubHttpUnicastHandler', newPayload)
+  }
+}
+
 export {
   ActivityPubHttpPayload,
-  activitypubHttpJobScheduler
+  activitypubHttpJobScheduler,
+  maybeRetryRequestLater
 }
index 8d3b755ad2d6accc2417d7325e4eb734bc2bbec8..f7f3dabbd383be0e852f8483c3672d8885f7aeb8 100644 (file)
@@ -1,6 +1,6 @@
 import { logger } from '../../../helpers'
 import { doRequest } from '../../../helpers/requests'
-import { ActivityPubHttpPayload } from './activitypub-http-job-scheduler'
+import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
 import { database as db } from '../../../initializers/database'
 import { buildSignedActivity } from '../../../helpers/activitypub'
 
@@ -18,7 +18,12 @@ async function process (payload: ActivityPubHttpPayload, jobId: number) {
     json: signedBody
   }
 
-  await doRequest(options)
+  try {
+    await doRequest(options)
+  } catch (err) {
+    await maybeRetryRequestLater(err, payload, uri)
+    throw err
+  }
 }
 
 function onError (err: Error, jobId: number) {
index 6f228c790a7d1f1e00ee735894e0d9e2a3f1434e..a0d620dd0b20cd504453828aa62d22cf33c06282 100644 (file)
@@ -5,7 +5,11 @@ import { ResultList } from '../../../shared/models/result-list.model'
 import { AccountInstance } from './account-interface'
 
 export namespace AccountFollowMethods {
-  export type LoadByAccountAndTarget = (accountId: number, targetAccountId: number) => Bluebird<AccountFollowInstance>
+  export type LoadByAccountAndTarget = (
+    accountId: number,
+    targetAccountId: number,
+    t?: Sequelize.Transaction
+  ) => Bluebird<AccountFollowInstance>
 
   export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>
   export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>
index 578bcda39b22b3d990b5b17872303296cd323842..8e35c7d207fde9e800a1c0838016cde5bfa02ccf 100644 (file)
@@ -93,7 +93,7 @@ toFormattedJSON = function (this: AccountFollowInstance) {
   return json
 }
 
-loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
+loadByAccountAndTarget = function (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
   const query = {
     where: {
       accountId,
@@ -110,7 +110,8 @@ loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
         required: true,
         as: 'AccountFollowing'
       }
-    ]
+    ],
+    transaction: t
   }
 
   return AccountFollow.findOne(query)
index da56398b17c171e2533da93a85d5592fee0e2faa..2448147d84733ae46d51fac980f26146f0273403 100644 (file)
@@ -1,4 +1,5 @@
 // Order of the tests we want to execute
 // import './multiple-servers'
 import './video-transcoder'
+import './multiple-servers'
 import './follows'
index ce150bc12dab2556bc62c3a57b00f08eba71c5a3..cbfd6157a3723befa97b79d70dbff6828953bb42 100644 (file)
@@ -1,13 +1,14 @@
 import { ActivityPubSignature } from './activitypub-signature'
 import { VideoChannelObject, VideoTorrentObject } from './objects'
+import { DislikeObject } from './objects/dislike-object'
 import { VideoAbuseObject } from './objects/video-abuse-object'
 import { ViewObject } from './objects/view-object'
 
 export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate |
   ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce |
-  ActivityUndo
+  ActivityUndo | ActivityLike
 
-export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo'
+export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like'
 
 export interface BaseActivity {
   '@context'?: any[]
@@ -21,7 +22,7 @@ export interface BaseActivity {
 
 export interface ActivityCreate extends BaseActivity {
   type: 'Create'
-  object: VideoChannelObject | VideoAbuseObject | ViewObject
+  object: VideoChannelObject | VideoAbuseObject | ViewObject | DislikeObject
 }
 
 export interface ActivityAdd extends BaseActivity {
@@ -55,5 +56,10 @@ export interface ActivityAnnounce extends BaseActivity {
 
 export interface ActivityUndo extends BaseActivity {
   type: 'Undo',
-  object: ActivityFollow
+  object: ActivityFollow | ActivityLike | ActivityCreate
+}
+
+export interface ActivityLike extends BaseActivity {
+  type: 'Like',
+  object: string
 }
diff --git a/shared/models/activitypub/objects/dislike-object.ts b/shared/models/activitypub/objects/dislike-object.ts
new file mode 100644 (file)
index 0000000..2951757
--- /dev/null
@@ -0,0 +1,5 @@
+export interface DislikeObject {
+  type: 'Dislike',
+  actor: string
+  object: string
+}
index d92f772e29a3e3d8c0effce97eeaa9a3a7dca42b..f1f761e446932e593d1e1185ffd33047ff8ec84f 100644 (file)
@@ -3,3 +3,4 @@ export * from './video-abuse-object'
 export * from './video-channel-object'
 export * from './video-torrent-object'
 export * from './view-object'
+export * from './dislike-object'