Send server announce when users upload a video
authorChocobozzz <florian.bigard@gmail.com>
Thu, 16 Nov 2017 10:08:25 +0000 (11:08 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 18:40:52 +0000 (19:40 +0100)
22 files changed:
client/src/app/core/auth/auth.service.ts
server/controllers/activitypub/client.ts
server/controllers/api/server/follows.ts
server/controllers/api/videos/index.ts
server/helpers/activitypub.ts
server/helpers/core-utils.ts
server/helpers/custom-validators/activitypub/videos.ts
server/helpers/peertube-crypto.ts
server/helpers/utils.ts
server/initializers/constants.ts
server/initializers/installer.ts
server/lib/activitypub/misc.ts
server/lib/activitypub/process-create.ts
server/lib/activitypub/send-request.ts
server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts
server/lib/video-channel.ts
server/models/account/account-follow-interface.ts
server/models/account/account-follow.ts
server/models/video/video-channel.ts
server/models/video/video.ts
shared/models/activitypub/objects/video-channel-object.ts
shared/models/activitypub/objects/video-torrent-object.ts

index 0aa276c693b4d866baecd6818f54741487d51991..9e6c6b8887cb0055b55ddcea8ed53d11e21ef7da 100644 (file)
@@ -257,6 +257,7 @@ export class AuthService {
     this.user.save()
 
     this.setStatus(AuthStatus.LoggedIn)
+    this.userInformationLoaded.next(true)
   }
 
   private handleRefreshToken (obj: UserRefreshToken) {
index 49dd24e79e118346456755c9928d7450b54653bc..76049f496055681630a08e5a5e8b645f41cee4b1 100644 (file)
@@ -46,7 +46,7 @@ async function accountFollowersController (req: express.Request, res: express.Re
   const page = req.params.page || 1
   const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
 
-  const result = await db.AccountFollow.listAcceptedFollowerUrlsForApi(account.id, start, count)
+  const result = await db.AccountFollow.listAcceptedFollowerUrlsForApi([ account.id ], start, count)
   const activityPubResult = activityPubCollectionPagination(req.url, page, result)
 
   return res.json(activityPubResult)
@@ -58,7 +58,7 @@ async function accountFollowingController (req: express.Request, res: express.Re
   const page = req.params.page || 1
   const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
 
-  const result = await db.AccountFollow.listAcceptedFollowingUrlsForApi(account.id, start, count)
+  const result = await db.AccountFollow.listAcceptedFollowingUrlsForApi([ account.id ], start, count)
   const activityPubResult = activityPubCollectionPagination(req.url, page, result)
 
   return res.json(activityPubResult)
index ac8ea87f93cbf1947bf287171eb9eccf218b11b9..e00787f0252ff1e6501866f5dca96e3b7cfb8603 100644 (file)
@@ -2,7 +2,7 @@ import * as express from 'express'
 import { UserRight } from '../../../../shared/models/users/user-right.enum'
 import { getFormattedObjects } from '../../../helpers'
 import { logger } from '../../../helpers/logger'
-import { getApplicationAccount } from '../../../helpers/utils'
+import { getServerAccount } from '../../../helpers/utils'
 import { getAccountFromWebfinger } from '../../../helpers/webfinger'
 import { SERVER_ACCOUNT_NAME } from '../../../initializers/constants'
 import { database as db } from '../../../initializers/database'
@@ -50,14 +50,14 @@ export {
 // ---------------------------------------------------------------------------
 
 async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) {
-  const applicationAccount = await getApplicationAccount()
+  const applicationAccount = await getServerAccount()
   const resultList = await db.AccountFollow.listFollowingForApi(applicationAccount.id, req.query.start, req.query.count, req.query.sort)
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
 
 async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) {
-  const applicationAccount = await getApplicationAccount()
+  const applicationAccount = await getServerAccount()
   const resultList = await db.AccountFollow.listFollowersForApi(applicationAccount.id, req.query.start, req.query.count, req.query.sort)
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
@@ -65,7 +65,7 @@ async function listFollowers (req: express.Request, res: express.Response, next:
 
 async function follow (req: express.Request, res: express.Response, next: express.NextFunction) {
   const hosts = req.body.hosts as string[]
-  const fromAccount = await getApplicationAccount()
+  const fromAccount = await getServerAccount()
 
   const tasks: Promise<any>[] = []
   const accountName = SERVER_ACCOUNT_NAME
index ebc07e17909aeed6ecca03bceb4d6faee4166136..a5414cc50aa145b6d3cae0aafd2e2d3cd23962d3 100644 (file)
@@ -12,10 +12,10 @@ import {
   resetSequelizeInstance,
   retryTransactionWrapper
 } from '../../../helpers'
-import { getActivityPubUrl } from '../../../helpers/activitypub'
+import { getActivityPubUrl, 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, sendUpdateVideoChannel } from '../../../lib/activitypub/send-request'
+import { sendAddVideo, sendUpdateVideo } from '../../../lib/activitypub/send-request'
 import { transcodingJobScheduler } from '../../../lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler'
 import {
   asyncMiddleware,
@@ -56,7 +56,7 @@ const storage = multer.diskStorage({
       randomString = 'fake-random-string'
     }
 
-    cb(null, randomString + '.' + extension)
+    cb(null, randomString + extension)
   }
 })
 
@@ -237,6 +237,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
     if (video.privacy === VideoPrivacy.PRIVATE) return undefined
 
     await sendAddVideo(video, t)
+    await shareVideoByServer(video, t)
   })
 
   logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoUUID)
@@ -254,7 +255,7 @@ async function updateVideoRetryWrapper (req: express.Request, res: express.Respo
 }
 
 async function updateVideo (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.video
+  const videoInstance: VideoInstance = res.locals.video
   const videoFieldsSave = videoInstance.toJSON()
   const videoInfoToUpdate: VideoUpdate = req.body
   const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
@@ -284,7 +285,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 sendUpdateVideoChannel(videoInstance, t)
+        await sendUpdateVideo(videoInstance, t)
       }
 
       // Video is not private anymore, send a create action to remote servers
index de20ba55d28989ce3e19a0b651c7a02cca3e7079..b376b8ca20dada4c73d4b36c2293391350a3c084 100644 (file)
@@ -1,15 +1,19 @@
 import { join } from 'path'
 import * as request from 'request'
+import * as Sequelize from 'sequelize'
 import * as url from 'url'
 import { ActivityIconObject } from '../../shared/index'
 import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
 import { ResultList } from '../../shared/models/result-list.model'
 import { database as db, REMOTE_SCHEME } from '../initializers'
 import { ACTIVITY_PUB_ACCEPT_HEADER, CONFIG, STATIC_PATHS } from '../initializers/constants'
+import { sendAnnounce } from '../lib/activitypub/send-request'
+import { VideoChannelInstance } from '../models/video/video-channel-interface'
 import { VideoInstance } from '../models/video/video-interface'
 import { isRemoteAccountValid } from './custom-validators'
 import { logger } from './logger'
 import { doRequest, doRequestAndSaveToFile } from './requests'
+import { getServerAccount } from './utils'
 
 function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) {
   const thumbnailName = video.getThumbnailName()
@@ -22,6 +26,28 @@ function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObjec
   return doRequestAndSaveToFile(options, thumbnailPath)
 }
 
+async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
+  const serverAccount = await getServerAccount()
+
+  await db.VideoChannelShare.create({
+    accountId: serverAccount.id,
+    videoChannelId: videoChannel.id
+  }, { transaction: t })
+
+  return sendAnnounce(serverAccount, videoChannel, t)
+}
+
+async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transaction) {
+  const serverAccount = await getServerAccount()
+
+  await db.VideoShare.create({
+    accountId: serverAccount.id,
+    videoId: video.id
+  }, { transaction: t })
+
+  return sendAnnounce(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
@@ -172,7 +198,9 @@ export {
   generateThumbnailFromUrl,
   getOrCreateAccount,
   fetchRemoteVideoPreview,
-  fetchRemoteVideoDescription
+  fetchRemoteVideoDescription,
+  shareVideoChannelByServer,
+  shareVideoByServer
 }
 
 // ---------------------------------------------------------------------------
index d8748e1d7e2c7c52060beaa73ae56639f653b383..4ff07848ca86079f8a437478f6275d8e6337e242 100644 (file)
@@ -20,9 +20,6 @@ import * as bcrypt from 'bcrypt'
 import * as createTorrent from 'create-torrent'
 import * as rimraf from 'rimraf'
 import * as pem from 'pem'
-import * as jsonld from 'jsonld'
-import * as jsig from 'jsonld-signatures'
-jsig.use('jsonld', jsonld)
 
 function isTestInstance () {
   return process.env.NODE_ENV === 'test'
@@ -120,8 +117,6 @@ const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
 const createTorrentPromise = promisify2<string, any, any>(createTorrent)
 const rimrafPromise = promisify1WithVoid<string>(rimraf)
 const statPromise = promisify1<string, Stats>(stat)
-const jsonldSignPromise = promisify2<object, { privateKeyPem: string, creator: string }, object>(jsig.sign)
-const jsonldVerifyPromise = promisify2<object, object, object>(jsig.verify)
 
 // ---------------------------------------------------------------------------
 
@@ -150,7 +145,5 @@ export {
   bcryptHashPromise,
   createTorrentPromise,
   rimrafPromise,
-  statPromise,
-  jsonldSignPromise,
-  jsonldVerifyPromise
+  statPromise
 }
index 9ddacd6010ab25628902e8a6a47ba9851b5ccc9d..89c49b0dfd36d222839e84ebe4483829520c82bd 100644 (file)
@@ -34,7 +34,7 @@ function isActivityPubVideoDurationValid (value: string) {
     typeof value === 'string' &&
     value.startsWith('PT') &&
     value.endsWith('S') &&
-    isVideoDurationValid(value.replace(/[^0-9]+/, ''))
+    isVideoDurationValid(value.replace(/[^0-9]+/g, ''))
 }
 
 function isVideoTorrentObjectValid (video: any) {
@@ -46,13 +46,14 @@ function isVideoTorrentObjectValid (video: any) {
     isRemoteIdentifierValid(video.category) &&
     isRemoteIdentifierValid(video.licence) &&
     isRemoteIdentifierValid(video.language) &&
-    isVideoViewsValid(video.video) &&
+    isVideoViewsValid(video.views) &&
     isVideoNSFWValid(video.nsfw) &&
     isDateValid(video.published) &&
     isDateValid(video.updated) &&
     isRemoteVideoContentValid(video.mediaType, video.content) &&
     isRemoteVideoIconValid(video.icon) &&
-    setValidRemoteVideoUrls(video.url)
+    setValidRemoteVideoUrls(video) &&
+    video.url.length !== 0
 }
 
 function isVideoFlagValid (activity: any) {
@@ -132,8 +133,8 @@ function isRemoteVideoIconValid (icon: any) {
   return icon.type === 'Image' &&
     isVideoUrlValid(icon.url) &&
     icon.mediaType === 'image/jpeg' &&
-    validator.isInt(icon.width, { min: 0 }) &&
-    validator.isInt(icon.height, { min: 0 })
+    validator.isInt(icon.width + '', { min: 0 }) &&
+    validator.isInt(icon.height + '', { min: 0 })
 }
 
 function setValidRemoteVideoUrls (video: any) {
@@ -149,6 +150,6 @@ function isRemoteVideoUrlValid (url: any) {
   return url.type === 'Link' &&
     ACTIVITY_PUB.VIDEO_URL_MIME_TYPES.indexOf(url.mimeType) !== -1 &&
     isVideoUrlValid(url.url) &&
-    validator.isInt(url.width, { min: 0 }) &&
-    validator.isInt(url.size, { min: 0 })
+    validator.isInt(url.width + '', { min: 0 }) &&
+    validator.isInt(url.size + '', { min: 0 })
 }
index 6d50e446f59a124488f2eb392e3212142d426e69..04a8d5681d6be87cc97746a35efff993b5f59413 100644 (file)
@@ -1,4 +1,6 @@
+import * as jsonld from 'jsonld'
 import * as jsig from 'jsonld-signatures'
+jsig.use('jsonld', jsonld)
 
 import {
   PRIVATE_RSA_KEY_SIZE,
@@ -9,9 +11,7 @@ import {
   bcryptGenSaltPromise,
   bcryptHashPromise,
   createPrivateKey,
-  getPublicKey,
-  jsonldSignPromise,
-  jsonldVerifyPromise
+  getPublicKey
 } from './core-utils'
 import { logger } from './logger'
 import { AccountInstance } from '../models/account/account-interface'
@@ -45,7 +45,7 @@ function isSignatureVerified (fromAccount: AccountInstance, signedDocument: obje
     publicKeyOwner: publicKeyOwnerObject
   }
 
-  return jsonldVerifyPromise(signedDocument, options)
+  return jsig.promises.verify(signedDocument, options)
     .catch(err => {
       logger.error('Cannot check signature.', err)
       return false
@@ -58,7 +58,7 @@ function signObject (byAccount: AccountInstance, data: any) {
     creator: byAccount.url
   }
 
-  return jsonldSignPromise(data, options)
+  return jsig.promises.sign(data, options)
 }
 
 function comparePassword (plainPassword: string, hashPassword: string) {
index 39957c90f5c0c9e6c00b2bc5e9cad5ba7de50733..3af14a68a51182ba16d81564d8ec2ffdc94d175a 100644 (file)
@@ -6,6 +6,7 @@ import { CONFIG, database as db } from '../initializers'
 import { ResultList } from '../../shared'
 import { VideoResolution } from '../../shared/models/videos/video-resolution.enum'
 import { AccountInstance } from '../models/account/account-interface'
+import { logger } from './logger'
 
 function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) {
   return res.type('json').status(400).end()
@@ -79,13 +80,18 @@ function resetSequelizeInstance (instance: Sequelize.Instance<any>, savedFields:
   })
 }
 
-let applicationAccount: AccountInstance
-async function getApplicationAccount () {
-  if (applicationAccount === undefined) {
-    applicationAccount = await db.Account.loadApplication()
+let serverAccount: AccountInstance
+async function getServerAccount () {
+  if (serverAccount === undefined) {
+    serverAccount = await db.Account.loadApplication()
   }
 
-  return Promise.resolve(applicationAccount)
+  if (!serverAccount) {
+    logger.error('Cannot load server account.')
+    process.exit(0)
+  }
+
+  return Promise.resolve(serverAccount)
 }
 
 type SortType = { sortModel: any, sortValue: string }
@@ -99,6 +105,6 @@ export {
   isSignupAllowed,
   computeResolutionsToTranscode,
   resetSequelizeInstance,
-  getApplicationAccount,
+  getServerAccount,
   SortType
 }
index 99da95ab95bcb5547977ae33b3f718098a0f4696..dca223370a57f06049f93011064819d09c26e188 100644 (file)
@@ -209,9 +209,9 @@ const VIDEO_PRIVACIES = {
 }
 
 const VIDEO_MIMETYPE_EXT = {
-  'video/webm': 'webm',
-  'video/ogg': 'ogv',
-  'video/mp4': 'mp4'
+  'video/webm': '.webm',
+  'video/ogg': '.ogv',
+  'video/mp4': '.mp4'
 }
 
 // ---------------------------------------------------------------------------
index c3521a9e4e6ab0b11c0b51f8cb0b74df4c543fc5..3f4c4dfbbf5b0544dad9c6212b4afb385062a0b4 100644 (file)
@@ -13,9 +13,9 @@ async function installApplication () {
     await db.sequelize.sync()
     await removeCacheDirectories()
     await createDirectoriesIfNotExist()
+    await createApplicationIfNotExist()
     await createOAuthClientIfNotExist()
     await createOAuthAdminIfNotExist()
-    await createApplicationIfNotExist()
   } catch (err) {
     logger.error('Cannot install application.', err)
     throw err
index 2cf0c4fd172e31694b2b90605b839b750ce76266..43d26c3282be6c996ce5b97f4683c3a2d59bd11c 100644 (file)
@@ -28,9 +28,9 @@ async function videoActivityObjectToDBAttributes (
     description: videoObject.content,
     channelId: videoChannel.id,
     duration: parseInt(duration, 10),
-    createdAt: videoObject.published,
+    createdAt: new Date(videoObject.published),
     // FIXME: updatedAt does not seems to be considered by Sequelize
-    updatedAt: videoObject.updated,
+    updatedAt: new Date(videoObject.updated),
     views: videoObject.views,
     likes: 0,
     dislikes: 0,
@@ -46,7 +46,7 @@ async function videoActivityObjectToDBAttributes (
 
 function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) {
   const fileUrls = videoObject.url
-    .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1)
+    .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1 && u.url.startsWith('video/'))
 
   const attributes: VideoFileAttributes[] = []
   for (const url of fileUrls) {
index 1b825ebbcab24dcee3abb23b330910b07327c8de..4e4c9f7032925b951452bf508187f17cee2bc450 100644 (file)
@@ -48,8 +48,8 @@ async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCr
       name: videoChannelToCreateData.name,
       description: videoChannelToCreateData.content,
       uuid: videoChannelToCreateData.uuid,
-      createdAt: videoChannelToCreateData.published,
-      updatedAt: videoChannelToCreateData.updated,
+      createdAt: new Date(videoChannelToCreateData.published),
+      updatedAt: new Date(videoChannelToCreateData.updated),
       remote: true,
       accountId: account.id
     }
index 1dad51828b15085f76dc8f91d767a3d0b221af96..664b9d8268db36c16f52d94c68e637cd8d8eaabb 100644 (file)
@@ -17,46 +17,67 @@ async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Se
   const videoChannelObject = videoChannel.toActivityPubObject()
   const data = await createActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
 
-  return broadcastToFollowers(data, videoChannel.Account, t)
+  return broadcastToFollowers(data, [ videoChannel.Account ], t)
 }
 
 async function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
   const videoChannelObject = videoChannel.toActivityPubObject()
   const data = await updateActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
 
-  return broadcastToFollowers(data, videoChannel.Account, t)
+  return broadcastToFollowers(data, [ videoChannel.Account ], t)
 }
 
 async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
   const data = await deleteActivityData(videoChannel.url, videoChannel.Account)
 
-  return broadcastToFollowers(data, videoChannel.Account, t)
+  return broadcastToFollowers(data, [ videoChannel.Account ], t)
 }
 
 async function sendAddVideo (video: VideoInstance, t: Sequelize.Transaction) {
   const videoObject = video.toActivityPubObject()
   const data = await addActivityData(video.url, video.VideoChannel.Account, video.VideoChannel.url, videoObject)
 
-  return broadcastToFollowers(data, video.VideoChannel.Account, t)
+  return broadcastToFollowers(data, [ video.VideoChannel.Account ], t)
 }
 
 async function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) {
   const videoObject = video.toActivityPubObject()
   const data = await updateActivityData(video.url, video.VideoChannel.Account, videoObject)
 
-  return broadcastToFollowers(data, video.VideoChannel.Account, t)
+  return broadcastToFollowers(data, [ video.VideoChannel.Account ], t)
 }
 
 async function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
   const data = await deleteActivityData(video.url, video.VideoChannel.Account)
 
-  return broadcastToFollowers(data, video.VideoChannel.Account, t)
+  return broadcastToFollowers(data, [ video.VideoChannel.Account ], t)
 }
 
 async function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) {
   const data = await deleteActivityData(account.url, account)
 
-  return broadcastToFollowers(data, account, t)
+  return broadcastToFollowers(data, [ account ], t)
+}
+
+async function sendAnnounce (byAccount: AccountInstance, instance: VideoInstance | VideoChannelInstance, t: Sequelize.Transaction) {
+  const object = instance.toActivityPubObject()
+
+  let url = ''
+  let objectActorUrl: string
+  if ((instance as any).VideoChannel !== undefined) {
+    objectActorUrl = (instance as VideoInstance).VideoChannel.Account.url
+    url = getActivityPubUrl('video', instance.uuid) + '#announce'
+  } else {
+    objectActorUrl = (instance as VideoChannelInstance).Account.url
+    url = getActivityPubUrl('videoChannel', instance.uuid) + '#announce'
+  }
+
+  const objectWithActor = Object.assign(object, {
+    actor: objectActorUrl
+  })
+
+  const data = await announceActivityData(url, byAccount, objectWithActor)
+  return broadcastToFollowers(data, [ byAccount ], t)
 }
 
 async function sendVideoAbuse (
@@ -95,15 +116,17 @@ export {
   sendDeleteAccount,
   sendAccept,
   sendFollow,
-  sendVideoAbuse
+  sendVideoAbuse,
+  sendAnnounce
 }
 
 // ---------------------------------------------------------------------------
 
-async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t: Sequelize.Transaction) {
-  const result = await db.AccountFollow.listAcceptedFollowerUrlsForApi(fromAccount.id)
+async function broadcastToFollowers (data: any, toAccountFollowers: AccountInstance[], t: Sequelize.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.')
+    logger.info('Not broadcast because of 0 followers for %s.', toAccountFollowerIds.join(', '))
     return
   }
 
@@ -186,6 +209,17 @@ async function addActivityData (url: string, byAccount: AccountInstance, target:
   return buildSignedActivity(byAccount, base)
 }
 
+async function announceActivityData (url: string, byAccount: AccountInstance, object: any) {
+  const base = {
+    type: 'Announce',
+    id: url,
+    actor: byAccount.url,
+    object
+  }
+
+  return buildSignedActivity(byAccount, base)
+}
+
 async function followActivityData (url: string, byAccount: AccountInstance) {
   const base = {
     type: 'Follow',
index f6d9627a5322fbf29d77351d0647b6d5fa1a98f6..6443899d380cae912d3696fa0429ce0f5830987f 100644 (file)
@@ -6,6 +6,7 @@ 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'
 
 async function process (data: TranscodingJobPayload, jobId: number) {
   const video = await db.Video.loadByUUIDAndPopulateAccountAndServerAndTags(data.videoUUID)
@@ -37,6 +38,7 @@ async function onSuccess (jobId: number, video: VideoInstance, jobScheduler: Job
 
   // Now we'll add the video's meta data to our followers
   await sendAddVideo(video, undefined)
+  await shareVideoByServer(video, undefined)
 
   const originalFileHeight = await videoDatabase.getOriginalFileHeight()
 
index 80303fb838c62320daca6f5244517e49bd57cb74..e69ec062f2ad7bd442de775826570991f51f78d2 100644 (file)
@@ -5,7 +5,7 @@ import { logger } from '../helpers'
 import { AccountInstance } from '../models'
 import { VideoChannelCreate } from '../../shared/models'
 import { sendCreateVideoChannel } from './activitypub/send-request'
-import { getActivityPubUrl } from '../helpers/activitypub'
+import { getActivityPubUrl, shareVideoChannelByServer } from '../helpers/activitypub'
 
 async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountInstance, t: Sequelize.Transaction) {
   const videoChannelData = {
@@ -25,7 +25,8 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account
   // Do not forget to add Account information to the created video channel
   videoChannelCreated.Account = account
 
-  sendCreateVideoChannel(videoChannelCreated, t)
+  await sendCreateVideoChannel(videoChannelCreated, t)
+  await shareVideoChannelByServer(videoChannelCreated, t)
 
   return videoChannelCreated
 }
index 54baf45ed4b857b76bafd6345fdb74461f87b3a7..21fda98cecc3bbc681df420bbf7acea412f1806f 100644 (file)
@@ -10,8 +10,9 @@ export namespace AccountFollowMethods {
   export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> >
   export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> >
 
-  export type ListAcceptedFollowerUrlsForApi = (id: number, start?: number, count?: number) => Promise< ResultList<string> >
-  export type ListAcceptedFollowingUrlsForApi = (id: number, start?: number, count?: number) => Promise< ResultList<string> >
+  export type ListAcceptedFollowerUrlsForApi = (accountId: number[], start?: number, count?: number) => Promise< ResultList<string> >
+  export type ListAcceptedFollowingUrlsForApi = (accountId: number[], start?: number, count?: number) => Promise< ResultList<string> >
+  export type ListAcceptedFollowerSharedInboxUrls = (accountId: number[]) => Promise< ResultList<string> >
 }
 
 export interface AccountFollowClass {
@@ -21,6 +22,7 @@ export interface AccountFollowClass {
 
   listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi
   listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi
+  listAcceptedFollowerSharedInboxUrls: AccountFollowMethods.ListAcceptedFollowerSharedInboxUrls
 }
 
 export interface AccountFollowAttributes {
index f457e43e9d28e6b8be764b0fad7f9a1eb9299c86..8a7474c9ddacefc0b1a805706ea5ac5dabe40220 100644 (file)
@@ -11,6 +11,7 @@ let listFollowingForApi: AccountFollowMethods.ListFollowingForApi
 let listFollowersForApi: AccountFollowMethods.ListFollowersForApi
 let listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi
 let listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi
+let listAcceptedFollowerSharedInboxUrls: AccountFollowMethods.ListAcceptedFollowerSharedInboxUrls
 
 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
   AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow',
@@ -42,7 +43,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     listFollowingForApi,
     listFollowersForApi,
     listAcceptedFollowerUrlsForApi,
-    listAcceptedFollowingUrlsForApi
+    listAcceptedFollowingUrlsForApi,
+    listAcceptedFollowerSharedInboxUrls
   ]
   addMethodsToModel(AccountFollow, classMethods)
 
@@ -146,17 +148,27 @@ listFollowersForApi = function (id: number, start: number, count: number, sort:
   })
 }
 
-listAcceptedFollowerUrlsForApi = function (accountId: number, start?: number, count?: number) {
-  return createListAcceptedFollowForApiQuery('followers', accountId, start, count)
+listAcceptedFollowerUrlsForApi = function (accountIds: number[], start?: number, count?: number) {
+  return createListAcceptedFollowForApiQuery('followers', accountIds, start, count)
 }
 
-listAcceptedFollowingUrlsForApi = function (accountId: number, start?: number, count?: number) {
-  return createListAcceptedFollowForApiQuery('following', accountId, start, count)
+listAcceptedFollowerSharedInboxUrls = function (accountIds: number[]) {
+  return createListAcceptedFollowForApiQuery('followers', accountIds, undefined, undefined, 'sharedInboxUrl')
+}
+
+listAcceptedFollowingUrlsForApi = function (accountIds: number[], start?: number, count?: number) {
+  return createListAcceptedFollowForApiQuery('following', accountIds, start, count)
 }
 
 // ------------------------------ UTILS ------------------------------
 
-async function createListAcceptedFollowForApiQuery (type: 'followers' | 'following', accountId: number, start?: number, count?: number) {
+async function createListAcceptedFollowForApiQuery (
+  type: 'followers' | 'following',
+  accountIds: number[],
+  start?: number,
+  count?: number,
+  columnUrl = 'url'
+) {
   let firstJoin: string
   let secondJoin: string
 
@@ -168,20 +180,20 @@ async function createListAcceptedFollowForApiQuery (type: 'followers' | 'followi
     secondJoin = 'targetAccountId'
   }
 
-  const selections = [ '"Follows"."url" AS "url"', 'COUNT(*) AS "total"' ]
+  const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ]
   const tasks: Promise<any>[] = []
 
   for (const selection of selections) {
     let query = 'SELECT ' + selection + ' FROM "Accounts" ' +
       'INNER JOIN "AccountFollows" ON "AccountFollows"."' + firstJoin + '" = "Accounts"."id" ' +
       'INNER JOIN "Accounts" AS "Follows" ON "AccountFollows"."' + secondJoin + '" = "Follows"."id" ' +
-      'WHERE "Accounts"."id" = $accountId AND "AccountFollows"."state" = \'accepted\' '
+      'WHERE "Accounts"."id" IN ($accountIds) AND "AccountFollows"."state" = \'accepted\' '
 
     if (start !== undefined) query += 'LIMIT ' + start
     if (count !== undefined) query += ', ' + count
 
     const options = {
-      bind: { accountId },
+      bind: { accountIds: accountIds.join(',') },
       type: Sequelize.QueryTypes.SELECT
     }
     tasks.push(AccountFollow['sequelize'].query(query, options))
index 3cb4a33b907cd90359666efc77cd7099ec288593..1f4604f1dc5b038177a0d19a8f6c6a9a81c52663 100644 (file)
@@ -153,8 +153,8 @@ toActivityPubObject = function (this: VideoChannelInstance) {
     uuid: this.uuid,
     content: this.description,
     name: this.name,
-    published: this.createdAt,
-    updated: this.updatedAt
+    published: this.createdAt.toISOString(),
+    updated: this.updatedAt.toISOString()
   }
 
   return json
index 480e54276dcc85a5850df47d0e26d44000eaf340..64ee7ae344b94e53e06180cffb9b56cb57eb962c 100644 (file)
@@ -558,7 +558,7 @@ toActivityPubObject = function (this: VideoInstance) {
   for (const file of this.VideoFiles) {
     url.push({
       type: 'Link',
-      mimeType: 'video/' + file.extname,
+      mimeType: 'video/' + file.extname.replace('.', ''),
       url: getVideoFileUrl(this, file, baseUrlHttp),
       width: file.resolution,
       size: file.size
@@ -601,8 +601,8 @@ toActivityPubObject = function (this: VideoInstance) {
     },
     views: this.views,
     nsfw: this.nsfw,
-    published: this.createdAt,
-    updated: this.updatedAt,
+    published: this.createdAt.toISOString(),
+    updated: this.updatedAt.toISOString(),
     mediaType: 'text/markdown',
     content: this.getTruncatedDescription(),
     icon: {
index 468e1535ea04e7c7118156ffc45c31ca31d773f0..c9325b5dfae7922dc229e1eb53216a1cfab6c491 100644 (file)
@@ -4,7 +4,7 @@ export interface VideoChannelObject {
   name: string
   content: string
   uuid: string
-  published: Date
-  updated: Date
+  published: string
+  updated: string
   actor?: string
 }
index 99e7157b8c1cd3324c9007975e0d038a8cb32186..ae8f807c8a4f7af49a08319c7858b35d13d1188e 100644 (file)
@@ -17,8 +17,8 @@ export interface VideoTorrentObject {
   language: ActivityIdentifierObject
   views: number
   nsfw: boolean
-  published: Date
-  updated: Date
+  published: string
+  updated: string
   mediaType: 'text/markdown'
   content: string
   icon: ActivityIconObject