aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-12-12 15:47:47 +0100
committerChocobozzz <me@florianbigard.com>2019-12-12 16:51:59 +0100
commit22a73cb879a5cc775d4bec3d72fa9c9cf52e5175 (patch)
tree4c8d2d4f6fce8a520420ec83722fefc6d57b7a83
parent91fa7960f42cff3481465bece3389007fbc278d3 (diff)
downloadPeerTube-22a73cb879a5cc775d4bec3d72fa9c9cf52e5175.tar.gz
PeerTube-22a73cb879a5cc775d4bec3d72fa9c9cf52e5175.tar.zst
PeerTube-22a73cb879a5cc775d4bec3d72fa9c9cf52e5175.zip
Add internal privacy mode
-rw-r--r--client/src/app/shared/video/modals/video-download.component.ts2
-rw-r--r--client/src/app/shared/video/video.service.ts30
-rw-r--r--server/controllers/api/videos/index.ts24
-rw-r--r--server/controllers/api/videos/ownership.ts4
-rw-r--r--server/helpers/custom-validators/videos.ts2
-rw-r--r--server/initializers/constants.ts3
-rw-r--r--server/lib/activitypub/send/send-create.ts2
-rw-r--r--server/lib/activitypub/send/send-update.ts2
-rw-r--r--server/lib/activitypub/share.ts5
-rw-r--r--server/lib/activitypub/videos.ts2
-rw-r--r--server/lib/client-html.ts2
-rw-r--r--server/lib/schedulers/update-videos-scheduler.ts10
-rw-r--r--server/middlewares/validators/videos/videos.ts9
-rw-r--r--server/models/account/user.ts18
-rw-r--r--server/models/video/schedule-video-update.ts2
-rw-r--r--server/models/video/video.ts55
-rw-r--r--server/tests/api/videos/video-privacy.ts128
-rw-r--r--shared/models/videos/video-privacy.enum.ts3
-rw-r--r--shared/models/videos/video-schedule-update.model.ts2
19 files changed, 217 insertions, 88 deletions
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts
index 5849ee458..712740086 100644
--- a/client/src/app/shared/video/modals/video-download.component.ts
+++ b/client/src/app/shared/video/modals/video-download.component.ts
@@ -59,7 +59,7 @@ export class VideoDownloadComponent {
59 return 59 return
60 } 60 }
61 61
62 const suffix = this.video.privacy.id === VideoPrivacy.PRIVATE 62 const suffix = this.video.privacy.id === VideoPrivacy.PRIVATE || this.video.privacy.id === VideoPrivacy.INTERNAL
63 ? '?access_token=' + this.auth.getAccessToken() 63 ? '?access_token=' + this.auth.getAccessToken()
64 : '' 64 : ''
65 65
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index 45366e3e3..b0fa55966 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -332,18 +332,26 @@ export class VideoService implements VideosProvider {
332 } 332 }
333 333
334 explainedPrivacyLabels (privacies: VideoConstant<VideoPrivacy>[]) { 334 explainedPrivacyLabels (privacies: VideoConstant<VideoPrivacy>[]) {
335 const newPrivacies = privacies.slice() 335 const base = [
336 336 {
337 const privatePrivacy = newPrivacies.find(p => p.id === VideoPrivacy.PRIVATE) 337 id: VideoPrivacy.PRIVATE,
338 if (privatePrivacy) privatePrivacy.label = this.i18n('Only I can see this video') 338 label: this.i18n('Only I can see this video')
339 339 },
340 const unlistedPrivacy = newPrivacies.find(p => p.id === VideoPrivacy.UNLISTED) 340 {
341 if (unlistedPrivacy) unlistedPrivacy.label = this.i18n('Only people with the private link can see this video') 341 id: VideoPrivacy.UNLISTED,
342 342 label: this.i18n('Only people with the private link can see this video')
343 const publicPrivacy = newPrivacies.find(p => p.id === VideoPrivacy.PUBLIC) 343 },
344 if (publicPrivacy) publicPrivacy.label = this.i18n('Anyone can see this video') 344 {
345 id: VideoPrivacy.PUBLIC,
346 label: this.i18n('Anyone can see this video')
347 },
348 {
349 id: VideoPrivacy.INTERNAL,
350 label: this.i18n('Only users of this instance can see this video')
351 }
352 ]
345 353
346 return privacies 354 return base.filter(o => !!privacies.find(p => p.id === o.id))
347 } 355 }
348 356
349 private setVideoRate (id: number, rateType: UserVideoRateType) { 357 private setVideoRate (id: number, rateType: UserVideoRateType) {
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 337795541..35f0b3152 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -326,9 +326,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
326 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) 326 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
327 const videoInfoToUpdate: VideoUpdate = req.body 327 const videoInfoToUpdate: VideoUpdate = req.body
328 328
329 const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE 329 const wasConfidentialVideo = videoInstance.isConfidential()
330 const wasNotPrivateVideo = videoInstance.privacy !== VideoPrivacy.PRIVATE 330 const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation()
331 const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED
332 331
333 // Process thumbnail or create it from the video 332 // Process thumbnail or create it from the video
334 const thumbnailModel = req.files && req.files['thumbnailfile'] 333 const thumbnailModel = req.files && req.files['thumbnailfile']
@@ -359,17 +358,15 @@ async function updateVideo (req: express.Request, res: express.Response) {
359 videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) 358 videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt)
360 } 359 }
361 360
361 let isNewVideo = false
362 if (videoInfoToUpdate.privacy !== undefined) { 362 if (videoInfoToUpdate.privacy !== undefined) {
363 const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) 363 isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
364 videoInstance.privacy = newPrivacy
365 364
366 // The video was private, and is not anymore -> publish it 365 const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
367 if (wasPrivateVideo === true && newPrivacy !== VideoPrivacy.PRIVATE) { 366 videoInstance.setPrivacy(newPrivacy)
368 videoInstance.publishedAt = new Date()
369 }
370 367
371 // The video was not private, but now it is -> we need to unfederate it 368 // Unfederate the video if the new privacy is not compatible with federation
372 if (wasNotPrivateVideo === true && newPrivacy === VideoPrivacy.PRIVATE) { 369 if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
373 await VideoModel.sendDelete(videoInstance, { transaction: t }) 370 await VideoModel.sendDelete(videoInstance, { transaction: t })
374 } 371 }
375 } 372 }
@@ -392,7 +389,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
392 await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) 389 await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
393 videoInstanceUpdated.VideoChannel = res.locals.videoChannel 390 videoInstanceUpdated.VideoChannel = res.locals.videoChannel
394 391
395 if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) 392 if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
396 } 393 }
397 394
398 // Schedule an update in the future? 395 // Schedule an update in the future?
@@ -414,7 +411,6 @@ async function updateVideo (req: express.Request, res: express.Response) {
414 transaction: t 411 transaction: t
415 }) 412 })
416 413
417 const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
418 await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) 414 await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
419 415
420 auditLogger.update( 416 auditLogger.update(
@@ -427,7 +423,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
427 return videoInstanceUpdated 423 return videoInstanceUpdated
428 }) 424 })
429 425
430 if (wasUnlistedVideo || wasPrivateVideo) { 426 if (wasConfidentialVideo) {
431 Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) 427 Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
432 } 428 }
433 429
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts
index abb34082e..41d7cdc43 100644
--- a/server/controllers/api/videos/ownership.ts
+++ b/server/controllers/api/videos/ownership.ts
@@ -12,7 +12,7 @@ import {
12 videosTerminateChangeOwnershipValidator 12 videosTerminateChangeOwnershipValidator
13} from '../../../middlewares' 13} from '../../../middlewares'
14import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' 14import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
15import { VideoChangeOwnershipStatus, VideoPrivacy, VideoState } from '../../../../shared/models/videos' 15import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos'
16import { VideoChannelModel } from '../../../models/video/video-channel' 16import { VideoChannelModel } from '../../../models/video/video-channel'
17import { getFormattedObjects } from '../../../helpers/utils' 17import { getFormattedObjects } from '../../../helpers/utils'
18import { changeVideoChannelShare } from '../../../lib/activitypub' 18import { changeVideoChannelShare } from '../../../lib/activitypub'
@@ -111,7 +111,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
111 const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight 111 const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight
112 targetVideoUpdated.VideoChannel = channel 112 targetVideoUpdated.VideoChannel = channel
113 113
114 if (targetVideoUpdated.privacy !== VideoPrivacy.PRIVATE && targetVideoUpdated.state === VideoState.PUBLISHED) { 114 if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) {
115 await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t) 115 await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t)
116 await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor) 116 await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor)
117 } 117 }
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index e92ef9b92..ff93f2e99 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -102,7 +102,7 @@ function isVideoPrivacyValid (value: number) {
102} 102}
103 103
104function isScheduleVideoUpdatePrivacyValid (value: number) { 104function isScheduleVideoUpdatePrivacyValid (value: number) {
105 return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC 105 return value === VideoPrivacy.UNLISTED || value === VideoPrivacy.PUBLIC || value === VideoPrivacy.INTERNAL
106} 106}
107 107
108function isVideoOriginallyPublishedAtValid (value: string | null) { 108function isVideoOriginallyPublishedAtValid (value: string | null) {
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index bdabe7f66..d0cf4d5de 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -353,7 +353,8 @@ let VIDEO_LANGUAGES: { [id: string]: string } = {}
353const VIDEO_PRIVACIES = { 353const VIDEO_PRIVACIES = {
354 [ VideoPrivacy.PUBLIC ]: 'Public', 354 [ VideoPrivacy.PUBLIC ]: 'Public',
355 [ VideoPrivacy.UNLISTED ]: 'Unlisted', 355 [ VideoPrivacy.UNLISTED ]: 'Unlisted',
356 [ VideoPrivacy.PRIVATE ]: 'Private' 356 [ VideoPrivacy.PRIVATE ]: 'Private',
357 [ VideoPrivacy.INTERNAL ]: 'Internal'
357} 358}
358 359
359const VIDEO_STATES = { 360const VIDEO_STATES = {
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index edbc14a73..1709d8348 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -18,7 +18,7 @@ import {
18} from '../../../typings/models' 18} from '../../../typings/models'
19 19
20async function sendCreateVideo (video: MVideoAP, t: Transaction) { 20async function sendCreateVideo (video: MVideoAP, t: Transaction) {
21 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 21 if (!video.hasPrivacyForFederation()) return undefined
22 22
23 logger.info('Creating job to send video creation of %s.', video.url) 23 logger.info('Creating job to send video creation of %s.', video.url)
24 24
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index 44e0e1161..cb14b8dbf 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -25,7 +25,7 @@ import {
25async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) { 25async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
26 const video = videoArg as MVideoAP 26 const video = videoArg as MVideoAP
27 27
28 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 28 if (!video.hasPrivacyForFederation()) return undefined
29 29
30 logger.info('Creating job to update video %s.', video.url) 30 logger.info('Creating job to update video %s.', video.url)
31 31
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index fdca9bed7..e847c4b7d 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -1,5 +1,4 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { VideoPrivacy } from '../../../shared/models/videos'
3import { getServerActor } from '../../helpers/utils' 2import { getServerActor } from '../../helpers/utils'
4import { VideoShareModel } from '../../models/video/video-share' 3import { VideoShareModel } from '../../models/video/video-share'
5import { sendUndoAnnounce, sendVideoAnnounce } from './send' 4import { sendUndoAnnounce, sendVideoAnnounce } from './send'
@@ -10,10 +9,10 @@ import { getOrCreateActorAndServerAndModel } from './actor'
10import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
11import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
12import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 11import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
13import { MChannelActor, MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video' 12import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video'
14 13
15async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { 14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
16 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 15 if (!video.hasPrivacyForFederation()) return undefined
17 16
18 return Promise.all([ 17 return Promise.all([
19 shareByServer(video, t), 18 shareByServer(video, t),
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index d80173e03..2fb1f8d49 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -79,7 +79,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
79 // Check this is not a blacklisted video, or unfederated blacklisted video 79 // Check this is not a blacklisted video, or unfederated blacklisted video
80 (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && 80 (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
81 // Check the video is public/unlisted and published 81 // Check the video is public/unlisted and published
82 video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED 82 video.hasPrivacyForFederation() && video.state === VideoState.PUBLISHED
83 ) { 83 ) {
84 // Fetch more attributes that we will need to serialize in AP object 84 // Fetch more attributes that we will need to serialize in AP object
85 if (isArray(video.VideoCaptions) === false) { 85 if (isArray(video.VideoCaptions) === false) {
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index a1f4ae858..42a30f84f 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -46,7 +46,7 @@ export class ClientHtml {
46 ]) 46 ])
47 47
48 // Let Angular application handle errors 48 // Let Angular application handle errors
49 if (!video || video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { 49 if (!video || video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL || video.VideoBlacklist) {
50 return ClientHtml.getIndexHTML(req, res) 50 return ClientHtml.getIndexHTML(req, res)
51 } 51 }
52 52
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts
index 293bba91f..350a335d3 100644
--- a/server/lib/schedulers/update-videos-scheduler.ts
+++ b/server/lib/schedulers/update-videos-scheduler.ts
@@ -35,16 +35,14 @@ export class UpdateVideosScheduler extends AbstractScheduler {
35 logger.info('Executing scheduled video update on %s.', video.uuid) 35 logger.info('Executing scheduled video update on %s.', video.uuid)
36 36
37 if (schedule.privacy) { 37 if (schedule.privacy) {
38 const oldPrivacy = video.privacy 38 const wasConfidentialVideo = video.isConfidential()
39 const isNewVideo = oldPrivacy === VideoPrivacy.PRIVATE 39 const isNewVideo = video.isNewVideo(schedule.privacy)
40
41 video.privacy = schedule.privacy
42 if (isNewVideo === true) video.publishedAt = new Date()
43 40
41 video.setPrivacy(schedule.privacy)
44 await video.save({ transaction: t }) 42 await video.save({ transaction: t })
45 await federateVideoIfNeeded(video, isNewVideo, t) 43 await federateVideoIfNeeded(video, isNewVideo, t)
46 44
47 if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) { 45 if (wasConfidentialVideo) {
48 const videoToPublish: MVideoFullLight = Object.assign(video, { ScheduleVideoUpdate: schedule, UserVideoHistories: [] }) 46 const videoToPublish: MVideoFullLight = Object.assign(video, { ScheduleVideoUpdate: schedule, UserVideoHistories: [] })
49 publishedVideos.push(videoToPublish) 47 publishedVideos.push(videoToPublish)
50 } 48 }
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index ab984d84a..5e0182cc3 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -161,18 +161,15 @@ const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-
161 const videoAll = video as MVideoFullLight 161 const videoAll = video as MVideoFullLight
162 162
163 // Video private or blacklisted 163 // Video private or blacklisted
164 if (video.privacy === VideoPrivacy.PRIVATE || videoAll.VideoBlacklist) { 164 if (videoAll.requiresAuth()) {
165 await authenticatePromiseIfNeeded(req, res, authenticateInQuery) 165 await authenticatePromiseIfNeeded(req, res, authenticateInQuery)
166 166
167 const user = res.locals.oauth ? res.locals.oauth.token.User : null 167 const user = res.locals.oauth ? res.locals.oauth.token.User : null
168 168
169 // Only the owner or a user that have blacklist rights can see the video 169 // Only the owner or a user that have blacklist rights can see the video
170 if ( 170 if (!user || !user.canGetVideo(videoAll)) {
171 !user ||
172 (videoAll.VideoChannel && videoAll.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
173 ) {
174 return res.status(403) 171 return res.status(403)
175 .json({ error: 'Cannot get this private or blacklisted video.' }) 172 .json({ error: 'Cannot get this private/internal or blacklisted video.' })
176 } 173 }
177 174
178 return next() 175 return next()
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 38c6d474a..522ea3310 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -19,7 +19,7 @@ import {
19 Table, 19 Table,
20 UpdatedAt 20 UpdatedAt
21} from 'sequelize-typescript' 21} from 'sequelize-typescript'
22import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' 22import { hasUserRight, USER_ROLE_LABELS, UserRight, VideoPrivacy } from '../../../shared'
23import { User, UserRole } from '../../../shared/models/users' 23import { User, UserRole } from '../../../shared/models/users'
24import { 24import {
25 isNoInstanceConfigWarningModal, 25 isNoInstanceConfigWarningModal,
@@ -63,7 +63,7 @@ import {
63 MUserFormattable, 63 MUserFormattable,
64 MUserId, 64 MUserId,
65 MUserNotifSettingChannelDefault, 65 MUserNotifSettingChannelDefault,
66 MUserWithNotificationSetting 66 MUserWithNotificationSetting, MVideoFullLight
67} from '@server/typings/models' 67} from '@server/typings/models'
68 68
69enum ScopeNames { 69enum ScopeNames {
@@ -575,6 +575,20 @@ export class UserModel extends Model<UserModel> {
575 .then(u => u.map(u => u.username)) 575 .then(u => u.map(u => u.username))
576 } 576 }
577 577
578 canGetVideo (video: MVideoFullLight) {
579 if (video.privacy === VideoPrivacy.INTERNAL) return true
580
581 if (video.privacy === VideoPrivacy.PRIVATE) {
582 return video.VideoChannel && video.VideoChannel.Account.userId === this.id
583 }
584
585 if (video.isBlacklisted()) {
586 return this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
587 }
588
589 return false
590 }
591
578 hasRight (right: UserRight) { 592 hasRight (right: UserRight) {
579 return hasUserRight(this.role, right) 593 return hasUserRight(this.role, right)
580 } 594 }
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts
index eefc10f14..00b7f5524 100644
--- a/server/models/video/schedule-video-update.ts
+++ b/server/models/video/schedule-video-update.ts
@@ -26,7 +26,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
26 @AllowNull(true) 26 @AllowNull(true)
27 @Default(null) 27 @Default(null)
28 @Column 28 @Column
29 privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED 29 privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED | VideoPrivacy.INTERNAL
30 30
31 @CreatedAt 31 @CreatedAt
32 createdAt: Date 32 createdAt: Date
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index af6fae0b6..7e18af497 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -348,9 +348,8 @@ export type AvailableForListIDsOptions = {
348 348
349 // Only list public/published videos 349 // Only list public/published videos
350 if (!options.filter || options.filter !== 'all-local') { 350 if (!options.filter || options.filter !== 'all-local') {
351 const privacyWhere = { 351
352 // Always list public videos 352 const publishWhere = {
353 privacy: VideoPrivacy.PUBLIC,
354 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding 353 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
355 [ Op.or ]: [ 354 [ Op.or ]: [
356 { 355 {
@@ -364,8 +363,26 @@ export type AvailableForListIDsOptions = {
364 } 363 }
365 ] 364 ]
366 } 365 }
366 whereAnd.push(publishWhere)
367 367
368 whereAnd.push(privacyWhere) 368 // List internal videos if the user is logged in
369 if (options.user) {
370 const privacyWhere = {
371 [Op.or]: [
372 {
373 privacy: VideoPrivacy.INTERNAL
374 },
375 {
376 privacy: VideoPrivacy.PUBLIC
377 }
378 ]
379 }
380
381 whereAnd.push(privacyWhere)
382 } else { // Or only public videos
383 const privacyWhere = { privacy: VideoPrivacy.PUBLIC }
384 whereAnd.push(privacyWhere)
385 }
369 } 386 }
370 387
371 if (options.videoPlaylistId) { 388 if (options.videoPlaylistId) {
@@ -1773,6 +1790,10 @@ export class VideoModel extends Model<VideoModel> {
1773 } 1790 }
1774 } 1791 }
1775 1792
1793 private static isPrivacyForFederation (privacy: VideoPrivacy) {
1794 return privacy === VideoPrivacy.PUBLIC || privacy === VideoPrivacy.UNLISTED
1795 }
1796
1776 static getCategoryLabel (id: number) { 1797 static getCategoryLabel (id: number) {
1777 return VIDEO_CATEGORIES[ id ] || 'Misc' 1798 return VIDEO_CATEGORIES[ id ] || 'Misc'
1778 } 1799 }
@@ -1980,12 +2001,38 @@ export class VideoModel extends Model<VideoModel> {
1980 return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL) 2001 return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
1981 } 2002 }
1982 2003
2004 hasPrivacyForFederation () {
2005 return VideoModel.isPrivacyForFederation(this.privacy)
2006 }
2007
2008 isNewVideo (newPrivacy: VideoPrivacy) {
2009 return this.hasPrivacyForFederation() === false && VideoModel.isPrivacyForFederation(newPrivacy) === true
2010 }
2011
1983 setAsRefreshed () { 2012 setAsRefreshed () {
1984 this.changed('updatedAt', true) 2013 this.changed('updatedAt', true)
1985 2014
1986 return this.save() 2015 return this.save()
1987 } 2016 }
1988 2017
2018 requiresAuth () {
2019 return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist
2020 }
2021
2022 setPrivacy (newPrivacy: VideoPrivacy) {
2023 if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) {
2024 this.publishedAt = new Date()
2025 }
2026
2027 this.privacy = newPrivacy
2028 }
2029
2030 isConfidential () {
2031 return this.privacy === VideoPrivacy.PRIVATE ||
2032 this.privacy === VideoPrivacy.UNLISTED ||
2033 this.privacy === VideoPrivacy.INTERNAL
2034 }
2035
1989 async publishIfNeededAndSave (t: Transaction) { 2036 async publishIfNeededAndSave (t: Transaction) {
1990 if (this.state !== VideoState.PUBLISHED) { 2037 if (this.state !== VideoState.PUBLISHED) {
1991 this.state = VideoState.PUBLISHED 2038 this.state = VideoState.PUBLISHED
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts
index 40b539106..e630ca84a 100644
--- a/server/tests/api/videos/video-privacy.ts
+++ b/server/tests/api/videos/video-privacy.ts
@@ -16,14 +16,22 @@ import { userLogin } from '../../../../shared/extra-utils/users/login'
16import { createUser } from '../../../../shared/extra-utils/users/users' 16import { createUser } from '../../../../shared/extra-utils/users/users'
17import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/extra-utils/videos/videos' 17import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/extra-utils/videos/videos'
18import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 18import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
19import { Video } from '@shared/models'
19 20
20const expect = chai.expect 21const expect = chai.expect
21 22
22describe('Test video privacy', function () { 23describe('Test video privacy', function () {
23 let servers: ServerInfo[] = [] 24 let servers: ServerInfo[] = []
25 let anotherUserToken: string
26
24 let privateVideoId: number 27 let privateVideoId: number
25 let privateVideoUUID: string 28 let privateVideoUUID: string
29
30 let internalVideoId: number
31 let internalVideoUUID: string
32
26 let unlistedVideoUUID: string 33 let unlistedVideoUUID: string
34
27 let now: number 35 let now: number
28 36
29 before(async function () { 37 before(async function () {
@@ -39,39 +47,63 @@ describe('Test video privacy', function () {
39 await doubleFollow(servers[0], servers[1]) 47 await doubleFollow(servers[0], servers[1])
40 }) 48 })
41 49
42 it('Should upload a private video on server 1', async function () { 50 it('Should upload a private and internal videos on server 1', async function () {
43 this.timeout(10000) 51 this.timeout(10000)
44 52
45 const attributes = { 53 for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) {
46 privacy: VideoPrivacy.PRIVATE 54 const attributes = { privacy }
55 await uploadVideo(servers[0].url, servers[0].accessToken, attributes)
47 } 56 }
48 await uploadVideo(servers[0].url, servers[0].accessToken, attributes)
49 57
50 await waitJobs(servers) 58 await waitJobs(servers)
51 }) 59 })
52 60
53 it('Should not have this private video on server 2', async function () { 61 it('Should not have these private and internal videos on server 2', async function () {
54 const res = await getVideosList(servers[1].url) 62 const res = await getVideosList(servers[1].url)
55 63
56 expect(res.body.total).to.equal(0) 64 expect(res.body.total).to.equal(0)
57 expect(res.body.data).to.have.lengthOf(0) 65 expect(res.body.data).to.have.lengthOf(0)
58 }) 66 })
59 67
60 it('Should list my (private) videos', async function () { 68 it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () {
61 const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 1) 69 const res = await getVideosList(servers[0].url)
70
71 expect(res.body.total).to.equal(0)
72 expect(res.body.data).to.have.lengthOf(0)
73 })
74
75 it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () {
76 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
62 77
63 expect(res.body.total).to.equal(1) 78 expect(res.body.total).to.equal(1)
64 expect(res.body.data).to.have.lengthOf(1) 79 expect(res.body.data).to.have.lengthOf(1)
65 80
66 privateVideoId = res.body.data[0].id 81 expect(res.body.data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL)
67 privateVideoUUID = res.body.data[0].uuid 82 })
83
84 it('Should list my (private and internal) videos', async function () {
85 const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 10)
86
87 expect(res.body.total).to.equal(2)
88 expect(res.body.data).to.have.lengthOf(2)
89
90 const videos: Video[] = res.body.data
91
92 const privateVideo = videos.find(v => v.privacy.id === VideoPrivacy.PRIVATE)
93 privateVideoId = privateVideo.id
94 privateVideoUUID = privateVideo.uuid
95
96 const internalVideo = videos.find(v => v.privacy.id === VideoPrivacy.INTERNAL)
97 internalVideoId = internalVideo.id
98 internalVideoUUID = internalVideo.uuid
68 }) 99 })
69 100
70 it('Should not be able to watch this video with non authenticated user', async function () { 101 it('Should not be able to watch the private/internal video with non authenticated user', async function () {
71 await getVideo(servers[0].url, privateVideoUUID, 401) 102 await getVideo(servers[0].url, privateVideoUUID, 401)
103 await getVideo(servers[0].url, internalVideoUUID, 401)
72 }) 104 })
73 105
74 it('Should not be able to watch this private video with another user', async function () { 106 it('Should not be able to watch the private video with another user', async function () {
75 this.timeout(10000) 107 this.timeout(10000)
76 108
77 const user = { 109 const user = {
@@ -80,12 +112,16 @@ describe('Test video privacy', function () {
80 } 112 }
81 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password }) 113 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password })
82 114
83 const token = await userLogin(servers[0], user) 115 anotherUserToken = await userLogin(servers[0], user)
84 await getVideoWithToken(servers[0].url, token, privateVideoUUID, 403) 116 await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, 403)
85 }) 117 })
86 118
87 it('Should be able to watch this video with the correct user', async function () { 119 it('Should be able to watch the internal video with another user', async function () {
88 await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID) 120 await getVideoWithToken(servers[0].url, anotherUserToken, internalVideoUUID, 200)
121 })
122
123 it('Should be able to watch the private video with the correct user', async function () {
124 await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID, 200)
89 }) 125 })
90 126
91 it('Should upload an unlisted video on server 2', async function () { 127 it('Should upload an unlisted video on server 2', async function () {
@@ -127,16 +163,27 @@ describe('Test video privacy', function () {
127 } 163 }
128 }) 164 })
129 165
130 it('Should update the private video to public on server 1', async function () { 166 it('Should update the private and internal videos to public on server 1', async function () {
131 this.timeout(10000) 167 this.timeout(10000)
132 168
133 const attribute = { 169 now = Date.now()
134 name: 'super video public', 170
135 privacy: VideoPrivacy.PUBLIC 171 {
172 const attribute = {
173 name: 'private video becomes public',
174 privacy: VideoPrivacy.PUBLIC
175 }
176
177 await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, privateVideoId, attribute)
136 } 178 }
137 179
138 now = Date.now() 180 {
139 await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, attribute) 181 const attribute = {
182 name: 'internal video becomes public',
183 privacy: VideoPrivacy.PUBLIC
184 }
185 await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, internalVideoId, attribute)
186 }
140 187
141 await waitJobs(servers) 188 await waitJobs(servers)
142 }) 189 })
@@ -144,18 +191,30 @@ describe('Test video privacy', function () {
144 it('Should have this new public video listed on server 1 and 2', async function () { 191 it('Should have this new public video listed on server 1 and 2', async function () {
145 for (const server of servers) { 192 for (const server of servers) {
146 const res = await getVideosList(server.url) 193 const res = await getVideosList(server.url)
194 expect(res.body.total).to.equal(2)
195 expect(res.body.data).to.have.lengthOf(2)
196
197 const videos: Video[] = res.body.data
198 const privateVideo = videos.find(v => v.name === 'private video becomes public')
199 const internalVideo = videos.find(v => v.name === 'internal video becomes public')
200
201 expect(privateVideo).to.not.be.undefined
202 expect(internalVideo).to.not.be.undefined
203
204 expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now)
205 // We don't change the publish date of internal videos
206 expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now)
147 207
148 expect(res.body.total).to.equal(1) 208 expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC)
149 expect(res.body.data).to.have.lengthOf(1) 209 expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC)
150 expect(res.body.data[0].name).to.equal('super video public')
151 expect(new Date(res.body.data[0].publishedAt).getTime()).to.be.at.least(now)
152 } 210 }
153 }) 211 })
154 212
155 it('Should set this new video as private', async function () { 213 it('Should set these videos as private and internal', async function () {
156 this.timeout(10000) 214 this.timeout(10000)
157 215
158 await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.PRIVATE }) 216 await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, { privacy: VideoPrivacy.PRIVATE })
217 await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.INTERNAL })
159 218
160 await waitJobs(servers) 219 await waitJobs(servers)
161 220
@@ -168,10 +227,19 @@ describe('Test video privacy', function () {
168 227
169 { 228 {
170 const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5) 229 const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
230 const videos = res.body.data
231
232 expect(res.body.total).to.equal(2)
233 expect(videos).to.have.lengthOf(2)
234
235 const privateVideo = videos.find(v => v.name === 'private video becomes public')
236 const internalVideo = videos.find(v => v.name === 'internal video becomes public')
237
238 expect(privateVideo).to.not.be.undefined
239 expect(internalVideo).to.not.be.undefined
171 240
172 expect(res.body.total).to.equal(1) 241 expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL)
173 expect(res.body.data).to.have.lengthOf(1) 242 expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE)
174 expect(res.body.data[0].name).to.equal('super video public')
175 } 243 }
176 }) 244 })
177 245
diff --git a/shared/models/videos/video-privacy.enum.ts b/shared/models/videos/video-privacy.enum.ts
index 29888c7b8..17ed0c9bb 100644
--- a/shared/models/videos/video-privacy.enum.ts
+++ b/shared/models/videos/video-privacy.enum.ts
@@ -1,5 +1,6 @@
1export enum VideoPrivacy { 1export enum VideoPrivacy {
2 PUBLIC = 1, 2 PUBLIC = 1,
3 UNLISTED = 2, 3 UNLISTED = 2,
4 PRIVATE = 3 4 PRIVATE = 3,
5 INTERNAL = 4
5} 6}
diff --git a/shared/models/videos/video-schedule-update.model.ts b/shared/models/videos/video-schedule-update.model.ts
index b865c1614..87d74f654 100644
--- a/shared/models/videos/video-schedule-update.model.ts
+++ b/shared/models/videos/video-schedule-update.model.ts
@@ -2,5 +2,5 @@ import { VideoPrivacy } from './video-privacy.enum'
2 2
3export interface VideoScheduleUpdate { 3export interface VideoScheduleUpdate {
4 updateAt: Date | string 4 updateAt: Date | string
5 privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED // Cannot schedule an update to PRIVATE 5 privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED | VideoPrivacy.INTERNAL // Cannot schedule an update to PRIVATE
6} 6}