Server: Bulk update videos support field
authorChocobozzz <me@florianbigard.com>
Fri, 31 May 2019 14:30:11 +0000 (16:30 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 31 May 2019 14:44:46 +0000 (16:44 +0200)
server/controllers/api/video-channel.ts
server/lib/activitypub/process/process-create.ts
server/lib/video-channel.ts
server/middlewares/validators/videos/video-channels.ts
server/models/video/video.ts
server/tests/api/check-params/video-channels.ts
server/tests/api/videos/video-channels.ts
shared/extra-utils/videos/video-channels.ts
shared/extra-utils/videos/videos.ts
shared/models/videos/channel/video-channel-update.model.ts
support/doc/api/openapi.yaml

index c98a39be28ac6677aa37d68e086bb883be827a8a..81a03a62bc85838d8409d5ea13bbf7d9515b4eda 100644 (file)
@@ -19,7 +19,7 @@ import { VideoChannelModel } from '../../models/video/video-channel'
 import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators'
 import { sendUpdateActor } from '../../lib/activitypub/send'
 import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
-import { createVideoChannel } from '../../lib/video-channel'
+import { createVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
 import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
 import { setAsyncActorKeys } from '../../lib/activitypub'
 import { AccountModel } from '../../models/account/account'
@@ -160,6 +160,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
   const videoChannelFieldsSave = videoChannelInstance.toJSON()
   const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())
   const videoChannelInfoToUpdate = req.body as VideoChannelUpdate
+  let doBulkVideoUpdate = false
 
   try {
     await sequelizeTypescript.transaction(async t => {
@@ -167,9 +168,18 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
         transaction: t
       }
 
-      if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.displayName)
-      if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description)
-      if (videoChannelInfoToUpdate.support !== undefined) videoChannelInstance.set('support', videoChannelInfoToUpdate.support)
+      if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName
+      if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description
+
+      if (videoChannelInfoToUpdate.support !== undefined) {
+        const oldSupportField = videoChannelInstance.support
+        videoChannelInstance.support = videoChannelInfoToUpdate.support
+
+        if (videoChannelInfoToUpdate.bulkVideosSupportUpdate === true && oldSupportField !== videoChannelInfoToUpdate.support) {
+          doBulkVideoUpdate = true
+          await VideoModel.bulkUpdateSupportField(videoChannelInstance, t)
+        }
+      }
 
       const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions)
       await sendUpdateActor(videoChannelInstanceUpdated, t)
@@ -179,6 +189,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
         new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()),
         oldVideoChannelAuditKeys
       )
+
       logger.info('Video channel %s updated.', videoChannelInstance.Actor.url)
     })
   } catch (err) {
@@ -192,7 +203,12 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
     throw err
   }
 
-  return res.type('json').status(204).end()
+  res.type('json').status(204).end()
+
+  // Don't process in a transaction, and after the response because it could be long
+  if (doBulkVideoUpdate) {
+    await federateAllVideosOfChannel(videoChannelInstance)
+  }
 }
 
 async function removeVideoChannel (req: express.Request, res: express.Response) {
index 3b6eb45c4f007fd13b5562db62937fd652848c58..daf8465136b5d29f11773cb9173b950b63dc70c2 100644 (file)
@@ -87,6 +87,7 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: Act
       commentObject.inReplyTo,
       { err }
     )
+    return
   }
 
   const { comment, created } = await addVideoComment(video, commentObject.id)
index 0fe95ca0986d41fc863ea6afb2f8f60daee37751..ee0482c3612919b3715eb64aca12e59aa178ef0d 100644 (file)
@@ -3,7 +3,8 @@ import * as uuidv4 from 'uuid/v4'
 import { VideoChannelCreate } from '../../shared/models'
 import { AccountModel } from '../models/account/account'
 import { VideoChannelModel } from '../models/video/video-channel'
-import { buildActorInstance, getVideoChannelActivityPubUrl } from './activitypub'
+import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUrl } from './activitypub'
+import { VideoModel } from '../models/video/video'
 
 async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountModel, t: Sequelize.Transaction) {
   const uuid = uuidv4()
@@ -33,8 +34,19 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account
   return videoChannelCreated
 }
 
+async function federateAllVideosOfChannel (videoChannel: VideoChannelModel) {
+  const videoIds = await VideoModel.getAllIdsFromChannel(videoChannel)
+
+  for (const videoId of videoIds) {
+    const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
+
+    await federateVideoIfNeeded(video, false)
+  }
+}
+
 // ---------------------------------------------------------------------------
 
 export {
-  createVideoChannel
+  createVideoChannel,
+  federateAllVideosOfChannel
 }
index 4b26f0bc408c54a0a98b4245a7929e36552a5819..f5a59cacbbcbdd6da6e1461d7003b6452eb7048f 100644 (file)
@@ -14,6 +14,7 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
 import { areValidationErrors } from '../utils'
 import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
 import { ActorModel } from '../../../models/activitypub/actor'
+import { isBooleanValid } from '../../../helpers/custom-validators/misc'
 
 const videoChannelsAddValidator = [
   body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
@@ -40,9 +41,18 @@ const videoChannelsAddValidator = [
 
 const videoChannelsUpdateValidator = [
   param('nameWithHost').exists().withMessage('Should have an video channel name with host'),
-  body('displayName').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
-  body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'),
-  body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'),
+  body('displayName')
+    .optional()
+    .custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
+  body('description')
+    .optional()
+    .custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'),
+  body('support')
+    .optional()
+    .custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'),
+  body('bulkVideosSupportUpdate')
+    .optional()
+    .custom(isBooleanValid).withMessage('Should have a valid bulkVideosSupportUpdate boolean field'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body })
index c0a7892a430f12f354671b6919c0e9edb54d0d07..eccf0a4fa1c8ad923f14cefa30f5633f1baf1aa6 100644 (file)
@@ -1515,6 +1515,29 @@ export class VideoModel extends Model<VideoModel> {
                      .then(results => results.length === 1)
   }
 
+  static bulkUpdateSupportField (videoChannel: VideoChannelModel, t: Transaction) {
+    const options = {
+      where: {
+        channelId: videoChannel.id
+      },
+      transaction: t
+    }
+
+    return VideoModel.update({ support: videoChannel.support }, options)
+  }
+
+  static getAllIdsFromChannel (videoChannel: VideoChannelModel) {
+    const query = {
+      attributes: [ 'id' ],
+      where: {
+        channelId: videoChannel.id
+      }
+    }
+
+    return VideoModel.findAll(query)
+      .then(videos => videos.map(v => v.id))
+  }
+
   // threshold corresponds to how many video the field should have to be returned
   static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
     const serverActor = await getServerActor()
index ff04f6b03f1727d9e1fc25298974480ecc044105..de88298d164360531ada329dedb3895848114b4a 100644 (file)
@@ -24,6 +24,7 @@ import {
   checkBadStartPagination
 } from '../../../../shared/extra-utils/requests/check-api-params'
 import { join } from 'path'
+import { VideoChannelUpdate } from '../../../../shared/models/videos'
 
 const expect = chai.expect
 
@@ -169,9 +170,11 @@ describe('Test video channels API validator', function () {
   })
 
   describe('When updating a video channel', function () {
-    const baseCorrectParams = {
+    const baseCorrectParams: VideoChannelUpdate = {
       displayName: 'hello',
-      description: 'super description'
+      description: 'super description',
+      support: 'toto',
+      bulkVideosSupportUpdate: false
     }
     let path: string
 
@@ -214,6 +217,11 @@ describe('Test video channels API validator', function () {
       await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
     })
 
+    it('Should fail with a bad bulkVideosSupportUpdate field', async function () {
+      const fields = immutableAssign(baseCorrectParams, { bulkVideosSupportUpdate: 'super' })
+      await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+    })
+
     it('Should succeed with the correct parameters', async function () {
       await makePutBodyRequest({
         url: server.url,
index 2d298dd3f522a289be2709639e7efca69f758454..4f600cae8eefb11ecd2be702734aeb26ca0810cb 100644 (file)
@@ -2,12 +2,12 @@
 
 import * as chai from 'chai'
 import 'mocha'
-import { User, Video, VideoChannel } from '../../../../shared/index'
+import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index'
 import {
   cleanupTests,
   createUser,
   doubleFollow,
-  flushAndRunMultipleServers,
+  flushAndRunMultipleServers, getVideo,
   getVideoChannelVideos,
   testImage,
   updateVideo,
@@ -79,7 +79,8 @@ describe('Test video channels', function () {
 
     // The channel is 1 is propagated to servers 2
     {
-      const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'my video name', channelId: secondVideoChannelId })
+      const videoAttributesArg = { name: 'my video name', channelId: secondVideoChannelId, support: 'video support field' }
+      const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAttributesArg)
       videoUUID = res.body.video.uuid
     }
 
@@ -201,12 +202,12 @@ describe('Test video channels', function () {
   })
 
   it('Should update video channel', async function () {
-    this.timeout(5000)
+    this.timeout(15000)
 
     const videoChannelAttributes = {
       displayName: 'video channel updated',
       description: 'video channel description updated',
-      support: 'video channel support text updated'
+      support: 'support updated'
     }
 
     await updateVideoChannel(servers[0].url, servers[0].accessToken, 'second_video_channel', videoChannelAttributes)
@@ -224,7 +225,36 @@ describe('Test video channels', function () {
       expect(res.body.data[0].name).to.equal('second_video_channel')
       expect(res.body.data[0].displayName).to.equal('video channel updated')
       expect(res.body.data[0].description).to.equal('video channel description updated')
-      expect(res.body.data[0].support).to.equal('video channel support text updated')
+      expect(res.body.data[0].support).to.equal('support updated')
+    }
+  })
+
+  it('Should not have updated the video support field', async function () {
+    for (const server of servers) {
+      const res = await getVideo(server.url, videoUUID)
+      const video: VideoDetails = res.body
+
+      expect(video.support).to.equal('video support field')
+    }
+  })
+
+  it('Should update the channel support field and update videos too', async function () {
+    this.timeout(35000)
+
+    const videoChannelAttributes = {
+      support: 'video channel support text updated',
+      bulkVideosSupportUpdate: true
+    }
+
+    await updateVideoChannel(servers[0].url, servers[0].accessToken, 'second_video_channel', videoChannelAttributes)
+
+    await waitJobs(servers)
+
+    for (const server of servers) {
+      const res = await getVideo(server.url, videoUUID)
+      const video: VideoDetails = res.body
+
+      expect(video.support).to.equal(videoChannelAttributes.support)
     }
   })
 
index b4755b486cf354d5d984cf4091c3ec0312e8139e..3e79cf15a4fbcaed264df3ac00b7a8928ec3b354 100644 (file)
@@ -74,12 +74,13 @@ function updateVideoChannel (
   attributes: VideoChannelUpdate,
   expectedStatus = 204
 ) {
-  const body = {}
+  const body: any = {}
   const path = '/api/v1/video-channels/' + channelName
 
-  if (attributes.displayName) body['displayName'] = attributes.displayName
-  if (attributes.description) body['description'] = attributes.description
-  if (attributes.support) body['support'] = attributes.support
+  if (attributes.displayName) body.displayName = attributes.displayName
+  if (attributes.description) body.description = attributes.description
+  if (attributes.support) body.support = attributes.support
+  if (attributes.bulkVideosSupportUpdate) body.bulkVideosSupportUpdate = attributes.bulkVideosSupportUpdate
 
   return request(url)
     .put(path)
index a4ca43f261c88406e2cb464a34aa1ea59957b3a0..debaaf9a7df7544e32f18ed31b96e02dd2bfbb86 100644 (file)
@@ -355,6 +355,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
               .set('Accept', 'application/json')
               .set('Authorization', 'Bearer ' + accessToken)
               .field('name', attributes.name)
+              .field('support', attributes.support)
               .field('nsfw', JSON.stringify(attributes.nsfw))
               .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
               .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
index 3626ce8a900991a2183036e98237df8ad734a9ff..8dde9188bb2bac1ba8de487f135e70f0f919ab61 100644 (file)
@@ -1,5 +1,7 @@
 export interface VideoChannelUpdate {
-  displayName: string
+  displayName?: string
   description?: string
   support?: string
+
+  bulkVideosSupportUpdate?: boolean
 }
index a8a064fd0bf6915fc6fd2e9233ba84c2ee5047b2..f3f565694d599c2affd33df02dc71bb9dfa9fac9 100644 (file)
@@ -1322,7 +1322,10 @@ paths:
         '204':
           $ref: '#/paths/~1users~1me/put/responses/204'
       requestBody:
-        $ref: '#/components/requestBodies/VideoChannelInput'
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/VideoChannelCreate'
   '/video-channels/{channelHandle}':
     get:
       summary: Get a video channel by its id
@@ -1349,7 +1352,10 @@ paths:
         '204':
           $ref: '#/paths/~1users~1me/put/responses/204'
       requestBody:
-        $ref: '#/components/requestBodies/VideoChannelInput'
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/VideoChannelUpdate'
     delete:
       summary: Delete a video channel by its id
       security:
@@ -1775,12 +1781,6 @@ components:
         type: array
         items:
           type: string
-  requestBodies:
-    VideoChannelInput:
-      content:
-        application/json:
-          schema:
-            $ref: '#/components/schemas/VideoChannelInput'
   securitySchemes:
     OAuth2:
       description: >
@@ -2294,10 +2294,28 @@ components:
         - username
         - password
         - email
-    VideoChannelInput:
+    VideoChannelCreate:
       properties:
         name:
           type: string
+        displayName:
+          type: string
         description:
           type: string
+        support:
+          type: string
+      required:
+        - name
+        - displayName
+    VideoChannelUpdate:
+      properties:
+        displayName:
+          type: string
+        description:
+          type: string
+        support:
+          type: string
+        bulkVideosSupportUpdate:
+          type: boolean
+          description: 'Update all videos support field of this channel'