Refractor activity pub lib/helpers
authorChocobozzz <florian.bigard@gmail.com>
Mon, 20 Nov 2017 08:43:39 +0000 (09:43 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 18:40:52 +0000 (19:40 +0100)
55 files changed:
server/controllers/activitypub/inbox.ts
server/controllers/api/server/follows.ts
server/controllers/api/videos/channel.ts
server/controllers/api/videos/index.ts
server/helpers/activitypub.ts
server/helpers/custom-validators/activitypub/activity.ts
server/helpers/custom-validators/activitypub/announce.ts [new file with mode: 0644]
server/helpers/custom-validators/activitypub/index.ts
server/helpers/custom-validators/activitypub/undo.ts [new file with mode: 0644]
server/helpers/custom-validators/activitypub/video-channels.ts [new file with mode: 0644]
server/helpers/custom-validators/activitypub/videos.ts
server/lib/activitypub/index.ts
server/lib/activitypub/misc.ts [deleted file]
server/lib/activitypub/process-accept.ts [deleted file]
server/lib/activitypub/process-add.ts [deleted file]
server/lib/activitypub/process-announce.ts [deleted file]
server/lib/activitypub/process-create.ts [deleted file]
server/lib/activitypub/process-delete.ts [deleted file]
server/lib/activitypub/process-follow.ts [deleted file]
server/lib/activitypub/process-update.ts [deleted file]
server/lib/activitypub/process/index.ts [new file with mode: 0644]
server/lib/activitypub/process/misc.ts [new file with mode: 0644]
server/lib/activitypub/process/process-accept.ts [new file with mode: 0644]
server/lib/activitypub/process/process-add.ts [new file with mode: 0644]
server/lib/activitypub/process/process-announce.ts [new file with mode: 0644]
server/lib/activitypub/process/process-create.ts [new file with mode: 0644]
server/lib/activitypub/process/process-delete.ts [new file with mode: 0644]
server/lib/activitypub/process/process-follow.ts [new file with mode: 0644]
server/lib/activitypub/process/process-undo.ts [new file with mode: 0644]
server/lib/activitypub/process/process-update.ts [new file with mode: 0644]
server/lib/activitypub/send-request.ts [deleted file]
server/lib/activitypub/send/index.ts [new file with mode: 0644]
server/lib/activitypub/send/misc.ts [new file with mode: 0644]
server/lib/activitypub/send/send-accept.ts [new file with mode: 0644]
server/lib/activitypub/send/send-add.ts [new file with mode: 0644]
server/lib/activitypub/send/send-announce.ts [new file with mode: 0644]
server/lib/activitypub/send/send-create.ts [new file with mode: 0644]
server/lib/activitypub/send/send-delete.ts [new file with mode: 0644]
server/lib/activitypub/send/send-follow.ts [new file with mode: 0644]
server/lib/activitypub/send/send-undo.ts [new file with mode: 0644]
server/lib/activitypub/send/send-update.ts [new file with mode: 0644]
server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts
server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts
server/lib/user.ts
server/lib/video-channel.ts
server/middlewares/validators/follows.ts [new file with mode: 0644]
server/middlewares/validators/index.ts
server/middlewares/validators/servers.ts [deleted file]
server/models/account/account-follow.ts
server/models/account/account-interface.ts
server/models/account/account.ts
server/models/video/video-channel.ts
server/models/video/video.ts
server/tests/api/multiple-servers.ts
shared/models/activitypub/activity.ts

index fd3695886e3f67de0fcd267b0151600af43d4fdb..807d0bdf4e53b378ed932f7c371f75adbef01aa1 100644 (file)
@@ -2,12 +2,12 @@ import * as express from 'express'
 import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, ActivityType, RootActivity } from '../../../shared'
 import { logger } from '../../helpers'
 import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
-import { processCreateActivity, processUpdateActivity } from '../../lib'
-import { processAcceptActivity } from '../../lib/activitypub/process-accept'
-import { processAddActivity } from '../../lib/activitypub/process-add'
-import { processAnnounceActivity } from '../../lib/activitypub/process-announce'
-import { processDeleteActivity } from '../../lib/activitypub/process-delete'
-import { processFollowActivity } from '../../lib/activitypub/process-follow'
+import { processCreateActivity, processUpdateActivity, processUndoActivity } from '../../lib'
+import { processAcceptActivity } from '../../lib/activitypub/process/process-accept'
+import { processAddActivity } from '../../lib/activitypub/process/process-add'
+import { processAnnounceActivity } from '../../lib/activitypub/process/process-announce'
+import { processDeleteActivity } from '../../lib/activitypub/process/process-delete'
+import { processFollowActivity } from '../../lib/activitypub/process/process-follow'
 import { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares'
 import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
 import { AccountInstance } from '../../models/account/account-interface'
@@ -19,7 +19,8 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccoun
   Delete: processDeleteActivity,
   Follow: processFollowActivity,
   Accept: processAcceptActivity,
-  Announce: processAnnounceActivity
+  Announce: processAnnounceActivity,
+  Undo: processUndoActivity
 }
 
 const inboxRouter = express.Router()
index 3d184ec1f8dce4d3f28e3ef80a17ff71b97075f0..8fc70f34f793de64eee0266f8c2a0ccd5ee32f7f 100644 (file)
@@ -6,14 +6,16 @@ import { getServerAccount } from '../../../helpers/utils'
 import { getAccountFromWebfinger } from '../../../helpers/webfinger'
 import { SERVER_ACCOUNT_NAME } from '../../../initializers/constants'
 import { database as db } from '../../../initializers/database'
-import { sendFollow } from '../../../lib/activitypub/send-request'
-import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../../middlewares'
+import { asyncMiddleware, paginationValidator, removeFollowingValidator, setFollowersSort, setPagination } from '../../../middlewares'
 import { authenticate } from '../../../middlewares/oauth'
 import { setBodyHostsPort } from '../../../middlewares/servers'
 import { setFollowingSort } from '../../../middlewares/sort'
 import { ensureUserHasRight } from '../../../middlewares/user-right'
-import { followValidator } from '../../../middlewares/validators/servers'
+import { followValidator } from '../../../middlewares/validators/follows'
 import { followersSortValidator, followingSortValidator } from '../../../middlewares/validators/sort'
+import { AccountFollowInstance } from '../../../models/index'
+import { sendFollow } from '../../../lib/index'
+import { sendUndoFollow } from '../../../lib/activitypub/send/send-undo'
 
 const serverFollowsRouter = express.Router()
 
@@ -33,6 +35,13 @@ serverFollowsRouter.post('/following',
   asyncMiddleware(follow)
 )
 
+serverFollowsRouter.delete('/following/:accountId',
+  authenticate,
+  ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
+  removeFollowingValidator,
+  asyncMiddleware(removeFollow)
+)
+
 serverFollowsRouter.get('/followers',
   paginationValidator,
   followersSortValidator,
@@ -96,10 +105,12 @@ async function follow (req: express.Request, res: express.Response, next: expres
             },
             transaction: t
           })
+          accountFollow.AccountFollowing = targetAccount
+          accountFollow.AccountFollower = fromAccount
 
           // Send a notification to remote server
           if (accountFollow.state === 'pending') {
-            await sendFollow(fromAccount, targetAccount, t)
+            await sendFollow(accountFollow, t)
           }
         })
       })
@@ -117,6 +128,17 @@ async function follow (req: express.Request, res: express.Response, next: expres
   return res.status(204).end()
 }
 
+async function removeFollow (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const following: AccountFollowInstance = res.locals.following
+
+  await db.sequelize.transaction(async t => {
+    await sendUndoFollow(following, t)
+    await following.destroy({ transaction: t })
+  })
+
+  return res.status(204).end()
+}
+
 async function loadLocalOrGetAccountFromWebfinger (name: string, host: string) {
   let loadedFromDB = true
   let account = await db.Account.loadByNameAndHost(name, host)
index 8f3df2550341bfdeb85fce870440cfa55e49af1c..ce2656e71b2d20677b7a2f8f2990c821f92c8369 100644 (file)
@@ -17,7 +17,7 @@ import {
   videoChannelsUpdateValidator
 } from '../../../middlewares'
 import { AccountInstance, VideoChannelInstance } from '../../../models'
-import { sendUpdateVideoChannel } from '../../../lib/activitypub/send-request'
+import { sendUpdateVideoChannel } from '../../../lib/activitypub/send/send-update'
 
 const videoChannelRouter = express.Router()
 
@@ -128,9 +128,9 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
       if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name)
       if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description)
 
-      await videoChannelInstance.save(sequelizeOptions)
+      const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions)
 
-      await sendUpdateVideoChannel(videoChannelInstance, t)
+      await sendUpdateVideoChannel(videoChannelInstanceUpdated, t)
     })
 
     logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.uuid)
index 22a88620a8ac12fa75cb8d14ac8fefa07c8b7306..8c9b0aa5032499734d6f0b3a6a466698cbc55393 100644 (file)
@@ -12,10 +12,11 @@ import {
   resetSequelizeInstance,
   retryTransactionWrapper
 } from '../../../helpers'
-import { getActivityPubUrl, shareVideoByServer } from '../../../helpers/activitypub'
+import { getVideoActivityPubUrl, shareVideoByServer } from '../../../helpers/activitypub'
 import { CONFIG, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers'
 import { database as db } from '../../../initializers/database'
-import { sendAddVideo, sendUpdateVideo } from '../../../lib/activitypub/send-request'
+import { sendAddVideo } from '../../../lib/activitypub/send/send-add'
+import { sendUpdateVideo } from '../../../lib/activitypub/send/send-update'
 import { transcodingJobScheduler } from '../../../lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler'
 import {
   asyncMiddleware,
@@ -175,7 +176,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
       channelId: res.locals.videoChannel.id
     }
     const video = db.Video.build(videoData)
-    video.url = getActivityPubUrl('video', video.uuid)
+    video.url = getVideoActivityPubUrl(video)
 
     const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename)
     const videoFileHeight = await getVideoFileHeight(videoFilePath)
@@ -274,7 +275,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
       if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', videoInfoToUpdate.privacy)
       if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
 
-      await videoInstance.save(sequelizeOptions)
+      const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
 
       if (videoInfoToUpdate.tags) {
         const tagInstances = await db.Tag.findOrCreateTags(videoInfoToUpdate.tags, t)
@@ -285,7 +286,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
 
       // Now we'll update the video's meta data to our friends
       if (wasPrivateVideo === false) {
-        await sendUpdateVideo(videoInstance, t)
+        await sendUpdateVideo(videoInstanceUpdated, t)
       }
 
       // Video is not private anymore, send a create action to remote servers
index aff58515a0e42b79f7b8c051753bdb6394cecf41..9622a18012537552121c82d3c61e6bd405d18776 100644 (file)
@@ -9,18 +9,20 @@ import { VideoChannelObject } from '../../shared/models/activitypub/objects/vide
 import { ResultList } from '../../shared/models/result-list.model'
 import { database as db, REMOTE_SCHEME } from '../initializers'
 import { ACTIVITY_PUB, CONFIG, STATIC_PATHS } from '../initializers/constants'
-import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/misc'
-import { sendVideoAnnounce } from '../lib/activitypub/send-request'
+import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/process/misc'
+import { sendVideoAnnounce } from '../lib/activitypub/send/send-announce'
 import { sendVideoChannelAnnounce } from '../lib/index'
+import { AccountFollowInstance } from '../models/account/account-follow-interface'
 import { AccountInstance } from '../models/account/account-interface'
+import { VideoAbuseInstance } from '../models/video/video-abuse-interface'
 import { VideoChannelInstance } from '../models/video/video-channel-interface'
 import { VideoInstance } from '../models/video/video-interface'
 import { isRemoteAccountValid } from './custom-validators'
-import { isVideoChannelObjectValid } from './custom-validators/activitypub/videos'
 import { logger } from './logger'
 import { signObject } from './peertube-crypto'
 import { doRequest, doRequestAndSaveToFile } from './requests'
 import { getServerAccount } from './utils'
+import { isVideoChannelObjectValid } from './custom-validators/activitypub/video-channels'
 
 function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) {
   const thumbnailName = video.getThumbnailName()
@@ -55,13 +57,46 @@ async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transactio
   return sendVideoAnnounce(serverAccount, video, t)
 }
 
-function getActivityPubUrl (type: 'video' | 'videoChannel' | 'account' | 'videoAbuse', id: string) {
-  if (type === 'video') return CONFIG.WEBSERVER.URL + '/videos/watch/' + id
-  else if (type === 'videoChannel') return CONFIG.WEBSERVER.URL + '/video-channels/' + id
-  else if (type === 'account') return CONFIG.WEBSERVER.URL + '/account/' + id
-  else if (type === 'videoAbuse') return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + id
+function getVideoActivityPubUrl (video: VideoInstance) {
+  return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
+}
+
+function getVideoChannelActivityPubUrl (videoChannel: VideoChannelInstance) {
+  return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannel.uuid
+}
+
+function getAccountActivityPubUrl (accountName: string) {
+  return CONFIG.WEBSERVER.URL + '/account/' + accountName
+}
+
+function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseInstance) {
+  return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
+}
+
+function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) {
+  const me = accountFollow.AccountFollower
+  const following = accountFollow.AccountFollowing
+
+  return me.url + '#follows/' + following.id
+}
+
+function getAccountFollowAcceptActivityPubUrl (accountFollow: AccountFollowInstance) {
+  const follower = accountFollow.AccountFollower
+  const me = accountFollow.AccountFollowing
+
+  return follower.url + '#accepts/follows/' + me.id
+}
+
+function getAnnounceActivityPubUrl (originalUrl: string, byAccount: AccountInstance) {
+  return originalUrl + '#announces/' + byAccount.id
+}
+
+function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) {
+  return originalUrl + '#updates/' + updatedAt
+}
 
-  return ''
+function getUndoActivityPubUrl (originalUrl: string) {
+  return originalUrl + '/undo'
 }
 
 async function getOrCreateAccount (accountUrl: string) {
@@ -257,7 +292,6 @@ export {
   fetchRemoteAccountAndCreateServer,
   activityPubContextify,
   activityPubCollectionPagination,
-  getActivityPubUrl,
   generateThumbnailFromUrl,
   getOrCreateAccount,
   fetchRemoteVideoPreview,
@@ -265,7 +299,16 @@ export {
   shareVideoChannelByServer,
   shareVideoByServer,
   getOrCreateVideoChannel,
-  buildSignedActivity
+  buildSignedActivity,
+  getVideoActivityPubUrl,
+  getVideoChannelActivityPubUrl,
+  getAccountActivityPubUrl,
+  getVideoAbuseActivityPubUrl,
+  getAccountFollowActivityPubUrl,
+  getAccountFollowAcceptActivityPubUrl,
+  getAnnounceActivityPubUrl,
+  getUpdateActivityPubUrl,
+  getUndoActivityPubUrl
 }
 
 // ---------------------------------------------------------------------------
index 8084cf7b0a8fdff6b95ac564951d7339a83d1db2..9305e092c91a7bbd43fd730dbc7a1eab9db98b8f 100644 (file)
@@ -1,11 +1,11 @@
 import * as validator from 'validator'
+import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity'
 import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account'
+import { isAnnounceValid } from './announce'
 import { isActivityPubUrlValid } from './misc'
+import { isUndoValid } from './undo'
+import { isVideoChannelCreateActivityValid, isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels'
 import {
-  isAnnounceValid,
-  isVideoChannelCreateActivityValid,
-  isVideoChannelDeleteActivityValid,
-  isVideoChannelUpdateActivityValid,
   isVideoFlagValid,
   isVideoTorrentAddActivityValid,
   isVideoTorrentDeleteActivityValid,
@@ -25,18 +25,23 @@ function isRootActivityValid (activity: any) {
     )
 }
 
+const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = {
+  Create: checkCreateActivity,
+  Add: checkAddActivity,
+  Update: checkUpdateActivity,
+  Delete: checkDeleteActivity,
+  Follow: checkFollowActivity,
+  Accept: checkAcceptActivity,
+  Announce: checkAnnounceActivity,
+  Undo: checkUndoActivity
+}
+
 function isActivityValid (activity: any) {
-  return isVideoTorrentAddActivityValid(activity) ||
-    isVideoChannelCreateActivityValid(activity) ||
-    isVideoTorrentUpdateActivityValid(activity) ||
-    isVideoChannelUpdateActivityValid(activity) ||
-    isVideoTorrentDeleteActivityValid(activity) ||
-    isVideoChannelDeleteActivityValid(activity) ||
-    isAccountDeleteActivityValid(activity) ||
-    isAccountFollowActivityValid(activity) ||
-    isAccountAcceptActivityValid(activity) ||
-    isVideoFlagValid(activity) ||
-    isAnnounceValid(activity)
+  const checker = activityCheckers[activity.type]
+  // Unknown activity type
+  if (!checker) return false
+
+  return checker(activity)
 }
 
 // ---------------------------------------------------------------------------
@@ -45,3 +50,41 @@ export {
   isRootActivityValid,
   isActivityValid
 }
+
+// ---------------------------------------------------------------------------
+
+function checkCreateActivity (activity: any) {
+  return isVideoChannelCreateActivityValid(activity) ||
+    isVideoFlagValid(activity)
+}
+
+function checkAddActivity (activity: any) {
+  return isVideoTorrentAddActivityValid(activity)
+}
+
+function checkUpdateActivity (activity: any) {
+  return isVideoTorrentUpdateActivityValid(activity) ||
+    isVideoChannelUpdateActivityValid(activity)
+}
+
+function checkDeleteActivity (activity: any) {
+  return isVideoTorrentDeleteActivityValid(activity) ||
+    isVideoChannelDeleteActivityValid(activity) ||
+    isAccountDeleteActivityValid(activity)
+}
+
+function checkFollowActivity (activity: any) {
+  return isAccountFollowActivityValid(activity)
+}
+
+function checkAcceptActivity (activity: any) {
+  return isAccountAcceptActivityValid(activity)
+}
+
+function checkAnnounceActivity (activity: any) {
+  return isAnnounceValid(activity)
+}
+
+function checkUndoActivity (activity: any) {
+  return isUndoValid(activity)
+}
diff --git a/server/helpers/custom-validators/activitypub/announce.ts b/server/helpers/custom-validators/activitypub/announce.ts
new file mode 100644 (file)
index 0000000..4ba99d1
--- /dev/null
@@ -0,0 +1,15 @@
+import { isBaseActivityValid } from './misc'
+import { isVideoTorrentAddActivityValid } from './videos'
+import { isVideoChannelCreateActivityValid } from './video-channels'
+
+function isAnnounceValid (activity: any) {
+  return isBaseActivityValid(activity, 'Announce') &&
+    (
+      isVideoChannelCreateActivityValid(activity.object) ||
+      isVideoTorrentAddActivityValid(activity.object)
+    )
+}
+
+export {
+  isAnnounceValid
+}
index 0eba06a7bc1aa20aa63bb444330e173ffcb5e29a..6685b269fabedc634401f84c009273df642dee33 100644 (file)
@@ -1,5 +1,7 @@
 export * from './account'
 export * from './activity'
-export * from './signature'
 export * from './misc'
+export * from './signature'
+export * from './undo'
+export * from './video-channels'
 export * from './videos'
diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts
new file mode 100644 (file)
index 0000000..a9a2a3a
--- /dev/null
@@ -0,0 +1,13 @@
+import { isAccountFollowActivityValid } from './account'
+import { isBaseActivityValid } from './misc'
+
+function isUndoValid (activity: any) {
+  return isBaseActivityValid(activity, 'Undo') &&
+    (
+      isAccountFollowActivityValid(activity.object)
+    )
+}
+
+export {
+  isUndoValid
+}
diff --git a/server/helpers/custom-validators/activitypub/video-channels.ts b/server/helpers/custom-validators/activitypub/video-channels.ts
new file mode 100644 (file)
index 0000000..9fd3bb1
--- /dev/null
@@ -0,0 +1,36 @@
+import { isDateValid, isUUIDValid } from '../misc'
+import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
+import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
+
+function isVideoChannelCreateActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Create') &&
+    isVideoChannelObjectValid(activity.object)
+}
+
+function isVideoChannelUpdateActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Update') &&
+    isVideoChannelObjectValid(activity.object)
+}
+
+function isVideoChannelDeleteActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Delete')
+}
+
+function isVideoChannelObjectValid (videoChannel: any) {
+  return videoChannel.type === 'VideoChannel' &&
+    isActivityPubUrlValid(videoChannel.id) &&
+    isVideoChannelNameValid(videoChannel.name) &&
+    isVideoChannelDescriptionValid(videoChannel.content) &&
+    isDateValid(videoChannel.published) &&
+    isDateValid(videoChannel.updated) &&
+    isUUIDValid(videoChannel.uuid)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  isVideoChannelCreateActivityValid,
+  isVideoChannelUpdateActivityValid,
+  isVideoChannelDeleteActivityValid,
+  isVideoChannelObjectValid
+}
index 728511e3db5e8a21c3a8e8fea0bdc02c0b3da883..faeedd3dfded34954066af0ba4c446ab1c2d130a 100644 (file)
@@ -1,7 +1,6 @@
 import * as validator from 'validator'
 import { ACTIVITY_PUB } from '../../../initializers'
 import { exists, isDateValid, isUUIDValid } from '../misc'
-import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
 import {
   isVideoAbuseReasonValid,
   isVideoDurationValid,
@@ -28,6 +27,13 @@ function isVideoTorrentDeleteActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Delete')
 }
 
+function isVideoFlagValid (activity: any) {
+  return isBaseActivityValid(activity, 'Create') &&
+    activity.object.type === 'Flag' &&
+    isVideoAbuseReasonValid(activity.object.content) &&
+    isActivityPubUrlValid(activity.object.object)
+}
+
 function isActivityPubVideoDurationValid (value: string) {
   // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
   return exists(value) &&
@@ -57,57 +63,13 @@ function isVideoTorrentObjectValid (video: any) {
     video.url.length !== 0
 }
 
-function isVideoFlagValid (activity: any) {
-  return isBaseActivityValid(activity, 'Create') &&
-    activity.object.type === 'Flag' &&
-    isVideoAbuseReasonValid(activity.object.content) &&
-    isActivityPubUrlValid(activity.object.object)
-}
-
-function isAnnounceValid (activity: any) {
-  return isBaseActivityValid(activity, 'Announce') &&
-    (
-      isVideoChannelCreateActivityValid(activity.object) ||
-      isVideoTorrentAddActivityValid(activity.object)
-    )
-}
-
-function isVideoChannelCreateActivityValid (activity: any) {
-  return isBaseActivityValid(activity, 'Create') &&
-    isVideoChannelObjectValid(activity.object)
-}
-
-function isVideoChannelUpdateActivityValid (activity: any) {
-  return isBaseActivityValid(activity, 'Update') &&
-    isVideoChannelObjectValid(activity.object)
-}
-
-function isVideoChannelDeleteActivityValid (activity: any) {
-  return isBaseActivityValid(activity, 'Delete')
-}
-
-function isVideoChannelObjectValid (videoChannel: any) {
-  return videoChannel.type === 'VideoChannel' &&
-    isActivityPubUrlValid(videoChannel.id) &&
-    isVideoChannelNameValid(videoChannel.name) &&
-    isVideoChannelDescriptionValid(videoChannel.content) &&
-    isDateValid(videoChannel.published) &&
-    isDateValid(videoChannel.updated) &&
-    isUUIDValid(videoChannel.uuid)
-}
-
 // ---------------------------------------------------------------------------
 
 export {
   isVideoTorrentAddActivityValid,
-  isVideoChannelCreateActivityValid,
   isVideoTorrentUpdateActivityValid,
-  isVideoChannelUpdateActivityValid,
-  isVideoChannelDeleteActivityValid,
   isVideoTorrentDeleteActivityValid,
-  isVideoFlagValid,
-  isAnnounceValid,
-  isVideoChannelObjectValid
+  isVideoFlagValid
 }
 
 // ---------------------------------------------------------------------------
index e08108aac40c6f72328b7a4dd1ba5bded9c6844e..1bea0a412e9235ba1bd5742952d638c97138b4c2 100644 (file)
@@ -1,8 +1,2 @@
-export * from './process-accept'
-export * from './process-add'
-export * from './process-announce'
-export * from './process-create'
-export * from './process-delete'
-export * from './process-follow'
-export * from './process-update'
-export * from './send-request'
+export * from './process'
+export * from './send'
diff --git a/server/lib/activitypub/misc.ts b/server/lib/activitypub/misc.ts
deleted file mode 100644 (file)
index 4c210eb..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-import * as magnetUtil from 'magnet-uri'
-import { VideoTorrentObject } from '../../../shared'
-import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object'
-import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
-import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../initializers/constants'
-import { AccountInstance } from '../../models/account/account-interface'
-import { VideoChannelInstance } from '../../models/video/video-channel-interface'
-import { VideoFileAttributes } from '../../models/video/video-file-interface'
-import { VideoAttributes, VideoInstance } from '../../models/video/video-interface'
-import { VideoPrivacy } from '../../../shared/models/videos/video-privacy.enum'
-
-function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) {
-  return {
-    name: videoChannelObject.name,
-    description: videoChannelObject.content,
-    uuid: videoChannelObject.uuid,
-    url: videoChannelObject.id,
-    createdAt: new Date(videoChannelObject.published),
-    updatedAt: new Date(videoChannelObject.updated),
-    remote: true,
-    accountId: account.id
-  }
-}
-
-async function videoActivityObjectToDBAttributes (
-  videoChannel: VideoChannelInstance,
-  videoObject: VideoTorrentObject,
-  to: string[] = [],
-  cc: string[] = []
-) {
-  let privacy = VideoPrivacy.PRIVATE
-  if (to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.PUBLIC
-  else if (cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.UNLISTED
-
-  const duration = videoObject.duration.replace(/[^\d]+/, '')
-  const videoData: VideoAttributes = {
-    name: videoObject.name,
-    uuid: videoObject.uuid,
-    url: videoObject.id,
-    category: parseInt(videoObject.category.identifier, 10),
-    licence: parseInt(videoObject.licence.identifier, 10),
-    language: parseInt(videoObject.language.identifier, 10),
-    nsfw: videoObject.nsfw,
-    description: videoObject.content,
-    channelId: videoChannel.id,
-    duration: parseInt(duration, 10),
-    createdAt: new Date(videoObject.published),
-    // FIXME: updatedAt does not seems to be considered by Sequelize
-    updatedAt: new Date(videoObject.updated),
-    views: videoObject.views,
-    likes: 0,
-    dislikes: 0,
-    remote: true,
-    privacy
-  }
-
-  return videoData
-}
-
-function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) {
-  const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT)
-  const fileUrls = videoObject.url.filter(u => {
-    return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/')
-  })
-
-  if (fileUrls.length === 0) {
-    throw new Error('Cannot find video files for ' + videoCreated.url)
-  }
-
-  const attributes: VideoFileAttributes[] = []
-  for (const fileUrl of fileUrls) {
-    // Fetch associated magnet uri
-    const magnet = videoObject.url.find(u => {
-      return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width
-    })
-
-    if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url)
-
-    const parsed = magnetUtil.decode(magnet.url)
-    if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url)
-
-    const attribute = {
-      extname: VIDEO_MIMETYPE_EXT[fileUrl.mimeType],
-      infoHash: parsed.infoHash,
-      resolution: fileUrl.width,
-      size: fileUrl.size,
-      videoId: videoCreated.id
-    }
-    attributes.push(attribute)
-  }
-
-  return attributes
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  videoFileActivityUrlToDBAttributes,
-  videoActivityObjectToDBAttributes,
-  videoChannelActivityObjectToDBAttributes
-}
diff --git a/server/lib/activitypub/process-accept.ts b/server/lib/activitypub/process-accept.ts
deleted file mode 100644 (file)
index 9e0cd40..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-import { ActivityAccept } from '../../../shared/models/activitypub/activity'
-import { database as db } from '../../initializers'
-import { AccountInstance } from '../../models/account/account-interface'
-
-async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountInstance) {
-  if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.')
-
-  const targetAccount = await db.Account.loadByUrl(activity.actor)
-
-  return processAccept(inboxAccount, targetAccount)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  processAcceptActivity
-}
-
-// ---------------------------------------------------------------------------
-
-async function processAccept (account: AccountInstance, targetAccount: AccountInstance) {
-  const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id)
-  if (!follow) throw new Error('Cannot find associated follow.')
-
-  follow.set('state', 'accepted')
-  await follow.save()
-}
diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts
deleted file mode 100644 (file)
index e1769be..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-import * as Bluebird from 'bluebird'
-import { VideoTorrentObject } from '../../../shared'
-import { ActivityAdd } from '../../../shared/models/activitypub/activity'
-import { generateThumbnailFromUrl, getOrCreateAccount, logger, retryTransactionWrapper } from '../../helpers'
-import { getOrCreateVideoChannel } from '../../helpers/activitypub'
-import { database as db } from '../../initializers'
-import { AccountInstance } from '../../models/account/account-interface'
-import { VideoChannelInstance } from '../../models/video/video-channel-interface'
-import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
-
-async function processAddActivity (activity: ActivityAdd) {
-  const activityObject = activity.object
-  const activityType = activityObject.type
-  const account = await getOrCreateAccount(activity.actor)
-
-  if (activityType === 'Video') {
-    const videoChannelUrl = activity.target
-    const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl)
-
-    return processAddVideo(account, activity, videoChannel, activityObject)
-  }
-
-  logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
-  return Promise.resolve(undefined)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  processAddActivity
-}
-
-// ---------------------------------------------------------------------------
-
-function processAddVideo (account: AccountInstance, activity: ActivityAdd, videoChannel: VideoChannelInstance, video: VideoTorrentObject) {
-  const options = {
-    arguments: [ account, activity, videoChannel, video ],
-    errorMessage: 'Cannot insert the remote video with many retries.'
-  }
-
-  return retryTransactionWrapper(addRemoteVideo, options)
-}
-
-function addRemoteVideo (
-  account: AccountInstance,
-  activity: ActivityAdd,
-  videoChannel: VideoChannelInstance,
-  videoToCreateData: VideoTorrentObject
-) {
-  logger.debug('Adding remote video %s.', videoToCreateData.url)
-
-  return db.sequelize.transaction(async t => {
-    const sequelizeOptions = {
-      transaction: t
-    }
-
-    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.')
-
-    const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, activity.to, activity.cc)
-    const video = db.Video.build(videoData)
-
-    // Don't block on request
-    generateThumbnailFromUrl(video, videoToCreateData.icon)
-      .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
-
-    const videoCreated = await video.save(sequelizeOptions)
-
-    const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
-    if (videoFileAttributes.length === 0) {
-      throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url)
-    }
-
-    const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f, { transaction: t }))
-    await Promise.all(tasks)
-
-    const tags = videoToCreateData.tag.map(t => t.name)
-    const tagInstances = await db.Tag.findOrCreateTags(tags, t)
-    await videoCreated.setTags(tagInstances, sequelizeOptions)
-
-    logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
-
-    return videoCreated
-  })
-}
diff --git a/server/lib/activitypub/process-announce.ts b/server/lib/activitypub/process-announce.ts
deleted file mode 100644 (file)
index eb38aec..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-import { ActivityAnnounce } from '../../../shared/models/activitypub/activity'
-import { getOrCreateAccount } from '../../helpers/activitypub'
-import { logger } from '../../helpers/logger'
-import { database as db } from '../../initializers/index'
-import { VideoInstance } from '../../models/index'
-import { VideoChannelInstance } from '../../models/video/video-channel-interface'
-import { processAddActivity } from './process-add'
-import { processCreateActivity } from './process-create'
-
-async function processAnnounceActivity (activity: ActivityAnnounce) {
-  const announcedActivity = activity.object
-  const accountAnnouncer = await getOrCreateAccount(activity.actor)
-
-  if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') {
-    // Add share entry
-    const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity)
-    await db.VideoChannelShare.create({
-      accountId: accountAnnouncer.id,
-      videoChannelId: videoChannel.id
-    })
-
-    return undefined
-  } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') {
-    // Add share entry
-    const video: VideoInstance = await processAddActivity(announcedActivity)
-    await db.VideoShare.create({
-      accountId: accountAnnouncer.id,
-      videoId: video.id
-    })
-
-    return undefined
-  }
-
-  logger.warn(
-    'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type,
-    { activity: activity.id }
-  )
-  return Promise.resolve(undefined)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  processAnnounceActivity
-}
diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts
deleted file mode 100644 (file)
index de8e09a..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-import { ActivityCreate, VideoChannelObject } from '../../../shared'
-import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object'
-import { logger, retryTransactionWrapper } from '../../helpers'
-import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub'
-import { database as db } from '../../initializers'
-import { AccountInstance } from '../../models/account/account-interface'
-import { videoChannelActivityObjectToDBAttributes } from './misc'
-
-async function processCreateActivity (activity: ActivityCreate) {
-  const activityObject = activity.object
-  const activityType = activityObject.type
-  const account = await getOrCreateAccount(activity.actor)
-
-  if (activityType === 'VideoChannel') {
-    return processCreateVideoChannel(account, activityObject as VideoChannelObject)
-  } else if (activityType === 'Flag') {
-    return processCreateVideoAbuse(account, activityObject as VideoAbuseObject)
-  }
-
-  logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
-  return Promise.resolve(undefined)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  processCreateActivity
-}
-
-// ---------------------------------------------------------------------------
-
-function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
-  const options = {
-    arguments: [ account, videoChannelToCreateData ],
-    errorMessage: 'Cannot insert the remote video channel with many retries.'
-  }
-
-  return retryTransactionWrapper(addRemoteVideoChannel, options)
-}
-
-function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
-  logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
-
-  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.')
-
-    const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account)
-    videoChannel = db.VideoChannel.build(videoChannelData)
-    videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid)
-
-    videoChannel = await videoChannel.save({ transaction: t })
-    logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
-
-    return videoChannel
-  })
-}
-
-function processCreateVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
-  const options = {
-    arguments: [ account, videoAbuseToCreateData ],
-    errorMessage: 'Cannot insert the remote video abuse with many retries.'
-  }
-
-  return retryTransactionWrapper(addRemoteVideoAbuse, options)
-}
-
-function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
-  logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object)
-
-  return db.sequelize.transaction(async t => {
-    const video = await db.Video.loadByUrlAndPopulateAccount(videoAbuseToCreateData.object, t)
-    if (!video) {
-      logger.warn('Unknown video %s for remote video abuse.', videoAbuseToCreateData.object)
-      return undefined
-    }
-
-    const videoAbuseData = {
-      reporterAccountId: account.id,
-      reason: videoAbuseToCreateData.content,
-      videoId: video.id
-    }
-
-    await db.VideoAbuse.create(videoAbuseData)
-
-    logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object)
-  })
-}
diff --git a/server/lib/activitypub/process-delete.ts b/server/lib/activitypub/process-delete.ts
deleted file mode 100644 (file)
index 0d5756e..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-import { ActivityDelete } from '../../../shared/models/activitypub/activity'
-import { getOrCreateAccount } from '../../helpers/activitypub'
-import { retryTransactionWrapper } from '../../helpers/database-utils'
-import { logger } from '../../helpers/logger'
-import { database as db } from '../../initializers'
-import { AccountInstance } from '../../models/account/account-interface'
-import { VideoChannelInstance } from '../../models/video/video-channel-interface'
-import { VideoInstance } from '../../models/video/video-interface'
-
-async function processDeleteActivity (activity: ActivityDelete) {
-  const account = await getOrCreateAccount(activity.actor)
-
-  if (account.url === activity.id) {
-    return processDeleteAccount(account)
-  }
-
-  {
-    let videoObject = await db.Video.loadByUrlAndPopulateAccount(activity.id)
-    if (videoObject !== undefined) {
-      return processDeleteVideo(account, videoObject)
-    }
-  }
-
-  {
-    let videoChannelObject = await db.VideoChannel.loadByUrl(activity.id)
-    if (videoChannelObject !== undefined) {
-      return processDeleteVideoChannel(account, videoChannelObject)
-    }
-  }
-
-  return
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  processDeleteActivity
-}
-
-// ---------------------------------------------------------------------------
-
-async function processDeleteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
-  const options = {
-    arguments: [ account, videoToDelete ],
-    errorMessage: 'Cannot remove the remote video with many retries.'
-  }
-
-  await retryTransactionWrapper(deleteRemoteVideo, options)
-}
-
-async function deleteRemoteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
-  logger.debug('Removing remote video "%s".', videoToDelete.uuid)
-
-  await db.sequelize.transaction(async t => {
-    if (videoToDelete.VideoChannel.Account.id !== account.id) {
-      throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url)
-    }
-
-    await videoToDelete.destroy({ transaction: t })
-  })
-
-  logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
-}
-
-async function processDeleteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
-  const options = {
-    arguments: [ account, videoChannelToRemove ],
-    errorMessage: 'Cannot remove the remote video channel with many retries.'
-  }
-
-  await retryTransactionWrapper(deleteRemoteVideoChannel, options)
-}
-
-async function deleteRemoteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
-  logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid)
-
-  await db.sequelize.transaction(async t => {
-    if (videoChannelToRemove.Account.id !== account.id) {
-      throw new Error('Account ' + account.url + ' does not own video channel ' + videoChannelToRemove.url)
-    }
-
-    await videoChannelToRemove.destroy({ transaction: t })
-  })
-
-  logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid)
-}
-
-async function processDeleteAccount (accountToRemove: AccountInstance) {
-  const options = {
-    arguments: [ accountToRemove ],
-    errorMessage: 'Cannot remove the remote account with many retries.'
-  }
-
-  await retryTransactionWrapper(deleteRemoteAccount, options)
-}
-
-async function deleteRemoteAccount (accountToRemove: AccountInstance) {
-  logger.debug('Removing remote account "%s".', accountToRemove.uuid)
-
-  await db.sequelize.transaction(async t => {
-    await accountToRemove.destroy({ transaction: t })
-  })
-
-  logger.info('Remote account with uuid %s removed.', accountToRemove.uuid)
-}
diff --git a/server/lib/activitypub/process-follow.ts b/server/lib/activitypub/process-follow.ts
deleted file mode 100644 (file)
index a805c07..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-import { ActivityFollow } from '../../../shared/models/activitypub/activity'
-import { getOrCreateAccount, retryTransactionWrapper } from '../../helpers'
-import { database as db } from '../../initializers'
-import { AccountInstance } from '../../models/account/account-interface'
-import { sendAccept } from './send-request'
-import { logger } from '../../helpers/logger'
-
-async function processFollowActivity (activity: ActivityFollow) {
-  const activityObject = activity.object
-  const account = await getOrCreateAccount(activity.actor)
-
-  return processFollow(account, activityObject)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  processFollowActivity
-}
-
-// ---------------------------------------------------------------------------
-
-function processFollow (account: AccountInstance, targetAccountURL: string) {
-  const options = {
-    arguments: [ account, targetAccountURL ],
-    errorMessage: 'Cannot follow with many retries.'
-  }
-
-  return retryTransactionWrapper(follow, options)
-}
-
-async function follow (account: AccountInstance, targetAccountURL: string) {
-  await db.sequelize.transaction(async t => {
-    const targetAccount = await db.Account.loadByUrl(targetAccountURL, t)
-
-    if (targetAccount === undefined) throw new Error('Unknown account')
-    if (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
-
-    await db.AccountFollow.findOrCreate({
-      where: {
-        accountId: account.id,
-        targetAccountId: targetAccount.id
-      },
-      defaults: {
-        accountId: account.id,
-        targetAccountId: targetAccount.id,
-        state: 'accepted'
-      },
-      transaction: t
-    })
-
-    // Target sends to account he accepted the follow request
-    return sendAccept(targetAccount, account, t)
-  })
-
-  logger.info('Account uuid %s is followed by account %s.', account.url, targetAccountURL)
-}
diff --git a/server/lib/activitypub/process-update.ts b/server/lib/activitypub/process-update.ts
deleted file mode 100644 (file)
index a9aa5ee..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-import { VideoChannelObject, VideoTorrentObject } from '../../../shared'
-import { ActivityUpdate } from '../../../shared/models/activitypub/activity'
-import { getOrCreateAccount } from '../../helpers/activitypub'
-import { retryTransactionWrapper } from '../../helpers/database-utils'
-import { logger } from '../../helpers/logger'
-import { resetSequelizeInstance } from '../../helpers/utils'
-import { database as db } from '../../initializers'
-import { AccountInstance } from '../../models/account/account-interface'
-import { VideoInstance } from '../../models/video/video-interface'
-import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
-import Bluebird = require('bluebird')
-
-async function processUpdateActivity (activity: ActivityUpdate) {
-  const account = await getOrCreateAccount(activity.actor)
-
-  if (activity.object.type === 'Video') {
-    return processUpdateVideo(account, activity.object)
-  } else if (activity.object.type === 'VideoChannel') {
-    return processUpdateVideoChannel(account, activity.object)
-  }
-
-  return undefined
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  processUpdateActivity
-}
-
-// ---------------------------------------------------------------------------
-
-function processUpdateVideo (account: AccountInstance, video: VideoTorrentObject) {
-  const options = {
-    arguments: [ account, video ],
-    errorMessage: 'Cannot update the remote video with many retries'
-  }
-
-  return retryTransactionWrapper(updateRemoteVideo, options)
-}
-
-async function updateRemoteVideo (account: AccountInstance, videoAttributesToUpdate: VideoTorrentObject) {
-  logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
-  let videoInstance: VideoInstance
-  let videoFieldsSave: object
-
-  try {
-    await db.sequelize.transaction(async t => {
-      const sequelizeOptions = {
-        transaction: t
-      }
-
-      const videoInstance = await db.Video.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t)
-      if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.')
-
-      if (videoInstance.VideoChannel.Account.id !== account.id) {
-        throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url)
-      }
-
-      const videoData = await videoActivityObjectToDBAttributes(videoInstance.VideoChannel, videoAttributesToUpdate)
-      videoInstance.set('name', videoData.name)
-      videoInstance.set('category', videoData.category)
-      videoInstance.set('licence', videoData.licence)
-      videoInstance.set('language', videoData.language)
-      videoInstance.set('nsfw', videoData.nsfw)
-      videoInstance.set('description', videoData.description)
-      videoInstance.set('duration', videoData.duration)
-      videoInstance.set('createdAt', videoData.createdAt)
-      videoInstance.set('updatedAt', videoData.updatedAt)
-      videoInstance.set('views', videoData.views)
-      // videoInstance.set('likes', videoData.likes)
-      // videoInstance.set('dislikes', videoData.dislikes)
-
-      await videoInstance.save(sequelizeOptions)
-
-      // Remove old video files
-      const videoFileDestroyTasks: Bluebird<void>[] = []
-      for (const videoFile of videoInstance.VideoFiles) {
-        videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
-      }
-      await Promise.all(videoFileDestroyTasks)
-
-      const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate)
-      const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f))
-      await Promise.all(tasks)
-
-      const tags = videoAttributesToUpdate.tag.map(t => t.name)
-      const tagInstances = await db.Tag.findOrCreateTags(tags, t)
-      await videoInstance.setTags(tagInstances, sequelizeOptions)
-    })
-
-    logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
-  } catch (err) {
-    if (videoInstance !== undefined && videoFieldsSave !== undefined) {
-      resetSequelizeInstance(videoInstance, videoFieldsSave)
-    }
-
-    // This is just a debug because we will retry the insert
-    logger.debug('Cannot update the remote video.', err)
-    throw err
-  }
-}
-
-async function processUpdateVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
-  const options = {
-    arguments: [ account, videoChannel ],
-    errorMessage: 'Cannot update the remote video channel with many retries.'
-  }
-
-  await retryTransactionWrapper(updateRemoteVideoChannel, options)
-}
-
-async function updateRemoteVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
-  logger.debug('Updating remote video channel "%s".', videoChannel.uuid)
-
-  await db.sequelize.transaction(async t => {
-    const sequelizeOptions = { transaction: t }
-
-    const videoChannelInstance = await db.VideoChannel.loadByUrl(videoChannel.id)
-    if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.')
-
-    if (videoChannelInstance.Account.id !== account.id) {
-      throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url)
-    }
-
-    videoChannelInstance.set('name', videoChannel.name)
-    videoChannelInstance.set('description', videoChannel.content)
-    videoChannelInstance.set('createdAt', videoChannel.published)
-    videoChannelInstance.set('updatedAt', videoChannel.updated)
-
-    await videoChannelInstance.save(sequelizeOptions)
-  })
-
-  logger.info('Remote video channel with uuid %s updated', videoChannel.uuid)
-}
diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts
new file mode 100644 (file)
index 0000000..e80b46b
--- /dev/null
@@ -0,0 +1,8 @@
+export * from './process-accept'
+export * from './process-add'
+export * from './process-announce'
+export * from './process-create'
+export * from './process-delete'
+export * from './process-follow'
+export * from './process-undo'
+export * from './process-update'
diff --git a/server/lib/activitypub/process/misc.ts b/server/lib/activitypub/process/misc.ts
new file mode 100644 (file)
index 0000000..e90a793
--- /dev/null
@@ -0,0 +1,101 @@
+import * as magnetUtil from 'magnet-uri'
+import { VideoTorrentObject } from '../../../../shared'
+import { VideoChannelObject } from '../../../../shared/models/activitypub/objects/video-channel-object'
+import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos'
+import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../../initializers/constants'
+import { AccountInstance } from '../../../models/account/account-interface'
+import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
+import { VideoFileAttributes } from '../../../models/video/video-file-interface'
+import { VideoAttributes, VideoInstance } from '../../../models/video/video-interface'
+import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
+
+function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) {
+  return {
+    name: videoChannelObject.name,
+    description: videoChannelObject.content,
+    uuid: videoChannelObject.uuid,
+    url: videoChannelObject.id,
+    createdAt: new Date(videoChannelObject.published),
+    updatedAt: new Date(videoChannelObject.updated),
+    remote: true,
+    accountId: account.id
+  }
+}
+
+async function videoActivityObjectToDBAttributes (
+  videoChannel: VideoChannelInstance,
+  videoObject: VideoTorrentObject,
+  to: string[] = [],
+  cc: string[] = []
+) {
+  let privacy = VideoPrivacy.PRIVATE
+  if (to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.PUBLIC
+  else if (cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.UNLISTED
+
+  const duration = videoObject.duration.replace(/[^\d]+/, '')
+  const videoData: VideoAttributes = {
+    name: videoObject.name,
+    uuid: videoObject.uuid,
+    url: videoObject.id,
+    category: parseInt(videoObject.category.identifier, 10),
+    licence: parseInt(videoObject.licence.identifier, 10),
+    language: parseInt(videoObject.language.identifier, 10),
+    nsfw: videoObject.nsfw,
+    description: videoObject.content,
+    channelId: videoChannel.id,
+    duration: parseInt(duration, 10),
+    createdAt: new Date(videoObject.published),
+    // FIXME: updatedAt does not seems to be considered by Sequelize
+    updatedAt: new Date(videoObject.updated),
+    views: videoObject.views,
+    likes: 0,
+    dislikes: 0,
+    remote: true,
+    privacy
+  }
+
+  return videoData
+}
+
+function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) {
+  const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT)
+  const fileUrls = videoObject.url.filter(u => {
+    return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/')
+  })
+
+  if (fileUrls.length === 0) {
+    throw new Error('Cannot find video files for ' + videoCreated.url)
+  }
+
+  const attributes: VideoFileAttributes[] = []
+  for (const fileUrl of fileUrls) {
+    // Fetch associated magnet uri
+    const magnet = videoObject.url.find(u => {
+      return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width
+    })
+
+    if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url)
+
+    const parsed = magnetUtil.decode(magnet.url)
+    if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url)
+
+    const attribute = {
+      extname: VIDEO_MIMETYPE_EXT[fileUrl.mimeType],
+      infoHash: parsed.infoHash,
+      resolution: fileUrl.width,
+      size: fileUrl.size,
+      videoId: videoCreated.id
+    }
+    attributes.push(attribute)
+  }
+
+  return attributes
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  videoFileActivityUrlToDBAttributes,
+  videoActivityObjectToDBAttributes,
+  videoChannelActivityObjectToDBAttributes
+}
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts
new file mode 100644 (file)
index 0000000..e159c41
--- /dev/null
@@ -0,0 +1,27 @@
+import { ActivityAccept } from '../../../../shared/models/activitypub/activity'
+import { database as db } from '../../../initializers'
+import { AccountInstance } from '../../../models/account/account-interface'
+
+async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountInstance) {
+  if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.')
+
+  const targetAccount = await db.Account.loadByUrl(activity.actor)
+
+  return processAccept(inboxAccount, targetAccount)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processAcceptActivity
+}
+
+// ---------------------------------------------------------------------------
+
+async function processAccept (account: AccountInstance, targetAccount: AccountInstance) {
+  const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id)
+  if (!follow) throw new Error('Cannot find associated follow.')
+
+  follow.set('state', 'accepted')
+  await follow.save()
+}
diff --git a/server/lib/activitypub/process/process-add.ts b/server/lib/activitypub/process/process-add.ts
new file mode 100644 (file)
index 0000000..f064c1a
--- /dev/null
@@ -0,0 +1,87 @@
+import * as Bluebird from 'bluebird'
+import { VideoTorrentObject } from '../../../../shared'
+import { ActivityAdd } from '../../../../shared/models/activitypub/activity'
+import { generateThumbnailFromUrl, getOrCreateAccount, logger, retryTransactionWrapper } from '../../../helpers'
+import { getOrCreateVideoChannel } from '../../../helpers/activitypub'
+import { database as db } from '../../../initializers'
+import { AccountInstance } from '../../../models/account/account-interface'
+import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
+import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
+
+async function processAddActivity (activity: ActivityAdd) {
+  const activityObject = activity.object
+  const activityType = activityObject.type
+  const account = await getOrCreateAccount(activity.actor)
+
+  if (activityType === 'Video') {
+    const videoChannelUrl = activity.target
+    const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl)
+
+    return processAddVideo(account, activity, videoChannel, activityObject)
+  }
+
+  logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
+  return Promise.resolve(undefined)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processAddActivity
+}
+
+// ---------------------------------------------------------------------------
+
+function processAddVideo (account: AccountInstance, activity: ActivityAdd, videoChannel: VideoChannelInstance, video: VideoTorrentObject) {
+  const options = {
+    arguments: [ account, activity, videoChannel, video ],
+    errorMessage: 'Cannot insert the remote video with many retries.'
+  }
+
+  return retryTransactionWrapper(addRemoteVideo, options)
+}
+
+function addRemoteVideo (
+  account: AccountInstance,
+  activity: ActivityAdd,
+  videoChannel: VideoChannelInstance,
+  videoToCreateData: VideoTorrentObject
+) {
+  logger.debug('Adding remote video %s.', videoToCreateData.url)
+
+  return db.sequelize.transaction(async t => {
+    const sequelizeOptions = {
+      transaction: t
+    }
+
+    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.')
+
+    const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, activity.to, activity.cc)
+    const video = db.Video.build(videoData)
+
+    // Don't block on request
+    generateThumbnailFromUrl(video, videoToCreateData.icon)
+      .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
+
+    const videoCreated = await video.save(sequelizeOptions)
+
+    const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
+    if (videoFileAttributes.length === 0) {
+      throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url)
+    }
+
+    const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f, { transaction: t }))
+    await Promise.all(tasks)
+
+    const tags = videoToCreateData.tag.map(t => t.name)
+    const tagInstances = await db.Tag.findOrCreateTags(tags, t)
+    await videoCreated.setTags(tagInstances, sequelizeOptions)
+
+    logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
+
+    return videoCreated
+  })
+}
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
new file mode 100644 (file)
index 0000000..656db08
--- /dev/null
@@ -0,0 +1,46 @@
+import { ActivityAnnounce } from '../../../../shared/models/activitypub/activity'
+import { getOrCreateAccount } from '../../../helpers/activitypub'
+import { logger } from '../../../helpers/logger'
+import { database as db } from '../../../initializers/index'
+import { VideoInstance } from '../../../models/index'
+import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
+import { processAddActivity } from './process-add'
+import { processCreateActivity } from './process-create'
+
+async function processAnnounceActivity (activity: ActivityAnnounce) {
+  const announcedActivity = activity.object
+  const accountAnnouncer = await getOrCreateAccount(activity.actor)
+
+  if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') {
+    // Add share entry
+    const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity)
+    await db.VideoChannelShare.create({
+      accountId: accountAnnouncer.id,
+      videoChannelId: videoChannel.id
+    })
+
+    return undefined
+  } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') {
+    // Add share entry
+    const video: VideoInstance = await processAddActivity(announcedActivity)
+    await db.VideoShare.create({
+      accountId: accountAnnouncer.id,
+      videoId: video.id
+    })
+
+    return undefined
+  }
+
+  logger.warn(
+    'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type,
+    { activity: activity.id }
+  )
+
+  return undefined
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processAnnounceActivity
+}
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
new file mode 100644 (file)
index 0000000..aac941a
--- /dev/null
@@ -0,0 +1,88 @@
+import { ActivityCreate, VideoChannelObject } from '../../../../shared'
+import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects/video-abuse-object'
+import { logger, retryTransactionWrapper } from '../../../helpers'
+import { getOrCreateAccount, getVideoChannelActivityPubUrl } from '../../../helpers/activitypub'
+import { database as db } from '../../../initializers'
+import { AccountInstance } from '../../../models/account/account-interface'
+import { videoChannelActivityObjectToDBAttributes } from './misc'
+
+async function processCreateActivity (activity: ActivityCreate) {
+  const activityObject = activity.object
+  const activityType = activityObject.type
+  const account = await getOrCreateAccount(activity.actor)
+
+  if (activityType === 'VideoChannel') {
+    return processCreateVideoChannel(account, activityObject as VideoChannelObject)
+  } else if (activityType === 'Flag') {
+    return processCreateVideoAbuse(account, activityObject as VideoAbuseObject)
+  }
+
+  logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
+  return Promise.resolve(undefined)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processCreateActivity
+}
+
+// ---------------------------------------------------------------------------
+
+function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
+  const options = {
+    arguments: [ account, videoChannelToCreateData ],
+    errorMessage: 'Cannot insert the remote video channel with many retries.'
+  }
+
+  return retryTransactionWrapper(addRemoteVideoChannel, options)
+}
+
+function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
+  logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
+
+  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.')
+
+    const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account)
+    videoChannel = db.VideoChannel.build(videoChannelData)
+    videoChannel.url = getVideoChannelActivityPubUrl(videoChannel)
+
+    videoChannel = await videoChannel.save({ transaction: t })
+    logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
+
+    return videoChannel
+  })
+}
+
+function processCreateVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
+  const options = {
+    arguments: [ account, videoAbuseToCreateData ],
+    errorMessage: 'Cannot insert the remote video abuse with many retries.'
+  }
+
+  return retryTransactionWrapper(addRemoteVideoAbuse, options)
+}
+
+function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
+  logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object)
+
+  return db.sequelize.transaction(async t => {
+    const video = await db.Video.loadByUrlAndPopulateAccount(videoAbuseToCreateData.object, t)
+    if (!video) {
+      logger.warn('Unknown video %s for remote video abuse.', videoAbuseToCreateData.object)
+      return undefined
+    }
+
+    const videoAbuseData = {
+      reporterAccountId: account.id,
+      reason: videoAbuseToCreateData.content,
+      videoId: video.id
+    }
+
+    await db.VideoAbuse.create(videoAbuseData)
+
+    logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object)
+  })
+}
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
new file mode 100644 (file)
index 0000000..af5d964
--- /dev/null
@@ -0,0 +1,105 @@
+import { ActivityDelete } from '../../../../shared/models/activitypub/activity'
+import { getOrCreateAccount } from '../../../helpers/activitypub'
+import { retryTransactionWrapper } from '../../../helpers/database-utils'
+import { logger } from '../../../helpers/logger'
+import { database as db } from '../../../initializers'
+import { AccountInstance } from '../../../models/account/account-interface'
+import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
+import { VideoInstance } from '../../../models/video/video-interface'
+
+async function processDeleteActivity (activity: ActivityDelete) {
+  const account = await getOrCreateAccount(activity.actor)
+
+  if (account.url === activity.id) {
+    return processDeleteAccount(account)
+  }
+
+  {
+    let videoObject = await db.Video.loadByUrlAndPopulateAccount(activity.id)
+    if (videoObject !== undefined) {
+      return processDeleteVideo(account, videoObject)
+    }
+  }
+
+  {
+    let videoChannelObject = await db.VideoChannel.loadByUrl(activity.id)
+    if (videoChannelObject !== undefined) {
+      return processDeleteVideoChannel(account, videoChannelObject)
+    }
+  }
+
+  return
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processDeleteActivity
+}
+
+// ---------------------------------------------------------------------------
+
+async function processDeleteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
+  const options = {
+    arguments: [ account, videoToDelete ],
+    errorMessage: 'Cannot remove the remote video with many retries.'
+  }
+
+  await retryTransactionWrapper(deleteRemoteVideo, options)
+}
+
+async function deleteRemoteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
+  logger.debug('Removing remote video "%s".', videoToDelete.uuid)
+
+  await db.sequelize.transaction(async t => {
+    if (videoToDelete.VideoChannel.Account.id !== account.id) {
+      throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url)
+    }
+
+    await videoToDelete.destroy({ transaction: t })
+  })
+
+  logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
+}
+
+async function processDeleteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
+  const options = {
+    arguments: [ account, videoChannelToRemove ],
+    errorMessage: 'Cannot remove the remote video channel with many retries.'
+  }
+
+  await retryTransactionWrapper(deleteRemoteVideoChannel, options)
+}
+
+async function deleteRemoteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
+  logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid)
+
+  await db.sequelize.transaction(async t => {
+    if (videoChannelToRemove.Account.id !== account.id) {
+      throw new Error('Account ' + account.url + ' does not own video channel ' + videoChannelToRemove.url)
+    }
+
+    await videoChannelToRemove.destroy({ transaction: t })
+  })
+
+  logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid)
+}
+
+async function processDeleteAccount (accountToRemove: AccountInstance) {
+  const options = {
+    arguments: [ accountToRemove ],
+    errorMessage: 'Cannot remove the remote account with many retries.'
+  }
+
+  await retryTransactionWrapper(deleteRemoteAccount, options)
+}
+
+async function deleteRemoteAccount (accountToRemove: AccountInstance) {
+  logger.debug('Removing remote account "%s".', accountToRemove.uuid)
+
+  await db.sequelize.transaction(async t => {
+    await accountToRemove.destroy({ transaction: t })
+  })
+
+  logger.info('Remote account with uuid %s removed.', accountToRemove.uuid)
+}
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
new file mode 100644 (file)
index 0000000..5536395
--- /dev/null
@@ -0,0 +1,59 @@
+import { ActivityFollow } from '../../../../shared/models/activitypub/activity'
+import { getOrCreateAccount, retryTransactionWrapper } from '../../../helpers'
+import { database as db } from '../../../initializers'
+import { AccountInstance } from '../../../models/account/account-interface'
+import { logger } from '../../../helpers/logger'
+import { sendAccept } from '../send/send-accept'
+
+async function processFollowActivity (activity: ActivityFollow) {
+  const activityObject = activity.object
+  const account = await getOrCreateAccount(activity.actor)
+
+  return processFollow(account, activityObject)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processFollowActivity
+}
+
+// ---------------------------------------------------------------------------
+
+function processFollow (account: AccountInstance, targetAccountURL: string) {
+  const options = {
+    arguments: [ account, targetAccountURL ],
+    errorMessage: 'Cannot follow with many retries.'
+  }
+
+  return retryTransactionWrapper(follow, options)
+}
+
+async function follow (account: AccountInstance, targetAccountURL: string) {
+  await db.sequelize.transaction(async t => {
+    const targetAccount = await db.Account.loadByUrl(targetAccountURL, t)
+
+    if (!targetAccount) throw new Error('Unknown account')
+    if (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
+
+    const [ accountFollow ] = await db.AccountFollow.findOrCreate({
+      where: {
+        accountId: account.id,
+        targetAccountId: targetAccount.id
+      },
+      defaults: {
+        accountId: account.id,
+        targetAccountId: targetAccount.id,
+        state: 'accepted'
+      },
+      transaction: t
+    })
+    accountFollow.AccountFollower = account
+    accountFollow.AccountFollowing = targetAccount
+
+    // Target sends to account he accepted the follow request
+    return sendAccept(accountFollow, t)
+  })
+
+  logger.info('Account uuid %s is followed by account %s.', account.url, targetAccountURL)
+}
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
new file mode 100644 (file)
index 0000000..5d09423
--- /dev/null
@@ -0,0 +1,31 @@
+import { ActivityUndo } from '../../../../shared/models/activitypub/activity'
+import { logger } from '../../../helpers/logger'
+import { database as db } from '../../../initializers'
+
+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
+  }
+
+  logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id })
+
+  return undefined
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processUndoActivity
+}
+
+// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
new file mode 100644 (file)
index 0000000..a3bfb1b
--- /dev/null
@@ -0,0 +1,135 @@
+import { VideoChannelObject, VideoTorrentObject } from '../../../../shared'
+import { ActivityUpdate } from '../../../../shared/models/activitypub/activity'
+import { getOrCreateAccount } from '../../../helpers/activitypub'
+import { retryTransactionWrapper } from '../../../helpers/database-utils'
+import { logger } from '../../../helpers/logger'
+import { resetSequelizeInstance } from '../../../helpers/utils'
+import { database as db } from '../../../initializers'
+import { AccountInstance } from '../../../models/account/account-interface'
+import { VideoInstance } from '../../../models/video/video-interface'
+import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
+import Bluebird = require('bluebird')
+
+async function processUpdateActivity (activity: ActivityUpdate) {
+  const account = await getOrCreateAccount(activity.actor)
+
+  if (activity.object.type === 'Video') {
+    return processUpdateVideo(account, activity.object)
+  } else if (activity.object.type === 'VideoChannel') {
+    return processUpdateVideoChannel(account, activity.object)
+  }
+
+  return undefined
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processUpdateActivity
+}
+
+// ---------------------------------------------------------------------------
+
+function processUpdateVideo (account: AccountInstance, video: VideoTorrentObject) {
+  const options = {
+    arguments: [ account, video ],
+    errorMessage: 'Cannot update the remote video with many retries'
+  }
+
+  return retryTransactionWrapper(updateRemoteVideo, options)
+}
+
+async function updateRemoteVideo (account: AccountInstance, videoAttributesToUpdate: VideoTorrentObject) {
+  logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
+  let videoInstance: VideoInstance
+  let videoFieldsSave: object
+
+  try {
+    await db.sequelize.transaction(async t => {
+      const sequelizeOptions = {
+        transaction: t
+      }
+
+      const videoInstance = await db.Video.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t)
+      if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.')
+
+      if (videoInstance.VideoChannel.Account.id !== account.id) {
+        throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url)
+      }
+
+      const videoData = await videoActivityObjectToDBAttributes(videoInstance.VideoChannel, videoAttributesToUpdate)
+      videoInstance.set('name', videoData.name)
+      videoInstance.set('category', videoData.category)
+      videoInstance.set('licence', videoData.licence)
+      videoInstance.set('language', videoData.language)
+      videoInstance.set('nsfw', videoData.nsfw)
+      videoInstance.set('description', videoData.description)
+      videoInstance.set('duration', videoData.duration)
+      videoInstance.set('createdAt', videoData.createdAt)
+      videoInstance.set('updatedAt', videoData.updatedAt)
+      videoInstance.set('views', videoData.views)
+      // videoInstance.set('likes', videoData.likes)
+      // videoInstance.set('dislikes', videoData.dislikes)
+
+      await videoInstance.save(sequelizeOptions)
+
+      // Remove old video files
+      const videoFileDestroyTasks: Bluebird<void>[] = []
+      for (const videoFile of videoInstance.VideoFiles) {
+        videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
+      }
+      await Promise.all(videoFileDestroyTasks)
+
+      const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate)
+      const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f))
+      await Promise.all(tasks)
+
+      const tags = videoAttributesToUpdate.tag.map(t => t.name)
+      const tagInstances = await db.Tag.findOrCreateTags(tags, t)
+      await videoInstance.setTags(tagInstances, sequelizeOptions)
+    })
+
+    logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
+  } catch (err) {
+    if (videoInstance !== undefined && videoFieldsSave !== undefined) {
+      resetSequelizeInstance(videoInstance, videoFieldsSave)
+    }
+
+    // This is just a debug because we will retry the insert
+    logger.debug('Cannot update the remote video.', err)
+    throw err
+  }
+}
+
+async function processUpdateVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
+  const options = {
+    arguments: [ account, videoChannel ],
+    errorMessage: 'Cannot update the remote video channel with many retries.'
+  }
+
+  await retryTransactionWrapper(updateRemoteVideoChannel, options)
+}
+
+async function updateRemoteVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
+  logger.debug('Updating remote video channel "%s".', videoChannel.uuid)
+
+  await db.sequelize.transaction(async t => {
+    const sequelizeOptions = { transaction: t }
+
+    const videoChannelInstance = await db.VideoChannel.loadByUrl(videoChannel.id)
+    if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.')
+
+    if (videoChannelInstance.Account.id !== account.id) {
+      throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url)
+    }
+
+    videoChannelInstance.set('name', videoChannel.name)
+    videoChannelInstance.set('description', videoChannel.content)
+    videoChannelInstance.set('createdAt', videoChannel.published)
+    videoChannelInstance.set('updatedAt', videoChannel.updated)
+
+    await videoChannelInstance.save(sequelizeOptions)
+  })
+
+  logger.info('Remote video channel with uuid %s updated', videoChannel.uuid)
+}
diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts
deleted file mode 100644 (file)
index 261ff04..0000000
+++ /dev/null
@@ -1,275 +0,0 @@
-import { Transaction } from 'sequelize'
-import {
-  ActivityAccept,
-  ActivityAdd,
-  ActivityCreate,
-  ActivityDelete,
-  ActivityFollow,
-  ActivityUpdate
-} from '../../../shared/models/activitypub/activity'
-import { getActivityPubUrl } from '../../helpers/activitypub'
-import { logger } from '../../helpers/logger'
-import { database as db } from '../../initializers'
-import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../models'
-import { VideoAbuseInstance } from '../../models/video/video-abuse-interface'
-import { activitypubHttpJobScheduler } from '../jobs'
-import { ACTIVITY_PUB } from '../../initializers/constants'
-import { VideoPrivacy } from '../../../shared/models/videos/video-privacy.enum'
-
-async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) {
-  const byAccount = videoChannel.Account
-
-  const videoChannelObject = videoChannel.toActivityPubObject()
-  const data = await createActivityData(videoChannel.url, byAccount, videoChannelObject)
-
-  return broadcastToFollowers(data, byAccount, [ byAccount ], t)
-}
-
-async function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) {
-  const byAccount = videoChannel.Account
-
-  const videoChannelObject = videoChannel.toActivityPubObject()
-  const data = await updateActivityData(videoChannel.url, byAccount, videoChannelObject)
-
-  const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id)
-  accountsInvolved.push(byAccount)
-
-  return broadcastToFollowers(data, byAccount, accountsInvolved, t)
-}
-
-async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) {
-  const byAccount = videoChannel.Account
-
-  const data = await deleteActivityData(videoChannel.url, byAccount)
-
-  const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id)
-  accountsInvolved.push(byAccount)
-
-  return broadcastToFollowers(data, byAccount, accountsInvolved, t)
-}
-
-async function sendAddVideo (video: VideoInstance, t: Transaction) {
-  const byAccount = video.VideoChannel.Account
-
-  const videoObject = video.toActivityPubObject()
-  const data = await addActivityData(video.url, byAccount, video, video.VideoChannel.url, videoObject)
-
-  return broadcastToFollowers(data, byAccount, [ byAccount ], t)
-}
-
-async function sendUpdateVideo (video: VideoInstance, t: Transaction) {
-  const byAccount = video.VideoChannel.Account
-
-  const videoObject = video.toActivityPubObject()
-  const data = await updateActivityData(video.url, byAccount, videoObject)
-
-  const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id)
-  accountsInvolved.push(byAccount)
-
-  return broadcastToFollowers(data, byAccount, accountsInvolved, t)
-}
-
-async function sendDeleteVideo (video: VideoInstance, t: Transaction) {
-  const byAccount = video.VideoChannel.Account
-
-  const data = await deleteActivityData(video.url, byAccount)
-
-  const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id)
-  accountsInvolved.push(byAccount)
-
-  return broadcastToFollowers(data, byAccount, accountsInvolved, t)
-}
-
-async function sendDeleteAccount (account: AccountInstance, t: Transaction) {
-  const data = await deleteActivityData(account.url, account)
-
-  return broadcastToFollowers(data, account, [ account ], t)
-}
-
-async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) {
-  const url = getActivityPubUrl('videoChannel', videoChannel.uuid) + '#announce'
-  const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject())
-
-  const data = await announceActivityData(url, byAccount, announcedActivity)
-  return broadcastToFollowers(data, byAccount, [ byAccount ], t)
-}
-
-async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
-  const url = getActivityPubUrl('video', video.uuid) + '#announce'
-
-  const videoChannel = video.VideoChannel
-  const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject())
-
-  const data = await announceActivityData(url, byAccount, announcedActivity)
-  return broadcastToFollowers(data, byAccount, [ byAccount ], t)
-}
-
-async function sendVideoAbuse (byAccount: AccountInstance, videoAbuse: VideoAbuseInstance, video: VideoInstance, t: Transaction) {
-  const url = getActivityPubUrl('videoAbuse', videoAbuse.id.toString())
-  const data = await createActivityData(url, byAccount, videoAbuse.toActivityPubObject())
-
-  return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
-}
-
-async function sendAccept (byAccount: AccountInstance, toAccount: AccountInstance, t: Transaction) {
-  const data = await acceptActivityData(byAccount)
-
-  return unicastTo(data, byAccount, toAccount.inboxUrl, t)
-}
-
-async function sendFollow (byAccount: AccountInstance, toAccount: AccountInstance, t: Transaction) {
-  const data = await followActivityData(toAccount.url, byAccount)
-
-  return unicastTo(data, byAccount, toAccount.inboxUrl, t)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  sendCreateVideoChannel,
-  sendUpdateVideoChannel,
-  sendDeleteVideoChannel,
-  sendAddVideo,
-  sendUpdateVideo,
-  sendDeleteVideo,
-  sendDeleteAccount,
-  sendAccept,
-  sendFollow,
-  sendVideoAbuse,
-  sendVideoChannelAnnounce,
-  sendVideoAnnounce
-}
-
-// ---------------------------------------------------------------------------
-
-async function broadcastToFollowers (data: any, byAccount: AccountInstance, toAccountFollowers: AccountInstance[], t: Transaction) {
-  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 jobPayload = {
-    uris: result.data,
-    signatureAccountId: byAccount.id,
-    body: data
-  }
-
-  return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload)
-}
-
-async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: string, t: Transaction) {
-  const jobPayload = {
-    uris: [ toAccountUrl ],
-    signatureAccountId: byAccount.id,
-    body: data
-  }
-
-  return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload)
-}
-
-async function getAudience (accountSender: AccountInstance, isPublic = true) {
-  const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls()
-
-  // Thanks Mastodon: https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/tag_manager.rb#L47
-  let to = []
-  let cc = []
-
-  if (isPublic) {
-    to = [ ACTIVITY_PUB.PUBLIC ]
-    cc = followerInboxUrls
-  } else { // Unlisted
-    to = followerInboxUrls
-    cc = [ ACTIVITY_PUB.PUBLIC ]
-  }
-
-  return { to, cc }
-}
-
-async function createActivityData (url: string, byAccount: AccountInstance, object: any) {
-  const { to, cc } = await getAudience(byAccount)
-  const activity: ActivityCreate = {
-    type: 'Create',
-    id: url,
-    actor: byAccount.url,
-    to,
-    cc,
-    object
-  }
-
-  return activity
-}
-
-async function updateActivityData (url: string, byAccount: AccountInstance, object: any) {
-  const { to, cc } = await getAudience(byAccount)
-  const activity: ActivityUpdate = {
-    type: 'Update',
-    id: url,
-    actor: byAccount.url,
-    to,
-    cc,
-    object
-  }
-
-  return activity
-}
-
-async function deleteActivityData (url: string, byAccount: AccountInstance) {
-  const activity: ActivityDelete = {
-    type: 'Delete',
-    id: url,
-    actor: byAccount.url
-  }
-
-  return activity
-}
-
-async function addActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, target: string, object: any) {
-  const videoPublic = video.privacy === VideoPrivacy.PUBLIC
-
-  const { to, cc } = await getAudience(byAccount, videoPublic)
-  const activity: ActivityAdd = {
-    type: 'Add',
-    id: url,
-    actor: byAccount.url,
-    to,
-    cc,
-    object,
-    target
-  }
-
-  return activity
-}
-
-async function announceActivityData (url: string, byAccount: AccountInstance, object: any) {
-  const activity = {
-    type: 'Announce',
-    id: url,
-    actor: byAccount.url,
-    object
-  }
-
-  return activity
-}
-
-async function followActivityData (url: string, byAccount: AccountInstance) {
-  const activity: ActivityFollow = {
-    type: 'Follow',
-    id: byAccount.url,
-    actor: byAccount.url,
-    object: url
-  }
-
-  return activity
-}
-
-async function acceptActivityData (byAccount: AccountInstance) {
-  const activity: ActivityAccept = {
-    type: 'Accept',
-    id: byAccount.url,
-    actor: byAccount.url
-  }
-
-  return activity
-}
diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts
new file mode 100644 (file)
index 0000000..5f15dd4
--- /dev/null
@@ -0,0 +1,7 @@
+export * from './send-accept'
+export * from './send-add'
+export * from './send-announce'
+export * from './send-create'
+export * from './send-delete'
+export * from './send-follow'
+export * from './send-update'
diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts
new file mode 100644 (file)
index 0000000..bea955b
--- /dev/null
@@ -0,0 +1,58 @@
+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'
+
+async function broadcastToFollowers (data: any, byAccount: AccountInstance, toAccountFollowers: AccountInstance[], t: Transaction) {
+  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 jobPayload = {
+    uris: result.data,
+    signatureAccountId: byAccount.id,
+    body: data
+  }
+
+  return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload)
+}
+
+async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: string, t: Transaction) {
+  const jobPayload = {
+    uris: [ toAccountUrl ],
+    signatureAccountId: byAccount.id,
+    body: data
+  }
+
+  return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload)
+}
+
+async function getAudience (accountSender: AccountInstance, isPublic = true) {
+  const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls()
+
+  // Thanks Mastodon: https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/tag_manager.rb#L47
+  let to = []
+  let cc = []
+
+  if (isPublic) {
+    to = [ ACTIVITY_PUB.PUBLIC ]
+    cc = followerInboxUrls
+  } else { // Unlisted
+    to = followerInboxUrls
+    cc = [ ACTIVITY_PUB.PUBLIC ]
+  }
+
+  return { to, cc }
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  broadcastToFollowers,
+  unicastTo,
+  getAudience
+}
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts
new file mode 100644 (file)
index 0000000..0324a30
--- /dev/null
@@ -0,0 +1,34 @@
+import { Transaction } from 'sequelize'
+import { ActivityAccept } from '../../../../shared/models/activitypub/activity'
+import { AccountInstance } from '../../../models'
+import { AccountFollowInstance } from '../../../models/account/account-follow-interface'
+import { unicastTo } from './misc'
+import { getAccountFollowAcceptActivityPubUrl } from '../../../helpers/activitypub'
+
+async function sendAccept (accountFollow: AccountFollowInstance, t: Transaction) {
+  const follower = accountFollow.AccountFollower
+  const me = accountFollow.AccountFollowing
+
+  const url = getAccountFollowAcceptActivityPubUrl(accountFollow)
+  const data = await acceptActivityData(url, me)
+
+  return unicastTo(data, me, follower.inboxUrl, t)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendAccept
+}
+
+// ---------------------------------------------------------------------------
+
+async function acceptActivityData (url: string, byAccount: AccountInstance) {
+  const activity: ActivityAccept = {
+    type: 'Accept',
+    id: url,
+    actor: byAccount.url
+  }
+
+  return activity
+}
diff --git a/server/lib/activitypub/send/send-add.ts b/server/lib/activitypub/send/send-add.ts
new file mode 100644 (file)
index 0000000..3012b75
--- /dev/null
@@ -0,0 +1,38 @@
+import { Transaction } from 'sequelize'
+import { ActivityAdd } from '../../../../shared/models/activitypub/activity'
+import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
+import { AccountInstance, VideoInstance } from '../../../models'
+import { broadcastToFollowers, getAudience } from './misc'
+
+async function sendAddVideo (video: VideoInstance, t: Transaction) {
+  const byAccount = video.VideoChannel.Account
+
+  const videoObject = video.toActivityPubObject()
+  const data = await addActivityData(video.url, byAccount, video, video.VideoChannel.url, videoObject)
+
+  return broadcastToFollowers(data, byAccount, [ byAccount ], t)
+}
+
+async function addActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, target: string, object: any) {
+  const videoPublic = video.privacy === VideoPrivacy.PUBLIC
+
+  const { to, cc } = await getAudience(byAccount, videoPublic)
+  const activity: ActivityAdd = {
+    type: 'Add',
+    id: url,
+    actor: byAccount.url,
+    to,
+    cc,
+    object,
+    target
+  }
+
+  return activity
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  addActivityData,
+  sendAddVideo
+}
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts
new file mode 100644 (file)
index 0000000..b9217e4
--- /dev/null
@@ -0,0 +1,45 @@
+import { Transaction } from 'sequelize'
+import { AccountInstance, VideoInstance } from '../../../models'
+import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
+import { broadcastToFollowers } from './misc'
+import { addActivityData } from './send-add'
+import { createActivityData } from './send-create'
+import { getAnnounceActivityPubUrl } from '../../../helpers/activitypub'
+
+async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const url = getAnnounceActivityPubUrl(video.url, byAccount)
+
+  const videoChannel = video.VideoChannel
+  const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject())
+
+  const data = await announceActivityData(url, byAccount, announcedActivity)
+  return broadcastToFollowers(data, byAccount, [ byAccount ], t)
+}
+
+async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) {
+  const url = getAnnounceActivityPubUrl(videoChannel.url, byAccount)
+  const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject())
+
+  const data = await announceActivityData(url, byAccount, announcedActivity)
+  return broadcastToFollowers(data, byAccount, [ byAccount ], t)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendVideoAnnounce,
+  sendVideoChannelAnnounce
+}
+
+// ---------------------------------------------------------------------------
+
+async function announceActivityData (url: string, byAccount: AccountInstance, object: any) {
+  const activity = {
+    type: 'Announce',
+    id: url,
+    actor: byAccount.url,
+    object
+  }
+
+  return activity
+}
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
new file mode 100644 (file)
index 0000000..66bfeee
--- /dev/null
@@ -0,0 +1,44 @@
+import { Transaction } from 'sequelize'
+import { ActivityCreate } from '../../../../shared/models/activitypub/activity'
+import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models'
+import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface'
+import { broadcastToFollowers, getAudience, unicastTo } from './misc'
+import { getVideoAbuseActivityPubUrl } from '../../../helpers/activitypub'
+
+async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) {
+  const byAccount = videoChannel.Account
+
+  const videoChannelObject = videoChannel.toActivityPubObject()
+  const data = await createActivityData(videoChannel.url, byAccount, videoChannelObject)
+
+  return broadcastToFollowers(data, byAccount, [ byAccount ], t)
+}
+
+async function sendVideoAbuse (byAccount: AccountInstance, videoAbuse: VideoAbuseInstance, video: VideoInstance, t: Transaction) {
+  const url = getVideoAbuseActivityPubUrl(videoAbuse)
+  const data = await createActivityData(url, byAccount, videoAbuse.toActivityPubObject())
+
+  return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
+}
+
+async function createActivityData (url: string, byAccount: AccountInstance, object: any) {
+  const { to, cc } = await getAudience(byAccount)
+  const activity: ActivityCreate = {
+    type: 'Create',
+    id: url,
+    actor: byAccount.url,
+    to,
+    cc,
+    object
+  }
+
+  return activity
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendCreateVideoChannel,
+  sendVideoAbuse,
+  createActivityData
+}
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts
new file mode 100644 (file)
index 0000000..5be0e2d
--- /dev/null
@@ -0,0 +1,53 @@
+import { Transaction } from 'sequelize'
+import { ActivityDelete } from '../../../../shared/models/activitypub/activity'
+import { database as db } from '../../../initializers'
+import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models'
+import { broadcastToFollowers } from './misc'
+
+async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) {
+  const byAccount = videoChannel.Account
+
+  const data = await deleteActivityData(videoChannel.url, byAccount)
+
+  const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id)
+  accountsInvolved.push(byAccount)
+
+  return broadcastToFollowers(data, byAccount, accountsInvolved, t)
+}
+
+async function sendDeleteVideo (video: VideoInstance, t: Transaction) {
+  const byAccount = video.VideoChannel.Account
+
+  const data = await deleteActivityData(video.url, byAccount)
+
+  const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id)
+  accountsInvolved.push(byAccount)
+
+  return broadcastToFollowers(data, byAccount, accountsInvolved, t)
+}
+
+async function sendDeleteAccount (account: AccountInstance, t: Transaction) {
+  const data = await deleteActivityData(account.url, account)
+
+  return broadcastToFollowers(data, account, [ account ], t)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendDeleteVideoChannel,
+  sendDeleteVideo,
+  sendDeleteAccount
+}
+
+// ---------------------------------------------------------------------------
+
+async function deleteActivityData (url: string, byAccount: AccountInstance) {
+  const activity: ActivityDelete = {
+    type: 'Delete',
+    id: url,
+    actor: byAccount.url
+  }
+
+  return activity
+}
diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts
new file mode 100644 (file)
index 0000000..48d641c
--- /dev/null
@@ -0,0 +1,34 @@
+import { Transaction } from 'sequelize'
+import { ActivityFollow } from '../../../../shared/models/activitypub/activity'
+import { AccountInstance } from '../../../models'
+import { AccountFollowInstance } from '../../../models/account/account-follow-interface'
+import { unicastTo } from './misc'
+import { getAccountFollowActivityPubUrl } from '../../../helpers/activitypub'
+
+async function sendFollow (accountFollow: AccountFollowInstance, t: Transaction) {
+  const me = accountFollow.AccountFollower
+  const following = accountFollow.AccountFollowing
+
+  const url = getAccountFollowActivityPubUrl(accountFollow)
+  const data = await followActivityData(url, me, following)
+
+  return unicastTo(data, me, following.inboxUrl, t)
+}
+
+async function followActivityData (url: string, byAccount: AccountInstance, targetAccount: AccountInstance) {
+  const activity: ActivityFollow = {
+    type: 'Follow',
+    id: url,
+    actor: byAccount.url,
+    object: targetAccount.url
+  }
+
+  return activity
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendFollow,
+  followActivityData
+}
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
new file mode 100644 (file)
index 0000000..39da824
--- /dev/null
@@ -0,0 +1,39 @@
+import { Transaction } from 'sequelize'
+import { ActivityFollow, ActivityUndo } from '../../../../shared/models/activitypub/activity'
+import { AccountInstance } from '../../../models'
+import { AccountFollowInstance } from '../../../models/account/account-follow-interface'
+import { unicastTo } from './misc'
+import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl } from '../../../helpers/activitypub'
+import { followActivityData } from './send-follow'
+
+async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transaction) {
+  const me = accountFollow.AccountFollower
+  const following = accountFollow.AccountFollowing
+
+  const followUrl = getAccountFollowActivityPubUrl(accountFollow)
+  const undoUrl = getUndoActivityPubUrl(followUrl)
+
+  const object = await followActivityData(followUrl, me, following)
+  const data = await undoActivityData(undoUrl, me, object)
+
+  return unicastTo(data, me, following.inboxUrl, t)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendUndoFollow
+}
+
+// ---------------------------------------------------------------------------
+
+async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow) {
+  const activity: ActivityUndo = {
+    type: 'Undo',
+    id: url,
+    actor: byAccount.url,
+    object
+  }
+
+  return activity
+}
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
new file mode 100644 (file)
index 0000000..42738f9
--- /dev/null
@@ -0,0 +1,55 @@
+import { Transaction } from 'sequelize'
+import { ActivityUpdate } from '../../../../shared/models/activitypub/activity'
+import { getUpdateActivityPubUrl } from '../../../helpers/activitypub'
+import { database as db } from '../../../initializers'
+import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models'
+import { broadcastToFollowers, getAudience } from './misc'
+
+async function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) {
+  const byAccount = videoChannel.Account
+
+  const url = getUpdateActivityPubUrl(videoChannel.url, videoChannel.updatedAt.toISOString())
+  const videoChannelObject = videoChannel.toActivityPubObject()
+  const data = await updateActivityData(url, byAccount, videoChannelObject)
+
+  const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id)
+  accountsInvolved.push(byAccount)
+
+  return broadcastToFollowers(data, byAccount, accountsInvolved, t)
+}
+
+async function sendUpdateVideo (video: VideoInstance, t: Transaction) {
+  const byAccount = video.VideoChannel.Account
+
+  const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
+  const videoObject = video.toActivityPubObject()
+  const data = await updateActivityData(url, byAccount, videoObject)
+
+  const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id)
+  accountsInvolved.push(byAccount)
+
+  return broadcastToFollowers(data, byAccount, accountsInvolved, t)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendUpdateVideoChannel,
+  sendUpdateVideo
+}
+
+// ---------------------------------------------------------------------------
+
+async function updateActivityData (url: string, byAccount: AccountInstance, object: any) {
+  const { to, cc } = await getAudience(byAccount)
+  const activity: ActivityUpdate = {
+    type: 'Update',
+    id: url,
+    actor: byAccount.url,
+    to,
+    cc,
+    object
+  }
+
+  return activity
+}
index 6443899d380cae912d3696fa0429ce0f5830987f..f26110973cfc771ace370a6b0bbbd54a131dfc48 100644 (file)
@@ -3,10 +3,11 @@ import { computeResolutionsToTranscode, logger } from '../../../helpers'
 
 import { database as db } from '../../../initializers/database'
 import { VideoInstance } from '../../../models'
-import { sendAddVideo } from '../../activitypub/send-request'
+
 import { JobScheduler } from '../job-scheduler'
 import { TranscodingJobPayload } from './transcoding-job-scheduler'
 import { shareVideoByServer } from '../../../helpers/activitypub'
+import { sendAddVideo } from '../../activitypub/send/send-add'
 
 async function process (data: TranscodingJobPayload, jobId: number) {
   const video = await db.Video.loadByUUIDAndPopulateAccountAndServerAndTags(data.videoUUID)
index 4f2ce3d24015647e69447a8969d5bd4aaf520ced..867580200b594cfb4aba7b3d7bf9b43a94b776c0 100644 (file)
@@ -2,7 +2,7 @@ import { VideoResolution } from '../../../../shared'
 import { logger } from '../../../helpers'
 import { database as db } from '../../../initializers/database'
 import { VideoInstance } from '../../../models'
-import { sendUpdateVideo } from '../../activitypub/send-request'
+import { sendUpdateVideo } from '../../activitypub/send/send-update'
 
 async function process (data: { videoUUID: string, resolution: VideoResolution }, jobId: number) {
   const video = await db.Video.loadByUUIDAndPopulateAccountAndServerAndTags(data.videoUUID)
index 2d7b36b4faae0e6dc380267ff78664db991df5c0..d54ffc916d34bee45960655a06ddde8370229361 100644 (file)
@@ -1,11 +1,11 @@
 import * as Sequelize from 'sequelize'
-import { getActivityPubUrl } from '../helpers/activitypub'
 import { createPrivateAndPublicKeys } from '../helpers/peertube-crypto'
 import { database as db } from '../initializers'
 import { CONFIG } from '../initializers/constants'
 import { UserInstance } from '../models'
 import { createVideoChannel } from './video-channel'
 import { logger } from '../helpers/logger'
+import { getAccountActivityPubUrl } from '../helpers/activitypub'
 
 async function createUserAccountAndChannel (user: UserInstance, validateUser = true) {
   const { account, videoChannel } = await db.sequelize.transaction(async t => {
@@ -36,7 +36,7 @@ async function createUserAccountAndChannel (user: UserInstance, validateUser = t
 }
 
 async function createLocalAccountWithoutKeys (name: string, userId: number, applicationId: number, t: Sequelize.Transaction) {
-  const url = getActivityPubUrl('account', name)
+  const url = getAccountActivityPubUrl(name)
 
   const accountInstance = db.Account.build({
     name,
index 5bb1814ea902bd19fe067be97dcc27284fd97413..5235d9cb56d2c56036a971e60c3d99ca5ae95124 100644 (file)
@@ -1,9 +1,9 @@
 import * as Sequelize from 'sequelize'
 import { VideoChannelCreate } from '../../shared/models'
 import { logger } from '../helpers'
-import { getActivityPubUrl } from '../helpers/activitypub'
 import { database as db } from '../initializers'
 import { AccountInstance } from '../models'
+import { getVideoChannelActivityPubUrl } from '../helpers/activitypub'
 
 async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountInstance, t: Sequelize.Transaction) {
   const videoChannelData = {
@@ -14,7 +14,7 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account
   }
 
   const videoChannel = db.VideoChannel.build(videoChannelData)
-  videoChannel.set('url', getActivityPubUrl('videoChannel', videoChannel.uuid))
+  videoChannel.set('url', getVideoChannelActivityPubUrl(videoChannel))
 
   const options = { transaction: t }
 
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
new file mode 100644 (file)
index 0000000..e223497
--- /dev/null
@@ -0,0 +1,62 @@
+import * as express from 'express'
+import { body } from 'express-validator/check'
+import { isTestInstance } from '../../helpers/core-utils'
+import { isAccountIdValid } from '../../helpers/custom-validators/activitypub/account'
+import { isEachUniqueHostValid } from '../../helpers/custom-validators/servers'
+import { logger } from '../../helpers/logger'
+import { CONFIG, database as db } from '../../initializers'
+import { checkErrors } from './utils'
+import { getServerAccount } from '../../helpers/utils'
+
+const followValidator = [
+  body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    // Force https if the administrator wants to make friends
+    if (isTestInstance() === false && CONFIG.WEBSERVER.SCHEME === 'http') {
+      return res.status(400)
+        .json({
+          error: 'Cannot follow non HTTPS web server.'
+        })
+        .end()
+    }
+
+    logger.debug('Checking follow parameters', { parameters: req.body })
+
+    checkErrors(req, res, next)
+  }
+]
+
+const removeFollowingValidator = [
+  body('accountId').custom(isAccountIdValid).withMessage('Should have a valid account id'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking follow parameters', { parameters: req.body })
+
+    checkErrors(req, res, async () => {
+      try {
+        const serverAccount = await getServerAccount()
+        const following = await db.AccountFollow.loadByAccountAndTarget(serverAccount.id, req.params.accountId)
+
+        if (!following) {
+          return res.status(404)
+            .end()
+        }
+
+        res.locals.following = following
+
+        return next()
+      } catch (err) {
+        logger.error('Error in remove following validator.', err)
+        return res.sendStatus(500)
+      }
+    })
+  }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+  followValidator,
+  removeFollowingValidator
+}
index 3f5afe5b3c28eeb0cd398db74c2c5eac902590b5..9840e8f653a24f21fc0316e1915811db8c8d81b8 100644 (file)
@@ -2,7 +2,7 @@ export * from './account'
 export * from './oembed'
 export * from './activitypub'
 export * from './pagination'
-export * from './servers'
+export * from './follows'
 export * from './sort'
 export * from './users'
 export * from './videos'
diff --git a/server/middlewares/validators/servers.ts b/server/middlewares/validators/servers.ts
deleted file mode 100644 (file)
index 95b69b7..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-import * as express from 'express'
-import { body } from 'express-validator/check'
-import { isEachUniqueHostValid } from '../../helpers/custom-validators/servers'
-import { isTestInstance } from '../../helpers/core-utils'
-import { CONFIG } from '../../initializers/constants'
-import { logger } from '../../helpers/logger'
-import { checkErrors } from './utils'
-
-const followValidator = [
-  body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
-
-  (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    // Force https if the administrator wants to make friends
-    if (isTestInstance() === false && CONFIG.WEBSERVER.SCHEME === 'http') {
-      return res.status(400)
-        .json({
-          error: 'Cannot follow non HTTPS web server.'
-        })
-        .end()
-    }
-
-    logger.debug('Checking follow parameters', { parameters: req.body })
-
-    checkErrors(req, res, next)
-  }
-]
-
-// ---------------------------------------------------------------------------
-
-export {
-  followValidator
-}
index cc9b7c42b873604234c1e248d31627c4b1ff05ec..f00c7dcd92c22e4e06910b01d7685bb039e115f3 100644 (file)
@@ -78,7 +78,19 @@ loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
     where: {
       accountId,
       targetAccountId
-    }
+    },
+    include: [
+      {
+        model: AccountFollow[ 'sequelize' ].models.Account,
+        required: true,
+        as: 'AccountFollower'
+      },
+      {
+        model: AccountFollow['sequelize'].models.Account,
+        required: true,
+        as: 'AccountFollowing'
+      }
+    ]
   }
 
   return AccountFollow.findOne(query)
index 1a567fb7a8802cd1242bef0985ab5e24c8d9c638..e30260f761e656039b34839c192e76c3fc8f239f 100644 (file)
@@ -37,7 +37,7 @@ export interface AccountClass {
 
 export interface AccountAttributes {
   name: string
-  url: string
+  url?: string
   publicKey: string
   privateKey: string
   followersCount: number
index faf5fa841f5ca3c645cea15e37d736f458329d8d..9a29215018ef5122e8040d42a9271651564ffc88 100644 (file)
@@ -1,5 +1,4 @@
 import * as Sequelize from 'sequelize'
-
 import {
   activityPubContextify,
   isAccountFollowersCountValid,
@@ -15,7 +14,7 @@ import {
   isUserUsernameValid
 } from '../../helpers'
 import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants'
-import { sendDeleteAccount } from '../../lib/activitypub/send-request'
+import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete'
 
 import { addMethodsToModel } from '../utils'
 import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface'
index f8414d4a870758cfd20fdef109aff6a8af942dc0..93566a5c642b112bf62a1fed0d7f0de331b0b82e 100644 (file)
@@ -1,17 +1,11 @@
 import * as Sequelize from 'sequelize'
-
-import { isVideoChannelNameValid, isVideoChannelDescriptionValid } from '../../helpers'
-
-import { addMethodsToModel, getSort } from '../utils'
-import {
-  VideoChannelInstance,
-  VideoChannelAttributes,
-
-  VideoChannelMethods
-} from './video-channel-interface'
-import { sendDeleteVideoChannel } from '../../lib/activitypub/send-request'
+import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers'
 import { isVideoChannelUrlValid } from '../../helpers/custom-validators/video-channels'
 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
+import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete'
+
+import { addMethodsToModel, getSort } from '../utils'
+import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface'
 
 let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
 let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
index dc10aca1a7a5ab00796fe49d474742e1f8a4ca89..e2069eb0c6d8110280433e4ba978616f627649a2 100644 (file)
@@ -9,7 +9,6 @@ import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/v
 import {
   createTorrentPromise,
   generateImageFromVideoFile,
-  getActivityPubUrl,
   getVideoFileHeight,
   isVideoCategoryValid,
   isVideoDescriptionValid,
@@ -40,13 +39,13 @@ import {
   VIDEO_LICENCES,
   VIDEO_PRIVACIES
 } from '../../initializers'
-import { sendDeleteVideo } from '../../lib/activitypub/send-request'
 
 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 { sendDeleteVideo } from '../../lib/index'
 
 const Buffer = safeBuffer.Buffer
 
@@ -584,7 +583,7 @@ toActivityPubObject = function (this: VideoInstance) {
 
   const videoObject: VideoTorrentObject = {
     type: 'Video' as 'Video',
-    id: getActivityPubUrl('video', this.uuid),
+    id: this.url,
     name: this.name,
     // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
     duration: 'PT' + this.duration + 'S',
@@ -615,7 +614,7 @@ toActivityPubObject = function (this: VideoInstance) {
       width: THUMBNAILS_SIZE.width,
       height: THUMBNAILS_SIZE.height
     },
-    url
+    url // FIXME: needed?
   }
 
   return videoObject
index b6a57ab6d46269979ffd42db24ef3534476ea616..cdbd24f5641e95ca7ccee21e95f79ab83713196e 100644 (file)
@@ -316,7 +316,7 @@ describe('Test multiple servers', function () {
         expect(video1.serverHost).to.equal('localhost:9003')
         expect(video1.duration).to.equal(5)
         expect(video1.tags).to.deep.equal([ 'tag1p3' ])
-        expect(video1.author).to.equal('root')
+        expect(video1.account).to.equal('root')
         expect(dateIsValid(video1.createdAt)).to.be.true
         expect(dateIsValid(video1.updatedAt)).to.be.true
 
@@ -342,7 +342,7 @@ describe('Test multiple servers', function () {
         expect(video2.serverHost).to.equal('localhost:9003')
         expect(video2.duration).to.equal(5)
         expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ])
-        expect(video2.author).to.equal('root')
+        expect(video2.account).to.equal('root')
         expect(dateIsValid(video2.createdAt)).to.be.true
         expect(dateIsValid(video2.updatedAt)).to.be.true
 
@@ -690,7 +690,7 @@ describe('Test multiple servers', function () {
         expect(baseVideo.licence).to.equal(video.licence)
         expect(baseVideo.category).to.equal(video.category)
         expect(baseVideo.nsfw).to.equal(video.nsfw)
-        expect(baseVideo.author).to.equal(video.account)
+        expect(baseVideo.account).to.equal(video.account)
         expect(baseVideo.tags).to.deep.equal(video.tags)
       }
     })
index 255cdd43c1f1c2d091f851ebb469b208ae8e132e..3d035d7d7b4291d15e5d1c09c2421adce19e133d 100644 (file)
@@ -3,10 +3,10 @@ import { ActivityPubSignature } from './activitypub-signature'
 import { VideoAbuseObject } from './objects/video-abuse-object'
 
 export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate |
-  ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce
+  ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce |
+  ActivityUndo
 
-// Flag -> report abuse
-export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce'
+export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo'
 
 export interface BaseActivity {
   '@context'?: any[]
@@ -51,3 +51,8 @@ export interface ActivityAnnounce extends BaseActivity {
   type: 'Announce'
   object: ActivityCreate | ActivityAdd
 }
+
+export interface ActivityUndo extends BaseActivity {
+  type: 'Undo',
+  object: ActivityFollow
+}