Add internal privacy mode
authorChocobozzz <me@florianbigard.com>
Thu, 12 Dec 2019 14:47:47 +0000 (15:47 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 12 Dec 2019 15:51:59 +0000 (16:51 +0100)
19 files changed:
client/src/app/shared/video/modals/video-download.component.ts
client/src/app/shared/video/video.service.ts
server/controllers/api/videos/index.ts
server/controllers/api/videos/ownership.ts
server/helpers/custom-validators/videos.ts
server/initializers/constants.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/send-update.ts
server/lib/activitypub/share.ts
server/lib/activitypub/videos.ts
server/lib/client-html.ts
server/lib/schedulers/update-videos-scheduler.ts
server/middlewares/validators/videos/videos.ts
server/models/account/user.ts
server/models/video/schedule-video-update.ts
server/models/video/video.ts
server/tests/api/videos/video-privacy.ts
shared/models/videos/video-privacy.enum.ts
shared/models/videos/video-schedule-update.model.ts

index 5849ee4586fc5093247de431772b4a11f8412419..71274008648b92c858dba599754091ae89968f65 100644 (file)
@@ -59,7 +59,7 @@ export class VideoDownloadComponent {
       return
     }
 
-    const suffix = this.video.privacy.id === VideoPrivacy.PRIVATE
+    const suffix = this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL
       ? '?access_token=' + this.auth.getAccessToken()
       : ''
 
index 45366e3e305039d0ad81377025e715c444f9c92c..b0fa559664632fb647fbdfeae653e94b2c3215e6 100644 (file)
@@ -332,18 +332,26 @@ export class VideoService implements VideosProvider {
   }
 
   explainedPrivacyLabels (privacies: VideoConstant<VideoPrivacy>[]) {
-    const newPrivacies = privacies.slice()
-
-    const privatePrivacy = newPrivacies.find(p => p.id === VideoPrivacy.PRIVATE)
-    if (privatePrivacy) privatePrivacy.label = this.i18n('Only I can see this video')
-
-    const unlistedPrivacy = newPrivacies.find(p => p.id === VideoPrivacy.UNLISTED)
-    if (unlistedPrivacy) unlistedPrivacy.label = this.i18n('Only people with the private link can see this video')
-
-    const publicPrivacy = newPrivacies.find(p => p.id === VideoPrivacy.PUBLIC)
-    if (publicPrivacy) publicPrivacy.label = this.i18n('Anyone can see this video')
+    const base = [
+      {
+        id: VideoPrivacy.PRIVATE,
+        label: this.i18n('Only I can see this video')
+      },
+      {
+        id: VideoPrivacy.UNLISTED,
+        label: this.i18n('Only people with the private link can see this video')
+      },
+      {
+        id: VideoPrivacy.PUBLIC,
+        label: this.i18n('Anyone can see this video')
+      },
+      {
+        id: VideoPrivacy.INTERNAL,
+        label: this.i18n('Only users of this instance can see this video')
+      }
+    ]
 
-    return privacies
+    return base.filter(o => !!privacies.find(p => p.id === o.id))
   }
 
   private setVideoRate (id: number, rateType: UserVideoRateType) {
index 337795541c309821291614944faa4dbf40825808..35f0b3152934e97cdbda3e4c2fba5220e9ea3fab 100644 (file)
@@ -326,9 +326,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
   const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
   const videoInfoToUpdate: VideoUpdate = req.body
 
-  const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
-  const wasNotPrivateVideo = videoInstance.privacy !== VideoPrivacy.PRIVATE
-  const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED
+  const wasConfidentialVideo = videoInstance.isConfidential()
+  const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation()
 
   // Process thumbnail or create it from the video
   const thumbnailModel = req.files && req.files['thumbnailfile']
@@ -359,17 +358,15 @@ async function updateVideo (req: express.Request, res: express.Response) {
         videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt)
       }
 
+      let isNewVideo = false
       if (videoInfoToUpdate.privacy !== undefined) {
-        const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
-        videoInstance.privacy = newPrivacy
+        isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
 
-        // The video was private, and is not anymore -> publish it
-        if (wasPrivateVideo === true && newPrivacy !== VideoPrivacy.PRIVATE) {
-          videoInstance.publishedAt = new Date()
-        }
+        const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
+        videoInstance.setPrivacy(newPrivacy)
 
-        // The video was not private, but now it is -> we need to unfederate it
-        if (wasNotPrivateVideo === true && newPrivacy === VideoPrivacy.PRIVATE) {
+        // Unfederate the video if the new privacy is not compatible with federation
+        if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
           await VideoModel.sendDelete(videoInstance, { transaction: t })
         }
       }
@@ -392,7 +389,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
         await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
         videoInstanceUpdated.VideoChannel = res.locals.videoChannel
 
-        if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
+        if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
       }
 
       // Schedule an update in the future?
@@ -414,7 +411,6 @@ async function updateVideo (req: express.Request, res: express.Response) {
         transaction: t
       })
 
-      const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
       await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
 
       auditLogger.update(
@@ -427,7 +423,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
       return videoInstanceUpdated
     })
 
-    if (wasUnlistedVideo || wasPrivateVideo) {
+    if (wasConfidentialVideo) {
       Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
     }
 
index abb34082e893c2d4382c74b7d3b947ed7f9bb29a..41d7cdc43b50b77b62e5f32a590f1848bd2caec6 100644 (file)
@@ -12,7 +12,7 @@ import {
   videosTerminateChangeOwnershipValidator
 } from '../../../middlewares'
 import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
-import { VideoChangeOwnershipStatus, VideoPrivacy, VideoState } from '../../../../shared/models/videos'
+import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos'
 import { VideoChannelModel } from '../../../models/video/video-channel'
 import { getFormattedObjects } from '../../../helpers/utils'
 import { changeVideoChannelShare } from '../../../lib/activitypub'
@@ -111,7 +111,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
     const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight
     targetVideoUpdated.VideoChannel = channel
 
-    if (targetVideoUpdated.privacy !== VideoPrivacy.PRIVATE && targetVideoUpdated.state === VideoState.PUBLISHED) {
+    if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) {
       await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t)
       await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor)
     }
index e92ef9b92588e7c6054d6b1c089b6206d044b7e1..ff93f2e99295a169895d321e36b39706e190895e 100644 (file)
@@ -102,7 +102,7 @@ function isVideoPrivacyValid (value: number) {
 }
 
 function isScheduleVideoUpdatePrivacyValid (value: number) {
-  return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC
+  return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL
 }
 
 function isVideoOriginallyPublishedAtValid (value: string | null) {
index bdabe7f6605667482b43ff1152e4a929ad58c95d..d0cf4d5deca5a3af11b383c7da53642b78b5286c 100644 (file)
@@ -353,7 +353,8 @@ let VIDEO_LANGUAGES: { [id: string]: string } = {}
 const VIDEO_PRIVACIES = {
   [ VideoPrivacy.PUBLIC ]: 'Public',
   [ VideoPrivacy.UNLISTED ]: 'Unlisted',
-  [ VideoPrivacy.PRIVATE ]: 'Private'
+  [ VideoPrivacy.PRIVATE ]: 'Private',
+  [ VideoPrivacy.INTERNAL ]: 'Internal'
 }
 
 const VIDEO_STATES = {
index edbc14a73e73fb66466bb5405ab0ad47d9879bad..1709d834841c8f759b3d8e6f7f9775a5f152bb71 100644 (file)
@@ -18,7 +18,7 @@ import {
 } from '../../../typings/models'
 
 async function sendCreateVideo (video: MVideoAP, t: Transaction) {
-  if (video.privacy === VideoPrivacy.PRIVATE) return undefined
+  if (!video.hasPrivacyForFederation()) return undefined
 
   logger.info('Creating job to send video creation of %s.', video.url)
 
index 44e0e1161ed239014838a0797a22bf92d2e1bf53..cb14b8dbfeffb08076cf6701b18ea9eb8a6a882f 100644 (file)
@@ -25,7 +25,7 @@ import {
 async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
   const video = videoArg as MVideoAP
 
-  if (video.privacy === VideoPrivacy.PRIVATE) return undefined
+  if (!video.hasPrivacyForFederation()) return undefined
 
   logger.info('Creating job to update video %s.', video.url)
 
index fdca9bed75c3e14c487f1af3164f0544cce99cac..e847c4b7d265c35ced580c13ea7e0eb33cd9f7c0 100644 (file)
@@ -1,5 +1,4 @@
 import { Transaction } from 'sequelize'
-import { VideoPrivacy } from '../../../shared/models/videos'
 import { getServerActor } from '../../helpers/utils'
 import { VideoShareModel } from '../../models/video/video-share'
 import { sendUndoAnnounce, sendVideoAnnounce } from './send'
@@ -10,10 +9,10 @@ import { getOrCreateActorAndServerAndModel } from './actor'
 import { logger } from '../../helpers/logger'
 import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
 import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
-import { MChannelActor, MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video'
+import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video'
 
 async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
-  if (video.privacy === VideoPrivacy.PRIVATE) return undefined
+  if (!video.hasPrivacyForFederation()) return undefined
 
   return Promise.all([
     shareByServer(video, t),
index d80173e0315a118a9db6ee9ba1738a4ce3614438..2fb1f8d4982f8082257766ab5c198381420295a1 100644 (file)
@@ -79,7 +79,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
     // Check this is not a blacklisted video, or unfederated blacklisted video
     (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
     // Check the video is public/unlisted and published
-    video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED
+    video.hasPrivacyForFederation() && video.state === VideoState.PUBLISHED
   ) {
     // Fetch more attributes that we will need to serialize in AP object
     if (isArray(video.VideoCaptions) === false) {
index a1f4ae85856227ba6cf9519058903f43096fb601..42a30f84f25e76f112972202225dbbd71109a814 100644 (file)
@@ -46,7 +46,7 @@ export class ClientHtml {
     ])
 
     // Let Angular application handle errors
-    if (!video || video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
+    if (!video || video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL || video.VideoBlacklist) {
       return ClientHtml.getIndexHTML(req, res)
     }
 
index 293bba91f671e74fb028854b5a8b21050a8d9397..350a335d379517d1d7f1a630c8037fe36f3059e1 100644 (file)
@@ -35,16 +35,14 @@ export class UpdateVideosScheduler extends AbstractScheduler {
         logger.info('Executing scheduled video update on %s.', video.uuid)
 
         if (schedule.privacy) {
-          const oldPrivacy = video.privacy
-          const isNewVideo = oldPrivacy === VideoPrivacy.PRIVATE
-
-          video.privacy = schedule.privacy
-          if (isNewVideo === true) video.publishedAt = new Date()
+          const wasConfidentialVideo = video.isConfidential()
+          const isNewVideo = video.isNewVideo(schedule.privacy)
 
+          video.setPrivacy(schedule.privacy)
           await video.save({ transaction: t })
           await federateVideoIfNeeded(video, isNewVideo, t)
 
-          if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) {
+          if (wasConfidentialVideo) {
             const videoToPublish: MVideoFullLight = Object.assign(video, { ScheduleVideoUpdate: schedule, UserVideoHistories: [] })
             publishedVideos.push(videoToPublish)
           }
index ab984d84a98cdbbea881ded9a1d98d29b21ae036..5e0182cc341389244e37dfb8e852d75ea43ef6c5 100644 (file)
@@ -161,18 +161,15 @@ const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-
       const videoAll = video as MVideoFullLight
 
       // Video private or blacklisted
-      if (video.privacy === VideoPrivacy.PRIVATE || videoAll.VideoBlacklist) {
+      if (videoAll.requiresAuth()) {
         await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
 
         const user = res.locals.oauth ? res.locals.oauth.token.User : null
 
         // Only the owner or a user that have blacklist rights can see the video
-        if (
-          !user ||
-          (videoAll.VideoChannel && videoAll.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
-        ) {
+        if (!user || !user.canGetVideo(videoAll)) {
           return res.status(403)
-                    .json({ error: 'Cannot get this private or blacklisted video.' })
+                    .json({ error: 'Cannot get this private/internal or blacklisted video.' })
         }
 
         return next()
index 38c6d474aa2e6916ce7170bcf28a58512a214666..522ea33109189b4a43ffd21f105217b775b78722 100644 (file)
@@ -19,7 +19,7 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
+import { hasUserRight, USER_ROLE_LABELS, UserRight, VideoPrivacy } from '../../../shared'
 import { User, UserRole } from '../../../shared/models/users'
 import {
   isNoInstanceConfigWarningModal,
@@ -63,7 +63,7 @@ import {
   MUserFormattable,
   MUserId,
   MUserNotifSettingChannelDefault,
-  MUserWithNotificationSetting
+  MUserWithNotificationSetting, MVideoFullLight
 } from '@server/typings/models'
 
 enum ScopeNames {
@@ -575,6 +575,20 @@ export class UserModel extends Model<UserModel> {
                     .then(u => u.map(u => u.username))
   }
 
+  canGetVideo (video: MVideoFullLight) {
+    if (video.privacy === VideoPrivacy.INTERNAL) return true
+
+    if (video.privacy === VideoPrivacy.PRIVATE) {
+      return video.VideoChannel && video.VideoChannel.Account.userId === this.id
+    }
+
+    if (video.isBlacklisted()) {
+      return this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
+    }
+
+    return false
+  }
+
   hasRight (right: UserRight) {
     return hasUserRight(this.role, right)
   }
index eefc10f14fe095f73703e1a518e2bbaf20d040ad..00b7f55247dd034738ca5defa3589c8f66c9fc2c 100644 (file)
@@ -26,7 +26,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
   @AllowNull(true)
   @Default(null)
   @Column
-  privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
+  privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED | VideoPrivacy.INTERNAL
 
   @CreatedAt
   createdAt: Date
index af6fae0b64892d0f6b2299b394a1d04a8b700fc4..7e18af497fce86694d34fad5aa999ad2e68e6b8d 100644 (file)
@@ -348,9 +348,8 @@ export type AvailableForListIDsOptions = {
 
     // Only list public/published videos
     if (!options.filter || options.filter !== 'all-local') {
-      const privacyWhere = {
-        // Always list public videos
-        privacy: VideoPrivacy.PUBLIC,
+
+      const publishWhere = {
         // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
         [ Op.or ]: [
           {
@@ -364,8 +363,26 @@ export type AvailableForListIDsOptions = {
           }
         ]
       }
+      whereAnd.push(publishWhere)
 
-      whereAnd.push(privacyWhere)
+      // List internal videos if the user is logged in
+      if (options.user) {
+        const privacyWhere = {
+          [Op.or]: [
+            {
+              privacy: VideoPrivacy.INTERNAL
+            },
+            {
+              privacy: VideoPrivacy.PUBLIC
+            }
+          ]
+        }
+
+        whereAnd.push(privacyWhere)
+      } else { // Or only public videos
+        const privacyWhere = { privacy: VideoPrivacy.PUBLIC }
+        whereAnd.push(privacyWhere)
+      }
     }
 
     if (options.videoPlaylistId) {
@@ -1773,6 +1790,10 @@ export class VideoModel extends Model<VideoModel> {
     }
   }
 
+  private static isPrivacyForFederation (privacy: VideoPrivacy) {
+    return privacy === VideoPrivacy.PUBLIC || privacy === VideoPrivacy.UNLISTED
+  }
+
   static getCategoryLabel (id: number) {
     return VIDEO_CATEGORIES[ id ] || 'Misc'
   }
@@ -1980,12 +2001,38 @@ export class VideoModel extends Model<VideoModel> {
     return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
   }
 
+  hasPrivacyForFederation () {
+    return VideoModel.isPrivacyForFederation(this.privacy)
+  }
+
+  isNewVideo (newPrivacy: VideoPrivacy) {
+    return this.hasPrivacyForFederation() === false && VideoModel.isPrivacyForFederation(newPrivacy) === true
+  }
+
   setAsRefreshed () {
     this.changed('updatedAt', true)
 
     return this.save()
   }
 
+  requiresAuth () {
+    return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist
+  }
+
+  setPrivacy (newPrivacy: VideoPrivacy) {
+    if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) {
+      this.publishedAt = new Date()
+    }
+
+    this.privacy = newPrivacy
+  }
+
+  isConfidential () {
+    return this.privacy === VideoPrivacy.PRIVATE ||
+      this.privacy === VideoPrivacy.UNLISTED ||
+      this.privacy === VideoPrivacy.INTERNAL
+  }
+
   async publishIfNeededAndSave (t: Transaction) {
     if (this.state !== VideoState.PUBLISHED) {
       this.state = VideoState.PUBLISHED
index 40b539106b2ec4fa8b7bffc17789347c51ec6dff..e630ca84af2f6d303b41d22afcdc45f682d345eb 100644 (file)
@@ -16,14 +16,22 @@ import { userLogin } from '../../../../shared/extra-utils/users/login'
 import { createUser } from '../../../../shared/extra-utils/users/users'
 import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/extra-utils/videos/videos'
 import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
+import { Video } from '@shared/models'
 
 const expect = chai.expect
 
 describe('Test video privacy', function () {
   let servers: ServerInfo[] = []
+  let anotherUserToken: string
+
   let privateVideoId: number
   let privateVideoUUID: string
+
+  let internalVideoId: number
+  let internalVideoUUID: string
+
   let unlistedVideoUUID: string
+
   let now: number
 
   before(async function () {
@@ -39,39 +47,63 @@ describe('Test video privacy', function () {
     await doubleFollow(servers[0], servers[1])
   })
 
-  it('Should upload a private video on server 1', async function () {
+  it('Should upload a private and internal videos on server 1', async function () {
     this.timeout(10000)
 
-    const attributes = {
-      privacy: VideoPrivacy.PRIVATE
+    for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) {
+      const attributes = { privacy }
+      await uploadVideo(servers[0].url, servers[0].accessToken, attributes)
     }
-    await uploadVideo(servers[0].url, servers[0].accessToken, attributes)
 
     await waitJobs(servers)
   })
 
-  it('Should not have this private video on server 2', async function () {
+  it('Should not have these private and internal videos on server 2', async function () {
     const res = await getVideosList(servers[1].url)
 
     expect(res.body.total).to.equal(0)
     expect(res.body.data).to.have.lengthOf(0)
   })
 
-  it('Should list my (private) videos', async function () {
-    const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 1)
+  it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () {
+    const res = await getVideosList(servers[0].url)
+
+    expect(res.body.total).to.equal(0)
+    expect(res.body.data).to.have.lengthOf(0)
+  })
+
+  it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () {
+    const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
 
     expect(res.body.total).to.equal(1)
     expect(res.body.data).to.have.lengthOf(1)
 
-    privateVideoId = res.body.data[0].id
-    privateVideoUUID = res.body.data[0].uuid
+    expect(res.body.data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL)
+  })
+
+  it('Should list my (private and internal) videos', async function () {
+    const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 10)
+
+    expect(res.body.total).to.equal(2)
+    expect(res.body.data).to.have.lengthOf(2)
+
+    const videos: Video[] = res.body.data
+
+    const privateVideo = videos.find(v => v.privacy.id === VideoPrivacy.PRIVATE)
+    privateVideoId = privateVideo.id
+    privateVideoUUID = privateVideo.uuid
+
+    const internalVideo = videos.find(v => v.privacy.id === VideoPrivacy.INTERNAL)
+    internalVideoId = internalVideo.id
+    internalVideoUUID = internalVideo.uuid
   })
 
-  it('Should not be able to watch this video with non authenticated user', async function () {
+  it('Should not be able to watch the private/internal video with non authenticated user', async function () {
     await getVideo(servers[0].url, privateVideoUUID, 401)
+    await getVideo(servers[0].url, internalVideoUUID, 401)
   })
 
-  it('Should not be able to watch this private video with another user', async function () {
+  it('Should not be able to watch the private video with another user', async function () {
     this.timeout(10000)
 
     const user = {
@@ -80,12 +112,16 @@ describe('Test video privacy', function () {
     }
     await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password })
 
-    const token = await userLogin(servers[0], user)
-    await getVideoWithToken(servers[0].url, token, privateVideoUUID, 403)
+    anotherUserToken = await userLogin(servers[0], user)
+    await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, 403)
   })
 
-  it('Should be able to watch this video with the correct user', async function () {
-    await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID)
+  it('Should be able to watch the internal video with another user', async function () {
+    await getVideoWithToken(servers[0].url, anotherUserToken, internalVideoUUID, 200)
+  })
+
+  it('Should be able to watch the private video with the correct user', async function () {
+    await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID, 200)
   })
 
   it('Should upload an unlisted video on server 2', async function () {
@@ -127,16 +163,27 @@ describe('Test video privacy', function () {
     }
   })
 
-  it('Should update the private video to public on server 1', async function () {
+  it('Should update the private and internal videos to public on server 1', async function () {
     this.timeout(10000)
 
-    const attribute = {
-      name: 'super video public',
-      privacy: VideoPrivacy.PUBLIC
+    now = Date.now()
+
+    {
+      const attribute = {
+        name: 'private video becomes public',
+        privacy: VideoPrivacy.PUBLIC
+      }
+
+      await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, privateVideoId, attribute)
     }
 
-    now = Date.now()
-    await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, attribute)
+    {
+      const attribute = {
+        name: 'internal video becomes public',
+        privacy: VideoPrivacy.PUBLIC
+      }
+      await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, internalVideoId, attribute)
+    }
 
     await waitJobs(servers)
   })
@@ -144,18 +191,30 @@ describe('Test video privacy', function () {
   it('Should have this new public video listed on server 1 and 2', async function () {
     for (const server of servers) {
       const res = await getVideosList(server.url)
+      expect(res.body.total).to.equal(2)
+      expect(res.body.data).to.have.lengthOf(2)
+
+      const videos: Video[] = res.body.data
+      const privateVideo = videos.find(v => v.name === 'private video becomes public')
+      const internalVideo = videos.find(v => v.name === 'internal video becomes public')
+
+      expect(privateVideo).to.not.be.undefined
+      expect(internalVideo).to.not.be.undefined
+
+      expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now)
+      // We don't change the publish date of internal videos
+      expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now)
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('super video public')
-      expect(new Date(res.body.data[0].publishedAt).getTime()).to.be.at.least(now)
+      expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC)
+      expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC)
     }
   })
 
-  it('Should set this new video as private', async function () {
+  it('Should set these videos as private and internal', async function () {
     this.timeout(10000)
 
-    await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.PRIVATE })
+    await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, { privacy: VideoPrivacy.PRIVATE })
+    await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.INTERNAL })
 
     await waitJobs(servers)
 
@@ -168,10 +227,19 @@ describe('Test video privacy', function () {
 
     {
       const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
+      const videos = res.body.data
+
+      expect(res.body.total).to.equal(2)
+      expect(videos).to.have.lengthOf(2)
+
+      const privateVideo = videos.find(v => v.name === 'private video becomes public')
+      const internalVideo = videos.find(v => v.name === 'internal video becomes public')
+
+      expect(privateVideo).to.not.be.undefined
+      expect(internalVideo).to.not.be.undefined
 
-      expect(res.body.total).to.equal(1)
-      expect(res.body.data).to.have.lengthOf(1)
-      expect(res.body.data[0].name).to.equal('super video public')
+      expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL)
+      expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE)
     }
   })
 
index 29888c7b8d66eb33cd8b0a7f3d4ad8bbd7b39efd..17ed0c9bb6c1afaec5a3135d28cc57b4f7ddfce8 100644 (file)
@@ -1,5 +1,6 @@
 export enum VideoPrivacy {
   PUBLIC = 1,
   UNLISTED = 2,
-  PRIVATE = 3
+  PRIVATE = 3,
+  INTERNAL = 4
 }
index b865c16141ad342e779887b28c3924855ed50db7..87d74f654aa02c521202f18a317d98cd197bc0b8 100644 (file)
@@ -2,5 +2,5 @@ import { VideoPrivacy } from './video-privacy.enum'
 
 export interface VideoScheduleUpdate {
   updateAt: Date | string
-  privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED // Cannot schedule an update to PRIVATE
+  privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED | VideoPrivacy.INTERNAL // Cannot schedule an update to PRIVATE
 }