Add shares forward and collection on videos/video channels
authorChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 13:44:51 +0000 (14:44 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 18:40:53 +0000 (19:40 +0100)
29 files changed:
server/controllers/activitypub/client.ts
server/helpers/custom-validators/accounts.ts
server/helpers/custom-validators/video-channels.ts
server/helpers/custom-validators/videos.ts
server/lib/activitypub/process/misc.ts
server/lib/activitypub/process/process-add.ts
server/lib/activitypub/process/process-announce.ts
server/lib/activitypub/process/process-create.ts
server/lib/activitypub/send/misc.ts
server/lib/activitypub/send/send-announce.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/send-like.ts
server/lib/activitypub/send/send-undo.ts
server/lib/activitypub/share.ts
server/lib/activitypub/url.ts
server/middlewares/async.ts
server/middlewares/validators/account.ts
server/middlewares/validators/utils.ts
server/middlewares/validators/video-channels.ts
server/middlewares/validators/videos.ts
server/models/video/video-channel-interface.ts
server/models/video/video-channel-share-interface.ts
server/models/video/video-channel-share.ts
server/models/video/video-channel.ts
server/models/video/video-share-interface.ts
server/models/video/video-share.ts
server/models/video/video.ts
shared/models/activitypub/objects/video-channel-object.ts
shared/models/activitypub/objects/video-torrent-object.ts

index eee89e2fd4f2cdd7e7304d7cfdddd66649fa05c3..41272bca08ec070fcbe99065873070b7096d7e02 100644 (file)
@@ -1,22 +1,26 @@
 // Intercept ActivityPub client requests
 import * as express from 'express'
-
-import { database as db } from '../../initializers'
-import { executeIfActivityPub, localAccountValidator } from '../../middlewares'
 import { pageToStartAndCount } from '../../helpers'
-import { AccountInstance, VideoChannelInstance } from '../../models'
 import { activityPubCollectionPagination } from '../../helpers/activitypub'
+
+import { database as db } from '../../initializers'
 import { ACTIVITY_PUB, CONFIG } from '../../initializers/constants'
+import { buildVideoChannelAnnounceToFollowers } from '../../lib/activitypub/send/send-announce'
+import { buildVideoAnnounceToFollowers } from '../../lib/index'
+import { executeIfActivityPub, localAccountValidator } from '../../middlewares'
 import { asyncMiddleware } from '../../middlewares/async'
-import { videosGetValidator } from '../../middlewares/validators/videos'
+import { videoChannelsGetValidator, videoChannelsShareValidator } from '../../middlewares/validators/video-channels'
+import { videosGetValidator, videosShareValidator } from '../../middlewares/validators/videos'
+import { AccountInstance, VideoChannelInstance } from '../../models'
+import { VideoChannelShareInstance } from '../../models/video/video-channel-share-interface'
 import { VideoInstance } from '../../models/video/video-interface'
-import { videoChannelsGetValidator } from '../../middlewares/validators/video-channels'
+import { VideoShareInstance } from '../../models/video/video-share-interface'
 
 const activityPubClientRouter = express.Router()
 
 activityPubClientRouter.get('/account/:name',
   executeIfActivityPub(localAccountValidator),
-  executeIfActivityPub(asyncMiddleware(accountController))
+  executeIfActivityPub(accountController)
 )
 
 activityPubClientRouter.get('/account/:name/followers',
@@ -31,7 +35,12 @@ activityPubClientRouter.get('/account/:name/following',
 
 activityPubClientRouter.get('/videos/watch/:id',
   executeIfActivityPub(videosGetValidator),
-  executeIfActivityPub(asyncMiddleware(videoController))
+  executeIfActivityPub(videoController)
+)
+
+activityPubClientRouter.get('/videos/watch/:id/announces/:accountId',
+  executeIfActivityPub(asyncMiddleware(videosShareValidator)),
+  executeIfActivityPub(asyncMiddleware(videoAnnounceController))
 )
 
 activityPubClientRouter.get('/video-channels/:id',
@@ -39,6 +48,11 @@ activityPubClientRouter.get('/video-channels/:id',
   executeIfActivityPub(asyncMiddleware(videoChannelController))
 )
 
+activityPubClientRouter.get('/video-channels/:id/announces/:accountId',
+  executeIfActivityPub(asyncMiddleware(videoChannelsShareValidator)),
+  executeIfActivityPub(asyncMiddleware(videoChannelAnnounceController))
+)
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -47,7 +61,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function accountController (req: express.Request, res: express.Response, next: express.NextFunction) {
+function accountController (req: express.Request, res: express.Response, next: express.NextFunction) {
   const account: AccountInstance = res.locals.account
 
   return res.json(account.toActivityPubObject()).end()
@@ -77,12 +91,26 @@ async function accountFollowingController (req: express.Request, res: express.Re
   return res.json(activityPubResult)
 }
 
-async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
+function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
   const video: VideoInstance = res.locals.video
 
   return res.json(video.toActivityPubObject())
 }
 
+async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const share = res.locals.videoShare as VideoShareInstance
+  const object = await buildVideoAnnounceToFollowers(share.Account, res.locals.video, undefined)
+
+  return res.json(object)
+}
+
+async function videoChannelAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const share = res.locals.videoChannelShare as VideoChannelShareInstance
+  const object = await buildVideoChannelAnnounceToFollowers(share.Account, share.VideoChannel, undefined)
+
+  return res.json(object)
+}
+
 async function videoChannelController (req: express.Request, res: express.Response, next: express.NextFunction) {
   const videoChannel: VideoChannelInstance = res.locals.videoChannel
 
index fe0fc650ae89109823da480cee539e502b67eb12..a6d7f2b821a0ca773d00378d5df6fc1e587080dc 100644 (file)
@@ -1,4 +1,4 @@
-import * as Promise from 'bluebird'
+import * as Bluebird from 'bluebird'
 import * as express from 'express'
 import 'express-validator'
 import * as validator from 'validator'
@@ -11,33 +11,45 @@ function isAccountNameValid (value: string) {
   return isUserUsernameValid(value)
 }
 
-function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) {
-  let promise: Promise<AccountInstance>
-  if (validator.isInt(id)) {
+function checkAccountIdExists (id: number | string, res: express.Response, callback: (err: Error, account: AccountInstance) => any) {
+  let promise: Bluebird<AccountInstance>
+
+  if (validator.isInt('' + id)) {
     promise = db.Account.load(+id)
   } else { // UUID
-    promise = db.Account.loadByUUID(id)
+    promise = db.Account.loadByUUID('' + id)
   }
 
-  promise.then(account => {
+  return checkAccountExists(promise, res, callback)
+}
+
+function checkLocalAccountNameExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => any) {
+  const p = db.Account.loadLocalByName(name)
+
+  return checkAccountExists(p, res, callback)
+}
+
+function checkAccountExists (p: Bluebird<AccountInstance>, res: express.Response, callback: (err: Error, account: AccountInstance) => any) {
+  p.then(account => {
     if (!account) {
       return res.status(404)
-        .json({ error: 'Video account not found' })
+        .send({ error: 'Account not found' })
         .end()
     }
 
     res.locals.account = account
-    callback()
-  })
-  .catch(err => {
-    logger.error('Error in video account request validator.', err)
-    return res.sendStatus(500)
+    return callback(null, account)
   })
+    .catch(err => {
+      logger.error('Error in account request validator.', err)
+      return res.sendStatus(500)
+    })
 }
 
 // ---------------------------------------------------------------------------
 
 export {
-  checkVideoAccountExists,
+  checkAccountIdExists,
+  checkLocalAccountNameExists,
   isAccountNameValid
 }
index 5de01f74b4ddbf34fd6cbc0b81b9726930e83a9b..267d987fc658f22c10b9378b077860eb1dabf01a 100644 (file)
@@ -1,14 +1,14 @@
-import * as Promise from 'bluebird'
-import * as validator from 'validator'
+import * as Bluebird from 'bluebird'
 import * as express from 'express'
 import 'express-validator'
 import 'multer'
+import * as validator from 'validator'
 
-import { database as db, CONSTRAINTS_FIELDS } from '../../initializers'
+import { CONSTRAINTS_FIELDS, database as db } from '../../initializers'
 import { VideoChannelInstance } from '../../models'
 import { logger } from '../logger'
-import { exists } from './misc'
 import { isActivityPubUrlValid } from './index'
+import { exists } from './misc'
 
 const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS
 
@@ -25,7 +25,7 @@ function isVideoChannelNameValid (value: string) {
 }
 
 function checkVideoChannelExists (id: string, res: express.Response, callback: () => void) {
-  let promise: Promise<VideoChannelInstance>
+  let promise: Bluebird<VideoChannelInstance>
   if (validator.isInt(id)) {
     promise = db.VideoChannel.loadAndPopulateAccount(+id)
   } else { // UUID
@@ -48,11 +48,32 @@ function checkVideoChannelExists (id: string, res: express.Response, callback: (
     })
 }
 
+async function isVideoChannelExistsPromise (id: string, res: express.Response) {
+  let videoChannel: VideoChannelInstance
+  if (validator.isInt(id)) {
+    videoChannel = await db.VideoChannel.loadAndPopulateAccount(+id)
+  } else { // UUID
+    videoChannel = await db.VideoChannel.loadByUUIDAndPopulateAccount(id)
+  }
+
+  if (!videoChannel) {
+    res.status(404)
+      .json({ error: 'Video channel not found' })
+      .end()
+
+    return false
+  }
+
+  res.locals.videoChannel = videoChannel
+  return true
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   isVideoChannelDescriptionValid,
-  isVideoChannelNameValid,
   checkVideoChannelExists,
+  isVideoChannelNameValid,
+  isVideoChannelExistsPromise,
   isVideoChannelUrlValid
 }
index 205d8c62f56e68fa44000199a362df325f2c5c86..27635462608e6db2d1927e67b7c713fa38671f14 100644 (file)
@@ -130,6 +130,27 @@ function checkVideoExists (id: string, res: Response, callback: () => void) {
     })
 }
 
+async function isVideoExistsPromise (id: string, res: Response) {
+  let video: VideoInstance
+
+  if (validator.isInt(id)) {
+    video = await db.Video.loadAndPopulateAccountAndServerAndTags(+id)
+  } else { // UUID
+    video = await db.Video.loadByUUIDAndPopulateAccountAndServerAndTags(id)
+  }
+
+  if (!video) {
+    res.status(404)
+      .json({ error: 'Video not found' })
+      .end()
+
+    return false
+  }
+
+  res.locals.video = video
+  return true
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -152,5 +173,6 @@ export {
   isVideoPrivacyValid,
   isVideoFileResolutionValid,
   isVideoFileSizeValid,
-  checkVideoExists
+  checkVideoExists,
+  isVideoExistsPromise
 }
index eefbe28842c429dcea971c79feea0914f973487d..f20e588ab6460f147984a717c326e0de119bcdd7 100644 (file)
@@ -1,13 +1,16 @@
 import * as magnetUtil from 'magnet-uri'
 import { VideoTorrentObject } from '../../../../shared'
 import { VideoChannelObject } from '../../../../shared/models/activitypub/objects/video-channel-object'
+import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
 import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos'
+import { doRequest } from '../../../helpers/requests'
+import { database as db } from '../../../initializers'
 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'
+import { getOrCreateAccountAndServer } from '../account'
 
 function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) {
   return {
@@ -97,10 +100,60 @@ function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoO
   return attributes
 }
 
+async function addVideoShares (instance: VideoInstance, shares: string[]) {
+  for (const share of shares) {
+    // Fetch url
+    const json = await doRequest({
+      uri: share,
+      json: true
+    })
+    const actor = json['actor']
+    if (!actor) continue
+
+    const account = await getOrCreateAccountAndServer(actor)
+
+    const entry = {
+      accountId: account.id,
+      videoId: instance.id
+    }
+
+    await db.VideoShare.findOrCreate({
+      where: entry,
+      defaults: entry
+    })
+  }
+}
+
+async function addVideoChannelShares (instance: VideoChannelInstance, shares: string[]) {
+  for (const share of shares) {
+    // Fetch url
+    const json = await doRequest({
+      uri: share,
+      json: true
+    })
+    const actor = json['actor']
+    if (!actor) continue
+
+    const account = await getOrCreateAccountAndServer(actor)
+
+    const entry = {
+      accountId: account.id,
+      videoChannelId: instance.id
+    }
+
+    await db.VideoChannelShare.findOrCreate({
+      where: entry,
+      defaults: entry
+    })
+  }
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   videoFileActivityUrlToDBAttributes,
   videoActivityObjectToDBAttributes,
-  videoChannelActivityObjectToDBAttributes
+  videoChannelActivityObjectToDBAttributes,
+  addVideoChannelShares,
+  addVideoShares
 }
index 98280b9f05fe490a9bcb1abdc1dd5c8cbc853ed0..e6bf63eb245c68fa2b52fffe29fa314012e738e7 100644 (file)
@@ -11,7 +11,7 @@ import { VideoInstance } from '../../../models/video/video-interface'
 import { getOrCreateAccountAndServer } from '../account'
 import { getOrCreateVideoChannel } from '../video-channels'
 import { generateThumbnailFromUrl } from '../videos'
-import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
+import { addVideoShares, videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
 
 async function processAddActivity (activity: ActivityAdd) {
   const activityObject = activity.object
@@ -37,12 +37,10 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processAddVideo (
-  account: AccountInstance,
-  activity: ActivityAdd,
-  videoChannel: VideoChannelInstance,
-  videoToCreateData: VideoTorrentObject
-) {
+async function processAddVideo (account: AccountInstance,
+                                activity: ActivityAdd,
+                                videoChannel: VideoChannelInstance,
+                                videoToCreateData: VideoTorrentObject) {
   const options = {
     arguments: [ account, activity, videoChannel, videoToCreateData ],
     errorMessage: 'Cannot insert the remote video with many retries.'
@@ -59,6 +57,10 @@ async function processAddVideo (
     await createRates(videoToCreateData.dislikes.orderedItems, video, 'dislike')
   }
 
+  if (videoToCreateData.shares && Array.isArray(videoToCreateData.shares.orderedItems)) {
+    await addVideoShares(video, videoToCreateData.shares.orderedItems)
+  }
+
   return video
 }
 
index d8532d3a1ee61a446779b08a064e497f3100e51e..2aa9ad5c745ed7892094fea53aaabddb6a5b7e69 100644 (file)
@@ -1,34 +1,23 @@
-import { ActivityAnnounce } from '../../../../shared/models/activitypub/activity'
+import { ActivityAdd, ActivityAnnounce, ActivityCreate } from '../../../../shared/models/activitypub/activity'
+import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
 import { database as db } from '../../../initializers/index'
+import { AccountInstance } from '../../../models/account/account-interface'
 import { VideoInstance } from '../../../models/index'
 import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
+import { getOrCreateAccountAndServer } from '../account'
+import { forwardActivity } from '../send/misc'
 import { processAddActivity } from './process-add'
 import { processCreateActivity } from './process-create'
-import { getOrCreateAccountAndServer } from '../account'
 
 async function processAnnounceActivity (activity: ActivityAnnounce) {
   const announcedActivity = activity.object
   const accountAnnouncer = await getOrCreateAccountAndServer(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
+    return processVideoChannelShare(accountAnnouncer, activity)
   } 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
+    return processVideoShare(accountAnnouncer, activity)
   }
 
   logger.warn(
@@ -44,3 +33,78 @@ async function processAnnounceActivity (activity: ActivityAnnounce) {
 export {
   processAnnounceActivity
 }
+
+// ---------------------------------------------------------------------------
+
+function processVideoChannelShare (accountAnnouncer: AccountInstance, activity: ActivityAnnounce) {
+  const options = {
+    arguments: [ accountAnnouncer, activity ],
+    errorMessage: 'Cannot share the video channel with many retries.'
+  }
+
+  return retryTransactionWrapper(shareVideoChannel, options)
+}
+
+async function shareVideoChannel (accountAnnouncer: AccountInstance, activity: ActivityAnnounce) {
+  const announcedActivity = activity.object as ActivityCreate
+
+  return db.sequelize.transaction(async t => {
+    // Add share entry
+    const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity)
+    const share = {
+      accountId: accountAnnouncer.id,
+      videoChannelId: videoChannel.id
+    }
+
+    const [ , created ] = await db.VideoChannelShare.findOrCreate({
+      where: share,
+      defaults: share,
+      transaction: t
+    })
+
+    if (videoChannel.isOwned() && created === true) {
+      // Don't resend the activity to the sender
+      const exceptions = [ accountAnnouncer ]
+      await forwardActivity(activity, t, exceptions)
+    }
+
+    return undefined
+  })
+}
+
+function processVideoShare (accountAnnouncer: AccountInstance, activity: ActivityAnnounce) {
+  const options = {
+    arguments: [ accountAnnouncer, activity ],
+    errorMessage: 'Cannot share the video with many retries.'
+  }
+
+  return retryTransactionWrapper(shareVideo, options)
+}
+
+function shareVideo (accountAnnouncer: AccountInstance, activity: ActivityAnnounce) {
+  const announcedActivity = activity.object as ActivityAdd
+
+  return db.sequelize.transaction(async t => {
+    // Add share entry
+    const video: VideoInstance = await processAddActivity(announcedActivity)
+
+    const share = {
+      accountId: accountAnnouncer.id,
+      videoId: video.id
+    }
+
+    const [ , created ] = await db.VideoShare.findOrCreate({
+      where: share,
+      defaults: share,
+      transaction: t
+    })
+
+    if (video.isOwned() && created === true) {
+      // Don't resend the activity to the sender
+      const exceptions = [ accountAnnouncer ]
+      await forwardActivity(activity, t, exceptions)
+    }
+
+    return undefined
+  })
+}
index 1f982598bca7af981de7edd9c643e71bfdecbee7..c88082bbf537cd0d174d681e048b32a26b451f43 100644 (file)
@@ -8,7 +8,7 @@ import { AccountInstance } from '../../../models/account/account-interface'
 import { getOrCreateAccountAndServer } from '../account'
 import { forwardActivity } from '../send/misc'
 import { getVideoChannelActivityPubUrl } from '../url'
-import { videoChannelActivityObjectToDBAttributes } from './misc'
+import { addVideoChannelShares, videoChannelActivityObjectToDBAttributes } from './misc'
 
 async function processCreateActivity (activity: ActivityCreate) {
   const activityObject = activity.object
@@ -92,13 +92,19 @@ async function processCreateView (byAccount: AccountInstance, activity: Activity
   }
 }
 
-function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
+async 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)
+  const videoChannel = await retryTransactionWrapper(addRemoteVideoChannel, options)
+
+  if (videoChannelToCreateData.shares && Array.isArray(videoChannelToCreateData.shares.orderedItems)) {
+    await addVideoChannelShares(videoChannel, videoChannelToCreateData.shares.orderedItems)
+  }
+
+  return videoChannel
 }
 
 function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
index 444c1cbd676e8958a244d68715720fe29f48e06c..fd1add68eaceedb0efde9f4c7303af8e7b7ff6c9 100644 (file)
@@ -1,13 +1,14 @@
 import { Transaction } from 'sequelize'
+import { Activity } from '../../../../shared/models/activitypub/activity'
 import { logger } from '../../../helpers/logger'
 import { ACTIVITY_PUB, database as db } from '../../../initializers'
 import { AccountInstance } from '../../../models/account/account-interface'
+import { VideoChannelInstance } from '../../../models/index'
+import { VideoInstance } from '../../../models/video/video-interface'
 import {
   activitypubHttpJobScheduler,
   ActivityPubHttpPayload
 } from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler'
-import { VideoInstance } from '../../../models/video/video-interface'
-import { Activity } from '../../../../shared/models/activitypub/activity'
 
 async function forwardActivity (
   activity: Activity,
@@ -85,9 +86,16 @@ function getOriginVideoAudience (video: VideoInstance, accountsInvolvedInVideo:
   }
 }
 
-function getVideoFollowersAudience (accountsInvolvedInVideo: AccountInstance[]) {
+function getOriginVideoChannelAudience (videoChannel: VideoChannelInstance, accountsInvolved: AccountInstance[]) {
+  return {
+    to: [ videoChannel.Account.url ],
+    cc: accountsInvolved.map(a => a.followersUrl)
+  }
+}
+
+function getObjectFollowersAudience (accountsInvolvedInObject: AccountInstance[]) {
   return {
-    to: accountsInvolvedInVideo.map(a => a.followersUrl),
+    to: accountsInvolvedInObject.map(a => a.followersUrl),
     cc: []
   }
 }
@@ -99,6 +107,13 @@ async function getAccountsInvolvedInVideo (video: VideoInstance) {
   return accountsToForwardView
 }
 
+async function getAccountsInvolvedInVideoChannel (videoChannel: VideoChannelInstance) {
+  const accountsToForwardView = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id)
+  accountsToForwardView.push(videoChannel.Account)
+
+  return accountsToForwardView
+}
+
 async function getAudience (accountSender: AccountInstance, isPublic = true) {
   const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls()
 
@@ -131,10 +146,12 @@ async function computeFollowerUris (toAccountFollower: AccountInstance[], follow
 
 export {
   broadcastToFollowers,
+  getOriginVideoChannelAudience,
   unicastTo,
   getAudience,
   getOriginVideoAudience,
   getAccountsInvolvedInVideo,
-  getVideoFollowersAudience,
+  getAccountsInvolvedInVideoChannel,
+  getObjectFollowersAudience,
   forwardActivity
 }
index b8ea51bc08c81189946698eb88148e171d74b948..efc23af46dd80590c5f354dcd06eb669989d3d96 100644 (file)
@@ -1,34 +1,96 @@
 import { Transaction } from 'sequelize'
 import { ActivityAdd } from '../../../../shared/index'
-import { ActivityAnnounce, ActivityCreate } from '../../../../shared/models/activitypub/activity'
+import { ActivityAnnounce, ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub/activity'
 import { AccountInstance, VideoInstance } from '../../../models'
 import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
 import { getAnnounceActivityPubUrl } from '../url'
-import { broadcastToFollowers } from './misc'
+import {
+  broadcastToFollowers,
+  getAccountsInvolvedInVideo,
+  getAccountsInvolvedInVideoChannel,
+  getAudience,
+  getObjectFollowersAudience,
+  getOriginVideoAudience,
+  getOriginVideoChannelAudience,
+  unicastTo
+} from './misc'
 import { addActivityData } from './send-add'
 import { createActivityData } from './send-create'
 
-async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+async function buildVideoAnnounceToFollowers (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)
+  const accountsToForwardView = await getAccountsInvolvedInVideo(video)
+  const audience = getObjectFollowersAudience(accountsToForwardView)
+  const data = await announceActivityData(url, byAccount, announcedActivity, audience)
+
+  return data
+}
+
+async function sendVideoAnnounceToFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
+  const data = await buildVideoAnnounceToFollowers(byAccount, video, t)
+
   return broadcastToFollowers(data, byAccount, [ byAccount ], t)
 }
 
-async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) {
+async function sendVideoAnnounceToOrigin (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 accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video)
+  const audience = getOriginVideoAudience(video, accountsInvolvedInVideo)
+  const data = await createActivityData(url, byAccount, announcedActivity, audience)
+
+  return unicastTo(data, byAccount, videoChannel.Account.sharedInboxUrl, t)
+}
+
+async function buildVideoChannelAnnounceToFollowers (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)
+  const accountsToForwardView = await getAccountsInvolvedInVideoChannel(videoChannel)
+  const audience = getObjectFollowersAudience(accountsToForwardView)
+  const data = await announceActivityData(url, byAccount, announcedActivity, audience)
+
+  return data
+}
+
+async function sendVideoChannelAnnounceToFollowers (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) {
+  const data = await buildVideoChannelAnnounceToFollowers(byAccount, videoChannel, t)
+
   return broadcastToFollowers(data, byAccount, [ byAccount ], t)
 }
 
-async function announceActivityData (url: string, byAccount: AccountInstance, object: ActivityCreate | ActivityAdd) {
+async function sendVideoChannelAnnounceToOrigin (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) {
+  const url = getAnnounceActivityPubUrl(videoChannel.url, byAccount)
+  const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject())
+
+  const accountsInvolvedInVideo = await getAccountsInvolvedInVideoChannel(videoChannel)
+  const audience = getOriginVideoChannelAudience(videoChannel, accountsInvolvedInVideo)
+  const data = await createActivityData(url, byAccount, announcedActivity, audience)
+
+  return unicastTo(data, byAccount, videoChannel.Account.sharedInboxUrl, t)
+}
+
+async function announceActivityData (
+  url: string,
+  byAccount: AccountInstance,
+  object: ActivityCreate | ActivityAdd,
+  audience?: ActivityAudience
+) {
+  if (!audience) {
+    audience = await getAudience(byAccount)
+  }
+
   const activity: ActivityAnnounce = {
     type: 'Announce',
+    to: audience.to,
+    cc: audience.cc,
     id: url,
     actor: byAccount.url,
     object
@@ -40,7 +102,11 @@ async function announceActivityData (url: string, byAccount: AccountInstance, ob
 // ---------------------------------------------------------------------------
 
 export {
-  sendVideoAnnounce,
-  sendVideoChannelAnnounce,
-  announceActivityData
+  sendVideoAnnounceToFollowers,
+  sendVideoChannelAnnounceToFollowers,
+  sendVideoAnnounceToOrigin,
+  sendVideoChannelAnnounceToOrigin,
+  announceActivityData,
+  buildVideoAnnounceToFollowers,
+  buildVideoChannelAnnounceToFollowers
 }
index 113d892335a667bc3e1cbcfcf3c5fb3d82cf96dd..bf66606c13426805a7138b362a4b0a729b08d7ce 100644 (file)
@@ -9,7 +9,7 @@ import {
   getAccountsInvolvedInVideo,
   getAudience,
   getOriginVideoAudience,
-  getVideoFollowersAudience,
+  getObjectFollowersAudience,
   unicastTo
 } from './misc'
 
@@ -47,7 +47,7 @@ async function sendCreateViewToVideoFollowers (byAccount: AccountInstance, video
   const viewActivity = createViewActivityData(byAccount, video)
 
   const accountsToForwardView = await getAccountsInvolvedInVideo(video)
-  const audience = getVideoFollowersAudience(accountsToForwardView)
+  const audience = getObjectFollowersAudience(accountsToForwardView)
   const data = await createActivityData(url, byAccount, viewActivity, audience)
 
   // Use the server account to send the view, because it could be an unregistered account
@@ -72,7 +72,7 @@ async function sendCreateDislikeToVideoFollowers (byAccount: AccountInstance, vi
   const dislikeActivity = createDislikeActivityData(byAccount, video)
 
   const accountsToForwardView = await getAccountsInvolvedInVideo(video)
-  const audience = getVideoFollowersAudience(accountsToForwardView)
+  const audience = getObjectFollowersAudience(accountsToForwardView)
   const data = await createActivityData(url, byAccount, dislikeActivity, audience)
 
   const followersException = [ byAccount ]
index 8ca775bf3d77066779e28663a39bc26ef1258829..41b879b8ac6f59768d46e01842ebeb8d465b5650 100644 (file)
@@ -7,7 +7,7 @@ import {
   getAccountsInvolvedInVideo,
   getAudience,
   getOriginVideoAudience,
-  getVideoFollowersAudience,
+  getObjectFollowersAudience,
   unicastTo
 } from './misc'
 
@@ -25,7 +25,7 @@ async function sendLikeToVideoFollowers (byAccount: AccountInstance, video: Vide
   const url = getVideoLikeActivityPubUrl(byAccount, video)
 
   const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video)
-  const audience = getVideoFollowersAudience(accountsInvolvedInVideo)
+  const audience = getObjectFollowersAudience(accountsInvolvedInVideo)
   const data = await likeActivityData(url, byAccount, video, audience)
 
   const toAccountsFollowers = await getAccountsInvolvedInVideo(video)
index 79fc113f05e7e459e1f8b8499107c5b0b8a0a45c..9b732df404649f3d96317d7c0b59bc126407953b 100644 (file)
@@ -10,7 +10,7 @@ import { AccountInstance } from '../../../models'
 import { AccountFollowInstance } from '../../../models/account/account-follow-interface'
 import { VideoInstance } from '../../../models/video/video-interface'
 import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
-import { broadcastToFollowers, getAccountsInvolvedInVideo, getAudience, getVideoFollowersAudience, unicastTo } from './misc'
+import { broadcastToFollowers, getAccountsInvolvedInVideo, getAudience, getObjectFollowersAudience, unicastTo } from './misc'
 import { createActivityData, createDislikeActivityData } from './send-create'
 import { followActivityData } from './send-follow'
 import { likeActivityData } from './send-like'
@@ -43,7 +43,7 @@ async function sendUndoLikeToVideoFollowers (byAccount: AccountInstance, video:
   const undoUrl = getUndoActivityPubUrl(likeUrl)
 
   const toAccountsFollowers = await getAccountsInvolvedInVideo(video)
-  const audience = getVideoFollowersAudience(toAccountsFollowers)
+  const audience = getObjectFollowersAudience(toAccountsFollowers)
   const object = await likeActivityData(likeUrl, byAccount, video)
   const data = await undoActivityData(undoUrl, byAccount, object, audience)
 
index 689e200a6c4cce00074b2db63e72e7d1a8169a8c..e14b0f50c0f5d290ac04befca30ac8a9ac28e94f 100644 (file)
@@ -3,7 +3,7 @@ import { getServerAccount } from '../../helpers/utils'
 import { database as db } from '../../initializers'
 import { VideoChannelInstance } from '../../models/index'
 import { VideoInstance } from '../../models/video/video-interface'
-import { sendVideoAnnounce, sendVideoChannelAnnounce } from './send/send-announce'
+import { sendVideoAnnounceToFollowers, sendVideoChannelAnnounceToFollowers } from './send/send-announce'
 
 async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t: Transaction) {
   const serverAccount = await getServerAccount()
@@ -13,7 +13,7 @@ async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t:
     videoChannelId: videoChannel.id
   }, { transaction: t })
 
-  return sendVideoChannelAnnounce(serverAccount, videoChannel, t)
+  return sendVideoChannelAnnounceToFollowers(serverAccount, videoChannel, t)
 }
 
 async function shareVideoByServer (video: VideoInstance, t: Transaction) {
@@ -24,7 +24,7 @@ async function shareVideoByServer (video: VideoInstance, t: Transaction) {
     videoId: video.id
   }, { transaction: t })
 
-  return sendVideoAnnounce(serverAccount, video, t)
+  return sendVideoAnnounceToFollowers(serverAccount, video, t)
 }
 
 export {
index 17395a99b81291d091877fbc2ad48741a9bd5e57..6475c4218c3aa412e473656600e61023129e0546 100644 (file)
@@ -22,37 +22,37 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseInstance) {
 }
 
 function getVideoViewActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) {
-  return video.url + '#views/' + byAccount.uuid + '/' + new Date().toISOString()
+  return video.url + '/views/' + byAccount.uuid + '/' + new Date().toISOString()
 }
 
 function getVideoLikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) {
-  return byAccount.url + '#likes/' + video.id
+  return byAccount.url + '/likes/' + video.id
 }
 
 function getVideoDislikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) {
-  return byAccount.url + '#dislikes/' + video.id
+  return byAccount.url + '/dislikes/' + video.id
 }
 
 function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) {
   const me = accountFollow.AccountFollower
   const following = accountFollow.AccountFollowing
 
-  return me.url + '#follows/' + following.id
+  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
+  return follower.url + '/accepts/follows/' + me.id
 }
 
 function getAnnounceActivityPubUrl (originalUrl: string, byAccount: AccountInstance) {
-  return originalUrl + '#announces/' + byAccount.id
+  return originalUrl + '/announces/' + byAccount.id
 }
 
 function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) {
-  return originalUrl + '#updates/' + updatedAt
+  return originalUrl + '/updates/' + updatedAt
 }
 
 function getUndoActivityPubUrl (originalUrl: string) {
index 29ebd169d4a3e911adc2f86a22226394fcc4068b..9692f9be732d99e649c6cf96d75a9af931e47c3e 100644 (file)
@@ -1,10 +1,18 @@
-import { Request, Response, NextFunction } from 'express'
+import { Request, Response, NextFunction, RequestHandler } from 'express'
+import { eachSeries } from 'async'
 
 // Syntactic sugar to avoid try/catch in express controllers
 // Thanks: https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
-function asyncMiddleware (fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) {
+function asyncMiddleware (fun: RequestHandler | RequestHandler[]) {
   return (req: Request, res: Response, next: NextFunction) => {
-    return Promise.resolve(fn(req, res, next))
+    if (Array.isArray(fun) === true) {
+      return eachSeries(fun as RequestHandler[], (f, cb) => {
+        Promise.resolve(f(req, res, cb))
+          .catch(next)
+      }, next)
+    }
+
+    return Promise.resolve((fun as RequestHandler)(req, res, next))
       .catch(next)
   }
 }
index 07ae76b63f43e68ac45bae7db20260d4c3fe8247..47ed6a7bb1591ac56f7e5d9979baf3defee9fa13 100644 (file)
@@ -1,9 +1,7 @@
 import * as express from 'express'
 import { param } from 'express-validator/check'
 import { logger } from '../../helpers'
-import { isAccountNameValid } from '../../helpers/custom-validators/accounts'
-import { database as db } from '../../initializers/database'
-import { AccountInstance } from '../../models'
+import { checkLocalAccountNameExists, isAccountNameValid } from '../../helpers/custom-validators/accounts'
 import { checkErrors } from './utils'
 
 const localAccountValidator = [
@@ -13,7 +11,7 @@ const localAccountValidator = [
     logger.debug('Checking localAccountValidator parameters', { parameters: req.params })
 
     checkErrors(req, res, () => {
-      checkLocalAccountExists(req.params.name, res, next)
+      checkLocalAccountNameExists(req.params.name, res, next)
     })
   }
 ]
@@ -23,23 +21,3 @@ const localAccountValidator = [
 export {
   localAccountValidator
 }
-
-// ---------------------------------------------------------------------------
-
-function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
-  db.Account.loadLocalByName(name)
-    .then(account => {
-      if (!account) {
-        return res.status(404)
-          .send({ error: 'Account not found' })
-          .end()
-      }
-
-      res.locals.account = account
-      return callback(null, account)
-    })
-    .catch(err => {
-      logger.error('Error in account request validator.', err)
-      return res.sendStatus(500)
-    })
-}
index ea107bbe8f67b5036b7f24a964d470de76b7d968..77a1a0d4bfb4c5a7ded8dec4ccdaecd890fc85a9 100644 (file)
@@ -14,8 +14,22 @@ function checkErrors (req: express.Request, res: express.Response, next: express
   return next()
 }
 
+function areValidationErrors (req: express.Request, res: express.Response) {
+  const errors = validationResult(req)
+
+  if (!errors.isEmpty()) {
+    logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() })
+    res.status(400).json({ errors: errors.mapped() })
+
+    return true
+  }
+
+  return false
+}
+
 // ---------------------------------------------------------------------------
 
 export {
-  checkErrors
+  checkErrors,
+  areValidationErrors
 }
index c6fd3b59d927158847ee0824eb752e53bb1ceeff..f30fbf0dcd9112803a841838f12afb7622492525 100644 (file)
@@ -1,13 +1,19 @@
 import * as express from 'express'
 import { body, param } from 'express-validator/check'
 import { UserRight } from '../../../shared'
-import { checkVideoAccountExists } from '../../helpers/custom-validators/accounts'
-import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
-import { checkVideoChannelExists, isIdOrUUIDValid } from '../../helpers/index'
+import { checkAccountIdExists } from '../../helpers/custom-validators/accounts'
+import { isIdValid } from '../../helpers/custom-validators/misc'
+import {
+  checkVideoChannelExists,
+  isVideoChannelDescriptionValid,
+  isVideoChannelExistsPromise,
+  isVideoChannelNameValid
+} from '../../helpers/custom-validators/video-channels'
+import { isIdOrUUIDValid } from '../../helpers/index'
 import { logger } from '../../helpers/logger'
 import { database as db } from '../../initializers'
 import { UserInstance } from '../../models'
-import { checkErrors } from './utils'
+import { areValidationErrors, checkErrors } from './utils'
 
 const listVideoAccountChannelsValidator = [
   param('accountId').custom(isIdOrUUIDValid).withMessage('Should have a valid account id'),
@@ -16,7 +22,7 @@ const listVideoAccountChannelsValidator = [
     logger.debug('Checking listVideoAccountChannelsValidator parameters', { parameters: req.body })
 
     checkErrors(req, res, () => {
-      checkVideoAccountExists(req.params.accountId, res, next)
+      checkAccountIdExists(req.params.accountId, res, next)
     })
   }
 ]
@@ -90,6 +96,28 @@ const videoChannelsGetValidator = [
   }
 ]
 
+const videoChannelsShareValidator = [
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+  param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking videoChannelShare parameters', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+    if (!await isVideoChannelExistsPromise(req.params.id, res)) return
+
+    const share = await db.VideoChannelShare.load(res.locals.video.id, req.params.accountId)
+    if (!share) {
+      return res.status(404)
+        .end()
+    }
+
+    res.locals.videoChannelShare = share
+
+    return next()
+  }
+]
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -97,7 +125,8 @@ export {
   videoChannelsAddValidator,
   videoChannelsUpdateValidator,
   videoChannelsRemoveValidator,
-  videoChannelsGetValidator
+  videoChannelsGetValidator,
+  videoChannelsShareValidator
 }
 
 // ---------------------------------------------------------------------------
index df0eb7b96c5d4793dc3b7a06ff7ae300854ee10c..5ffc85210502cf8062d98bbd4a820d89f9948f25 100644 (file)
@@ -24,7 +24,8 @@ import { CONSTRAINTS_FIELDS, SEARCHABLE_COLUMNS } from '../../initializers'
 import { database as db } from '../../initializers/database'
 import { UserInstance } from '../../models/account/user-interface'
 import { authenticate } from '../oauth'
-import { checkErrors } from './utils'
+import { areValidationErrors, checkErrors } from './utils'
+import { isVideoExistsPromise } from '../../helpers/index'
 
 const videosAddValidator = [
   body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
@@ -230,6 +231,28 @@ const videoRateValidator = [
   }
 ]
 
+const videosShareValidator = [
+  param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
+  param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking videoShare parameters', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+    if (!await isVideoExistsPromise(req.params.id, res)) return
+
+    const share = await db.VideoShare.load(req.params.accountId, res.locals.video.id)
+    if (!share) {
+      return res.status(404)
+        .end()
+    }
+
+    res.locals.videoShare = share
+
+    return next()
+  }
+]
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -238,6 +261,7 @@ export {
   videosGetValidator,
   videosRemoveValidator,
   videosSearchValidator,
+  videosShareValidator,
 
   videoAbuseReportValidator,
 
index b409d1db3dadc21e4e9cf577492c2c993a6f6c3a..21f81e901801c2058d586bb621a1a2f18ee1846d 100644 (file)
@@ -6,6 +6,7 @@ import { VideoChannelObject } from '../../../shared/models/activitypub/objects/v
 import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model'
 import { AccountInstance } from '../account/account-interface'
 import { VideoInstance } from './video-interface'
+import { VideoChannelShareInstance } from './video-channel-share-interface'
 
 export namespace VideoChannelMethods {
   export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel
@@ -47,6 +48,7 @@ export interface VideoChannelAttributes {
 
   Account?: AccountInstance
   Videos?: VideoInstance[]
+  VideoChannelShares?: VideoChannelShareInstance[]
 }
 
 export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAttributes, Sequelize.Instance<VideoChannelAttributes> {
index 8bb531af27b5412a24d655c405ab2ed56312d205..bcb3a0e246db48aa6683bd4a2aa359d140da8e61 100644 (file)
@@ -5,10 +5,12 @@ import { VideoChannelInstance } from './video-channel-interface'
 
 export namespace VideoChannelShareMethods {
   export type LoadAccountsByShare = (videoChannelId: number) => Bluebird<AccountInstance[]>
+  export type Load = (accountId: number, videoId: number) => Bluebird<VideoChannelShareInstance>
 }
 
 export interface VideoChannelShareClass {
   loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare
+  load: VideoChannelShareMethods.Load
 }
 
 export interface VideoChannelShareAttributes {
index 01f84c806cb83ae72e7ec16cecb86d2abd24df05..e47c0dae7b7e26d2bafc609b3db65b7c720b0047 100644 (file)
@@ -5,6 +5,7 @@ import { VideoChannelShareAttributes, VideoChannelShareInstance, VideoChannelSha
 
 let VideoChannelShare: Sequelize.Model<VideoChannelShareInstance, VideoChannelShareAttributes>
 let loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare
+let load: VideoChannelShareMethods.Load
 
 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
   VideoChannelShare = sequelize.define<VideoChannelShareInstance, VideoChannelShareAttributes>('VideoChannelShare',
@@ -23,6 +24,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
 
   const classMethods = [
     associate,
+    load,
     loadAccountsByShare
   ]
   addMethodsToModel(VideoChannelShare, classMethods)
@@ -50,6 +52,19 @@ function associate (models) {
   })
 }
 
+load = function (accountId: number, videoChannelId: number) {
+  return VideoChannelShare.findOne({
+    where: {
+      accountId,
+      videoChannelId
+    },
+    include: [
+      VideoChannelShare['sequelize'].models.Account,
+      VideoChannelShare['sequelize'].models.VideoChannel
+    ]
+  })
+}
+
 loadAccountsByShare = function (videoChannelId: number) {
   const query = {
     where: {
index 64130310d4db7ed47844812820f5072cc1d4b8b7..e11268b2c2d1e926636ccce0377adc904511e194 100644 (file)
@@ -6,6 +6,8 @@ import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete'
 
 import { addMethodsToModel, getSort } from '../utils'
 import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface'
+import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url'
+import { activityPubCollection } from '../../helpers/activitypub'
 
 let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
 let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
@@ -139,6 +141,18 @@ toFormattedJSON = function (this: VideoChannelInstance) {
 }
 
 toActivityPubObject = function (this: VideoChannelInstance) {
+  let sharesObject
+  if (Array.isArray(this.VideoChannelShares)) {
+    const shares: string[] = []
+
+    for (const videoChannelShare of this.VideoChannelShares) {
+      const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account)
+      shares.push(shareUrl)
+    }
+
+    sharesObject = activityPubCollection(shares)
+  }
+
   const json = {
     type: 'VideoChannel' as 'VideoChannel',
     id: this.url,
@@ -146,7 +160,8 @@ toActivityPubObject = function (this: VideoChannelInstance) {
     content: this.description,
     name: this.name,
     published: this.createdAt.toISOString(),
-    updated: this.updatedAt.toISOString()
+    updated: this.updatedAt.toISOString(),
+    shares: sharesObject
   }
 
   return json
index 56956884232a97d1f2bc495b299160333368608c..ad23444b617560f0eec82d60e334471c216aeda4 100644 (file)
@@ -1,14 +1,16 @@
+import * as Bluebird from 'bluebird'
 import * as Sequelize from 'sequelize'
 import { AccountInstance } from '../account/account-interface'
 import { VideoInstance } from './video-interface'
-import * as Bluebird from 'bluebird'
 
 export namespace VideoShareMethods {
-  export type LoadAccountsByShare = (videoChannelId: number) => Bluebird<AccountInstance[]>
+  export type LoadAccountsByShare = (videoId: number) => Bluebird<AccountInstance[]>
+  export type Load = (accountId: number, videoId: number) => Bluebird<VideoShareInstance>
 }
 
 export interface VideoShareClass {
   loadAccountsByShare: VideoShareMethods.LoadAccountsByShare
+  load: VideoShareMethods.Load
 }
 
 export interface VideoShareAttributes {
index 22ac31a4a04a917bccaa55c685bb9474c499dbac..fe5d56d42b16b4e151a465bc847320fdb70838e0 100644 (file)
@@ -5,6 +5,7 @@ import { VideoShareAttributes, VideoShareInstance, VideoShareMethods } from './v
 
 let VideoShare: Sequelize.Model<VideoShareInstance, VideoShareAttributes>
 let loadAccountsByShare: VideoShareMethods.LoadAccountsByShare
+let load: VideoShareMethods.Load
 
 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
   VideoShare = sequelize.define<VideoShareInstance, VideoShareAttributes>('VideoShare',
@@ -23,7 +24,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
 
   const classMethods = [
     associate,
-    loadAccountsByShare
+    loadAccountsByShare,
+    load
   ]
   addMethodsToModel(VideoShare, classMethods)
 
@@ -50,6 +52,18 @@ function associate (models) {
   })
 }
 
+load = function (accountId: number, videoId: number) {
+  return VideoShare.findOne({
+    where: {
+      accountId,
+      videoId
+    },
+    include: [
+      VideoShare['sequelize'].models.Account
+    ]
+  })
+}
+
 loadAccountsByShare = function (videoId: number) {
   const query = {
     where: {
index 457bfce7723cf6f144912a0e91e734cd8c37f3cd..4956b57eed975b920721f5aac9f607b0a785e284 100644 (file)
@@ -32,6 +32,7 @@ import { isVideoNameValid, isVideoLicenceValid, isVideoNSFWValid, isVideoDescrip
 import { logger } from '../../helpers/logger'
 import { generateImageFromVideoFile, transcode, getVideoFileHeight } from '../../helpers/ffmpeg-utils'
 import { createTorrentPromise, writeFilePromise, unlinkPromise, renamePromise, statPromise } from '../../helpers/core-utils'
+import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url'
 
 let Video: Sequelize.Model<VideoInstance, VideoAttributes>
 let getOriginalFile: VideoMethods.GetOriginalFile
@@ -573,6 +574,18 @@ toActivityPubObject = function (this: VideoInstance) {
     dislikesObject = activityPubCollection(dislikes)
   }
 
+  let sharesObject
+  if (Array.isArray(this.VideoShares)) {
+    const shares: string[] = []
+
+    for (const videoShare of this.VideoShares) {
+      const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account)
+      shares.push(shareUrl)
+    }
+
+    sharesObject = activityPubCollection(shares)
+  }
+
   const url = []
   for (const file of this.VideoFiles) {
     url.push({
@@ -630,7 +643,8 @@ toActivityPubObject = function (this: VideoInstance) {
     },
     url,
     likes: likesObject,
-    dislikes: dislikesObject
+    dislikes: dislikesObject,
+    shares: sharesObject
   }
 
   return videoObject
@@ -823,7 +837,8 @@ listAllAndSharedByAccountForOutbox = function (accountId: number, start: number,
               accountId
             }
           ]
-        }
+        },
+        include: [ Video['sequelize'].models.Account ]
       },
       {
         model: Video['sequelize'].models.VideoChannel,
index c9325b5dfae7922dc229e1eb53216a1cfab6c491..dcce8696b6ab6a6deb3bc6c60051f6e0f2efd353 100644 (file)
@@ -1,3 +1,5 @@
+import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection'
+
 export interface VideoChannelObject {
   type: 'VideoChannel'
   id: string
@@ -7,4 +9,5 @@ export interface VideoChannelObject {
   published: string
   updated: string
   actor?: string
+  shares?: ActivityPubOrderedCollection<string>
 }
index a4e032d042f58e773d1496efb871f4fdde947f29..a15ec7142323030b0b10180061976223106ff314 100644 (file)
@@ -27,4 +27,5 @@ export interface VideoTorrentObject {
   actor?: string
   likes?: ActivityPubOrderedCollection<string>
   dislikes?: ActivityPubOrderedCollection<string>
+  shares?: ActivityPubOrderedCollection<string>
 }