]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add max lives limit
authorChocobozzz <me@florianbigard.com>
Wed, 28 Oct 2020 14:24:40 +0000 (15:24 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Mon, 9 Nov 2020 14:33:04 +0000 (15:33 +0100)
19 files changed:
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
client/src/app/core/server/server.service.ts
client/src/app/shared/shared-instance/instance-features-table.component.html
client/src/app/shared/shared-instance/instance-features-table.component.ts
config/default.yaml
server/controllers/api/config.ts
server/initializers/checker-before-init.ts
server/initializers/config.ts
server/middlewares/validators/config.ts
server/middlewares/validators/videos/video-live.ts
server/models/video/video.ts
server/tests/api/check-params/config.ts
server/tests/api/server/config.ts
shared/extra-utils/server/config.ts
shared/models/server/custom-config.model.ts
shared/models/server/server-config.model.ts
shared/models/server/server-error-code.enum.ts

index 2f3202e06d10e3a3984bae5754a7a0962d9aadd2..686f3601b6a8145a5a908eb055cc2d5067eb7f1a 100644 (file)
                       </my-peertube-checkbox>
                     </div>
 
+                    <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
+                      <label i18n for="liveMaxInstanceLives">Max lives created on your instance (-1 for "unlimited")</label>
+                      <input type="number" name="liveMaxInstanceLives" formControlName="maxInstanceLives" />
+                    </div>
+
+                    <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
+                      <label i18n for="liveMaxUserLives">Max lives created per user (-1 for "unlimited")</label>
+                      <input type="number" name="liveMaxUserLives" formControlName="maxUserLives" />
+                    </div>
+
                     <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
                       <label i18n for="liveMaxDuration">Max live duration</label>
                       <div class="peertube-select-container">
index 745238647637bcb44ec2b08cb9cb035991941891..de1cf46b141114aac523cb1b3805b87119a80685 100644 (file)
@@ -216,6 +216,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
         enabled: null,
 
         maxDuration: null,
+        maxInstanceLives: null,
+        maxUserLives: null,
         allowReplay: null,
 
         transcoding: {
index 9868c37d2bb12616400d89505862d11d58cd5b9c..870a70d3d6e8c8f9f897b55394530197e5afe306 100644 (file)
@@ -7,7 +7,7 @@ import { scrollToTop } from '@app/helpers'
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { LiveVideoService, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
 import { LoadingBarService } from '@ngx-loading-bar/core'
-import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoPrivacy } from '@shared/models'
+import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, ServerErrorCode, VideoPrivacy } from '@shared/models'
 import { VideoSend } from './video-send'
 
 @Component({
@@ -81,7 +81,16 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
 
       err => {
         this.firstStepError.emit()
-        this.notifier.error(err.message)
+
+        let message = err.message
+
+        if (err.body?.code === ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED) {
+          message = $localize`Cannot create live because this instance have too many created lives`
+        } else if (err.body?.code) {
+          message = $localize`Cannot create live because you created too many lives`
+        }
+
+        this.notifier.error(message)
       }
     )
   }
index c19c3c12e037fa4ddd68b66ad9e652490841331c..1abf87118fdeaa2a827cb6212e1f1fdad8685fba 100644 (file)
@@ -78,6 +78,8 @@ export class ServerService {
       enabled: false,
       allowReplay: true,
       maxDuration: null,
+      maxInstanceLives: -1,
+      maxUserLives: -1,
       transcoding: {
         enabled: false,
         enabledResolutions: []
index 0026952385036a09a59ed1faf6b3f8de1deb9eb9..ce25571472049116386aff1e3db9e5b9179cc96c 100644 (file)
       </td>
     </tr>
 
+    <tr>
+      <th i18n class="sub-label" scope="row">Max parallel lives</th>
+      <td i18n>
+        {{ maxUserLives }} per user / {{ maxInstanceLives }} per instance
+      </td>
+    </tr>
+
     <tr>
       <th i18n class="label" colspan="2">Import</th>
     </tr>
index 76b595c20e3bf2c95433e5bd6b8cc49dd7aed06c..0166157f9f5e4d121bf00622c3aaa37e25ad8aca 100644 (file)
@@ -21,6 +21,20 @@ export class InstanceFeaturesTableComponent implements OnInit {
     return Math.min(this.initialUserVideoQuota, this.serverConfig.user.videoQuotaDaily)
   }
 
+  get maxInstanceLives () {
+    const value = this.serverConfig.live.maxInstanceLives
+    if (value === -1) return $localize`Unlimited`
+
+    return value
+  }
+
+  get maxUserLives () {
+    const value = this.serverConfig.live.maxUserLives
+    if (value === -1) return $localize`Unlimited`
+
+    return value
+  }
+
   ngOnInit () {
     this.serverConfig = this.serverService.getTmpConfig()
     this.serverService.getConfig()
index d0937bfc882990a294acea2e2e6b3a546b3db216..af16f081fbad00761603f9c778835dc823d767c7 100644 (file)
@@ -250,6 +250,14 @@ live:
   # Set null to disable duration limit
   max_duration: 5 hours
 
+  # Limit max number of live videos created on your instance
+  # -1 == unlimited
+  max_instance_lives: 20
+
+  # Limit max number of live videos created by a user on your instance
+  # -1 == unlimited
+  max_user_lives: 3
+
   # Allow your users to save a replay of their live
   # PeerTube will transcode segments in a video file
   # If the user daily/total quota is reached, PeerTube will stop the live
index 99aabba62e62a94c4b0fe7e051b0bdb59b3bb967..eb9f5f4b474d97bdbb4f85f00964c07a96e36fb5 100644 (file)
@@ -120,6 +120,8 @@ async function getConfig (req: express.Request, res: express.Response) {
 
       allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
       maxDuration: CONFIG.LIVE.MAX_DURATION,
+      maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
+      maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
 
       transcoding: {
         enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
@@ -430,6 +432,8 @@ function customConfig (): CustomConfig {
       enabled: CONFIG.LIVE.ENABLED,
       allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
       maxDuration: CONFIG.LIVE.MAX_DURATION,
+      maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
+      maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
       transcoding: {
         enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
         threads: CONFIG.LIVE.TRANSCODING.THREADS,
index d4140e3fa9863f6ff2ff8e409e82e613cf8a6939..93b71a242e3de51d17bafb10ae06ec96087b5900 100644 (file)
@@ -38,7 +38,7 @@ function checkMissedConfig () {
     'federation.videos.federate_unlisted',
     'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
     'search.search_index.disable_local_search', 'search.search_index.is_default_search',
-    'live.enabled', 'live.allow_replay', 'live.max_duration',
+    'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives',
     'live.transcoding.enabled', 'live.transcoding.threads',
     'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p', 'live.transcoding.resolutions.480p',
     'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p', 'live.transcoding.resolutions.2160p'
index 9e892735009f0456b13dab2010d4501dcba0b08f..b70361aa9b4e68aaf14974fce04249de78e24ad7 100644 (file)
@@ -202,6 +202,9 @@ const CONFIG = {
     get ENABLED () { return config.get<boolean>('live.enabled') },
 
     get MAX_DURATION () { return parseDurationToMs(config.get<string>('live.max_duration')) },
+    get MAX_INSTANCE_LIVES () { return config.get<number>('live.max_instance_lives') },
+    get MAX_USER_LIVES () { return config.get<number>('live.max_user_lives') },
+
     get ALLOW_REPLAY () { return config.get<boolean>('live.allow_replay') },
 
     RTMP: {
index 41a6ae4f9aa856c1f60e1b8b899007d6b1ec2d56..d0071ccc143872921e7adf2cddb29365c16d2ae2 100644 (file)
@@ -65,6 +65,8 @@ const customConfigUpdateValidator = [
   body('live.enabled').isBoolean().withMessage('Should have a valid live enabled boolean'),
   body('live.allowReplay').isBoolean().withMessage('Should have a valid live allow replay boolean'),
   body('live.maxDuration').custom(isIntOrNull).withMessage('Should have a valid live max duration'),
+  body('live.maxInstanceLives').custom(isIntOrNull).withMessage('Should have a valid max instance lives'),
+  body('live.maxUserLives').custom(isIntOrNull).withMessage('Should have a valid max user lives'),
   body('live.transcoding.enabled').isBoolean().withMessage('Should have a valid live transcoding enabled boolean'),
   body('live.transcoding.threads').isInt().withMessage('Should have a valid live transcoding threads'),
   body('live.transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'),
index ab57e67bf07e895f31ac1a7c6858b89106c20b54..69200cb60d32db628ea6405a3e19c80aadd8e992 100644 (file)
@@ -2,7 +2,7 @@ import * as express from 'express'
 import { body, param } from 'express-validator'
 import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '@server/helpers/middlewares/videos'
 import { VideoLiveModel } from '@server/models/video/video-live'
-import { UserRight, VideoState } from '@shared/models'
+import { ServerErrorCode, UserRight, VideoState } from '@shared/models'
 import { isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
 import { isVideoNameValid } from '../../../helpers/custom-validators/videos'
 import { cleanUpReqFiles } from '../../../helpers/express-utils'
@@ -10,6 +10,7 @@ import { logger } from '../../../helpers/logger'
 import { CONFIG } from '../../../initializers/config'
 import { areValidationErrors } from '../utils'
 import { getCommonVideoEditAttributes } from './videos'
+import { VideoModel } from '@server/models/video/video'
 
 const videoLiveGetValidator = [
   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -50,11 +51,15 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
     logger.debug('Checking videoLiveAddValidator parameters', { parameters: req.body })
 
     if (CONFIG.LIVE.ENABLED !== true) {
+      cleanUpReqFiles(req)
+
       return res.status(403)
         .json({ error: 'Live is not enabled on this instance' })
     }
 
     if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
+      cleanUpReqFiles(req)
+
       return res.status(403)
         .json({ error: 'Saving live replay is not allowed instance' })
     }
@@ -64,6 +69,34 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
     const user = res.locals.oauth.token.User
     if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
 
+    if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) {
+      const totalInstanceLives = await VideoModel.countLocalLives()
+
+      if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) {
+        cleanUpReqFiles(req)
+
+        return res.status(403)
+          .json({
+            code: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED,
+            error: 'Cannot create this live because the max instance lives limit is reached.'
+          })
+      }
+    }
+
+    if (CONFIG.LIVE.MAX_USER_LIVES !== -1) {
+      const totalUserLives = await VideoModel.countLivesOfAccount(user.Account.id)
+
+      if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) {
+        cleanUpReqFiles(req)
+
+        return res.status(403)
+          .json({
+            code: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED,
+            error: 'Cannot create this live because the max user lives limit is reached.'
+          })
+      }
+    }
+
     return next()
   }
 ])
index 78fec558573ab335a048efbca30e0f2b2fa9f49c..d094f19b08e3dbe99894c7bd9c8a1675e82068ae 100644 (file)
@@ -1142,6 +1142,37 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.getAvailableForApi(queryOptions)
   }
 
+  static countLocalLives () {
+    const options = {
+      where: {
+        remote: false,
+        isLive: true
+      }
+    }
+
+    return VideoModel.count(options)
+  }
+
+  static countLivesOfAccount (accountId: number) {
+    const options = {
+      where: {
+        remote: false,
+        isLive: true
+      },
+      include: [
+        {
+          required: true,
+          model: VideoChannelModel.unscoped(),
+          where: {
+            accountId
+          }
+        }
+      ]
+    }
+
+    return VideoModel.count(options)
+  }
+
   static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
     const where = buildWhereIdOrUUID(id)
     const options = {
index 2882ceb7c26c909f2f3bd1cb82351ff042f0a151..42ac5e1f9d6778b6098396da78973973ab5b8181 100644 (file)
@@ -105,6 +105,8 @@ describe('Test config API validators', function () {
 
       allowReplay: false,
       maxDuration: null,
+      maxInstanceLives: -1,
+      maxUserLives: 50,
 
       transcoding: {
         enabled: true,
index a7f03536274cfd8788c1c7a05999d61a82c055ad..6c37be11377475b9c8f49839725411ae80ab8bd3 100644 (file)
@@ -81,6 +81,8 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
   expect(data.live.enabled).to.be.false
   expect(data.live.allowReplay).to.be.true
   expect(data.live.maxDuration).to.equal(1000 * 3600 * 5)
+  expect(data.live.maxInstanceLives).to.equal(20)
+  expect(data.live.maxUserLives).to.equal(3)
   expect(data.live.transcoding.enabled).to.be.false
   expect(data.live.transcoding.threads).to.equal(2)
   expect(data.live.transcoding.resolutions['240p']).to.be.false
@@ -166,6 +168,8 @@ function checkUpdatedConfig (data: CustomConfig) {
   expect(data.live.enabled).to.be.true
   expect(data.live.allowReplay).to.be.false
   expect(data.live.maxDuration).to.equal(5000)
+  expect(data.live.maxInstanceLives).to.equal(-1)
+  expect(data.live.maxUserLives).to.equal(10)
   expect(data.live.transcoding.enabled).to.be.true
   expect(data.live.transcoding.threads).to.equal(4)
   expect(data.live.transcoding.resolutions['240p']).to.be.true
@@ -330,6 +334,8 @@ describe('Test config', function () {
         enabled: true,
         allowReplay: false,
         maxDuration: 5000,
+        maxInstanceLives: -1,
+        maxUserLives: 10,
         transcoding: {
           enabled: true,
           threads: 4,
index bb7e23d54a33d5a84716dc7984d8c2e232c9ee6a..7c1ad0a75093692177ab91cbfa92160cddda52fb 100644 (file)
@@ -130,6 +130,8 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
       enabled: true,
       allowReplay: false,
       maxDuration: null,
+      maxInstanceLives: -1,
+      maxUserLives: 50,
       transcoding: {
         enabled: true,
         threads: 4,
index 11b2ef2ebbd21b9639bbfce7218263b9b910e5d5..67e05e23fe712539645c096f0cd169357e59919a 100644 (file)
@@ -99,7 +99,10 @@ export interface CustomConfig {
     enabled: boolean
 
     allowReplay: boolean
+
     maxDuration: number
+    maxInstanceLives: number
+    maxUserLives: number
 
     transcoding: {
       enabled: boolean
index 1563d848e81d98f649bdf18e6858dfa51476324c..a01fcbe418f8c937552da15827328fb0a7a74239 100644 (file)
@@ -102,6 +102,8 @@ export interface ServerConfig {
     enabled: boolean
 
     maxDuration: number
+    maxInstanceLives: number
+    maxUserLives: number
     allowReplay: boolean
 
     transcoding: {
index 0bfb2c470bbea55b5610e7d4625c0470599ea86f..c02b0e6c78e7312b89044867f9e38e6a656a9d1f 100644 (file)
@@ -1,3 +1,5 @@
 export const enum ServerErrorCode {
-  DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS = 1
+  DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS = 1,
+  MAX_INSTANCE_LIVES_LIMIT_REACHED = 2,
+  MAX_USER_LIVES_LIMIT_REACHED = 3,
 }