1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-05 19:42:24 +02:00

Add defaults values config in web admin

This commit is contained in:
Chocobozzz 2025-06-13 15:36:19 +02:00
parent eb11e5793f
commit e9bb222b6c
No known key found for this signature in database
GPG key ID: 583A612D890159BE
9 changed files with 241 additions and 40 deletions

View file

@ -2,7 +2,7 @@ import { inject } from '@angular/core'
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot, Routes } from '@angular/router'
import { CanDeactivateGuard, ServerService, UserRightGuard } from '@app/core'
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
import { CustomConfig, UserRight, VideoConstant } from '@peertube/peertube-models'
import { CustomConfig, UserRight, VideoCommentPolicyType, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
import { map } from 'rxjs'
import { AdminConfigComponent } from './admin-config.component'
import {
@ -33,6 +33,24 @@ export const languagesResolver: ResolveFn<VideoConstant<string>[]> = (_route: Ac
return inject(ServerService).getVideoLanguages()
}
export const licencesResolver: ResolveFn<VideoConstant<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
return inject(ServerService).getVideoLicences()
}
export const privaciesResolver: ResolveFn<VideoConstant<VideoPrivacyType>[]> = (
_route: ActivatedRouteSnapshot,
_state: RouterStateSnapshot
) => {
return inject(ServerService).getVideoPrivacies()
}
export const commentPoliciesResolver: ResolveFn<VideoConstant<VideoCommentPolicyType>[]> = (
_route: ActivatedRouteSnapshot,
_state: RouterStateSnapshot
) => {
return inject(ServerService).getCommentPolicies()
}
export const configRoutes: Routes = [
{
path: 'config',
@ -97,6 +115,11 @@ export const configRoutes: Routes = [
path: 'general',
component: AdminConfigGeneralComponent,
canDeactivate: [ CanDeactivateGuard ],
resolve: {
privacies: privaciesResolver,
licences: licencesResolver,
commentPolicies: commentPoliciesResolver
},
data: {
meta: {
title: $localize`General configuration`

View file

@ -53,9 +53,9 @@
>
<ng-container ngProjectAs="description">
@if (countExternalAuth() === 0) {
<span *ngIf="" i18n>⚠️ You don't have any external auth plugin enabled.</span>
<span i18n>⚠️ You don't have any external auth plugin enabled</span>
} @else if (countExternalAuth() > 1) {
<span i18n>⚠️ You have multiple external auth plugins enabled.</span>
<span i18n>⚠️ You have multiple external auth plugins enabled</span>
}
</ng-container>
</my-peertube-checkbox>
@ -137,7 +137,7 @@
i18n-labelText labelText="Enable Signup"
>
<ng-container ngProjectAs="description">
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation</span>
<my-alert type="primary" class="d-block mt-2" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</my-alert>
</ng-container>
@ -171,7 +171,7 @@
<div *ngIf="formErrors.signup.limit" class="form-error" role="alert">{{ formErrors.signup.limit }}</div>
<small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users.</small>
<small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users</small>
</div>
<div [ngClass]="getDisabledSignupClass()" class="mt-3">
@ -253,7 +253,7 @@
<div class="form-group">
<label i18n for="importConcurrency">Import jobs concurrency</label>
<span i18n class="small muted ms-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
<span i18n class="small muted ms-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart</span>
<div class="number-with-unit">
<input type="number" id="importConcurrency" formControlName="concurrency" />
@ -399,6 +399,35 @@
</my-peertube-checkbox>
</div>
</ng-container>
<ng-container formGroupName="defaults">
<ng-container formGroupName="publish">
<div class="form-group">
<label i18n for="defaultsPublishPrivacy">Default video privacy</label>
<my-select-options inputId="defaultsPublishPrivacy" [items]="privacyOptions" formControlName="privacy"></my-select-options>
<div *ngIf="formErrors.defaults.publish.privacy" class="form-error" role="alert">{{ formErrors.defaults.publish.privacy }}</div>
</div>
<div class="form-group">
<label i18n for="defaultsPublishLicence">Default video licence</label>
<my-select-options inputId="defaultsPublishLicence" [items]="licenceOptions" formControlName="licence"></my-select-options>
<div *ngIf="formErrors.defaults.publish.licence" class="form-error" role="alert">{{ formErrors.defaults.publish.licence }}</div>
</div>
<div class="form-group">
<label i18n for="defaultsPublishCommentsPolicy">Default comment policy</label>
<my-select-options inputId="defaultsPublishCommentsPolicy" [items]="commentPoliciesOptions" formControlName="commentsPolicy"></my-select-options>
<div *ngIf="formErrors.defaults.publish.commentsPolicy" class="form-error" role="alert">{{ formErrors.defaults.publish.commentsPolicy }}</div>
</div>
</ng-container>
</ng-container>
</div>
</div>
@ -424,6 +453,41 @@
</div>
</div>
<div class="pt-two-cols mt-4">
<div class="title-col">
<h2 i18n>PLAYER</h2>
</div>
<div class="content-col">
<ng-container formGroupName="defaults">
<div class="form-group" formGroupName="player">
<my-peertube-checkbox
inputName="defaultsPlayerAutoplay" formControlName="autoPlay"
i18n-labelText labelText="Automatically play videos in the player"
></my-peertube-checkbox>
</div>
<ng-container formGroupName="p2p">
<div class="form-group" formGroupName="webapp">
<my-peertube-checkbox
inputName="defaultsP2PWebappEnabled" formControlName="enabled"
i18n-labelText labelText="Enable P2P streaming by default on your platform"
></my-peertube-checkbox>
</div>
<div class="form-group" formGroupName="embed">
<my-peertube-checkbox
inputName="defaultsP2PEmbedEnabled" formControlName="enabled"
i18n-labelText labelText="Enable P2P streaming by default for videos embedded on external websites"
></my-peertube-checkbox>
</div>
</ng-container>
</ng-container>
</div>
</div>
<div class="pt-two-cols mt-4">
<div class="title-col">
<h2 i18n>SEARCH</h2>
@ -465,7 +529,7 @@
i18n-labelText labelText="Enable global search"
>
<ng-container ngProjectAs="description">
<div i18n>⚠️ This functionality depends heavily on the moderation of platforms followed by the search index you select.</div>
<div i18n>⚠️ This functionality depends heavily on the moderation of platforms followed by the search index you select</div>
</ng-container>
<ng-container ngProjectAs="extra">
@ -573,7 +637,7 @@
<my-select-options inputId="exportUsersExportExpiration" [items]="exportExpirationOptions" formControlName="exportExpiration"></my-select-options>
<div i18n class="mt-1 small muted">The archive file is deleted after this period.</div>
<div i18n class="mt-1 small muted">The archive file is deleted after this period</div>
<div *ngIf="formErrors.export.users.exportExpiration" class="form-error" role="alert">{{ formErrors.export.users.exportExpiration }}</div>
</div>
@ -626,7 +690,7 @@
i18n-labelText labelText="Automatically follow back followers that follow your platform"
>
<ng-container ngProjectAs="description">
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation</span>
</ng-container>
</my-peertube-checkbox>
</div>

View file

@ -23,7 +23,8 @@ import {
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
import { BroadcastMessageLevel, CustomConfig } from '@peertube/peertube-models'
import { VideoService } from '@app/shared/shared-main/video/video.service'
import { BroadcastMessageLevel, CustomConfig, VideoCommentPolicyType, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
import { Subscription } from 'rxjs'
import { pairwise } from 'rxjs/operators'
import { SelectOptionsItem } from 'src/types/select-options-item.model'
@ -172,6 +173,28 @@ type Form = {
storyboards: FormGroup<{
enabled: FormControl<boolean>
}>
defaults: FormGroup<{
publish: FormGroup<{
commentsPolicy: FormControl<VideoCommentPolicyType>
privacy: FormControl<VideoPrivacyType>
licence: FormControl<number>
}>
p2p: FormGroup<{
webapp: FormGroup<{
enabled: FormControl<boolean>
}>
embed: FormGroup<{
enabled: FormControl<boolean>
}>
}>
player: FormGroup<{
autoPlay: FormControl<boolean>
}>
}>
}
@Component({
@ -198,6 +221,7 @@ export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanCompon
private route = inject(ActivatedRoute)
private formReactiveService = inject(FormReactiveService)
private adminConfigService = inject(AdminConfigService)
private videoService = inject(VideoService)
form: FormGroup<Form>
formErrors: FormReactiveErrorsTyped<Form> = {}
@ -209,12 +233,26 @@ export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanCompon
exportExpirationOptions: SelectOptionsItem[] = []
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
privacyOptions: SelectOptionsItem[] = []
commentPoliciesOptions: SelectOptionsItem[] = []
licenceOptions: SelectOptionsItem[] = []
private customConfig: CustomConfig
private customConfigSub: Subscription
ngOnInit () {
this.customConfig = this.route.parent.snapshot.data['customConfig']
const data = this.route.snapshot.data as {
licences: VideoConstant<number>[]
privacies: VideoConstant<VideoPrivacyType>[]
commentPolicies: VideoConstant<VideoCommentPolicyType>[]
}
this.privacyOptions = this.videoService.explainedPrivacyLabels(data.privacies).videoPrivacies
this.licenceOptions = data.licences
this.commentPoliciesOptions = data.commentPolicies
this.buildLandingPageOptions()
this.exportExpirationOptions = [
@ -360,9 +398,26 @@ export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanCompon
isDefaultSearch: null
}
},
storyboards: {
enabled: null
},
defaults: {
publish: {
commentsPolicy: null,
privacy: null,
licence: null
},
p2p: {
webapp: {
enabled: null
},
embed: {
enabled: null
}
},
player: {
autoPlay: null
}
}
}

View file

@ -1,5 +1,5 @@
import { exists } from '@peertube/peertube-core-utils'
import { CustomConfig, VideoPrivacy } from '@peertube/peertube-models'
import { CustomConfig, VideoCommentPolicy, VideoPrivacy } from '@peertube/peertube-models'
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
import { getBytes } from '@root-helpers/bytes'
import merge from 'lodash-es/merge'
@ -16,6 +16,7 @@ export class UsageType {
live: EnabledDisabled
globalSearch: EnabledDisabled
defaultPrivacy: typeof VideoPrivacy.INTERNAL | typeof VideoPrivacy.PUBLIC
defaultCommentPolicy: typeof VideoCommentPolicy.REQUIRES_APPROVAL
p2p: EnabledDisabled
federation: EnabledDisabled
keepOriginalVideo: EnabledDisabled
@ -46,7 +47,7 @@ export class UsageType {
usageType.keepOriginalVideo = 'disabled'
usageType.allowReplaceFile = 'disabled'
// Use current config for: authType, preferDisplayName and transcription
// Use current config for: defaultCommentPolicy, authType, preferDisplayName and transcription
usageType.compute()
@ -69,7 +70,7 @@ export class UsageType {
usageType.allowReplaceFile = 'enabled'
usageType.preferDisplayName = 'enabled'
// Use current config for: authType and transcription
// Use current config for: defaultCommentPolicy, authType and transcription
usageType.compute()
@ -94,6 +95,8 @@ export class UsageType {
usageType.authType = 'local'
usageType.transcription = 'enabled'
usageType.defaultCommentPolicy = VideoCommentPolicy.REQUIRES_APPROVAL
// Use current config for: federation
usageType.compute()
@ -107,7 +110,7 @@ export class UsageType {
this.config = {}
this.computeRegistration()
this.computeVideoPrivacy()
this.computeDefaultVideoPrivacy()
this.computeVideoQuota()
this.computeKeepOriginalVideo()
this.computeReplaceVideoFile()
@ -115,6 +118,7 @@ export class UsageType {
this.computeStreamLives()
this.computeP2P()
this.computeGlobalSearch()
this.computeDefaultVideoCommentPolicy()
this.computeFederation()
this.computeMiniatureSettings()
this.computeTranscription()
@ -256,7 +260,7 @@ export class UsageType {
}
}
private computeVideoPrivacy () {
private computeDefaultVideoPrivacy () {
if (!exists(this.defaultPrivacy)) return
this.addConfig({
@ -274,6 +278,20 @@ export class UsageType {
}
}
private computeDefaultVideoCommentPolicy () {
if (!exists(this.defaultCommentPolicy)) return
this.addConfig({
defaults: {
publish: {
commentsPolicy: this.defaultCommentPolicy
}
}
})
this.addExplanation($localize`<strong>Require approval</strong> by default of new video comment`)
}
private computeP2P () {
if (!exists(this.p2p)) return

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import merge from 'lodash-es/merge.js'
import { omit } from '@peertube/peertube-core-utils'
import { ActorImageType, CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import {
cleanupTests,
createSingleServer,
@ -12,7 +12,7 @@ import {
PeerTubeServer,
setAccessTokensToServers
} from '@peertube/peertube-server-commands'
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import merge from 'lodash-es/merge.js'
describe('Test config API validators', function () {
const path = '/api/v1/config/custom'
@ -91,13 +91,29 @@ describe('Test config API validators', function () {
})
it('Should fail with a bad default NSFW policy', async function () {
const newUpdateParams = {
...updateParams,
const newUpdateParams = merge({}, updateParams, {
instance: {
defaultNSFWPolicy: 'hello'
}
})
await makePutBodyRequest({
url: server.url,
path,
fields: newUpdateParams,
token: server.accessToken,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should fail with a bad default comment policy', async function () {
const newUpdateParams = merge({}, updateParams, {
defaults: {
publish: {
commentsPolicy: 11
}
}
})
await makePutBodyRequest({
url: server.url,
@ -110,16 +126,14 @@ describe('Test config API validators', function () {
it('Should fail if email disabled and signup requires email verification', async function () {
// opposite scenario - success when enable enabled - covered via tests/api/users/user-verification.ts
const newUpdateParams = {
...updateParams,
const newUpdateParams = merge({}, updateParams, {
signup: {
enabled: true,
limit: 5,
requiresApproval: true,
requiresEmailVerification: true
}
}
})
await makePutBodyRequest({
url: server.url,
@ -131,18 +145,17 @@ describe('Test config API validators', function () {
})
it('Should fail with a disabled web videos & hls transcoding', async function () {
const newUpdateParams = {
...updateParams,
const newUpdateParams = merge({}, updateParams, {
transcoding: {
enabled: true,
hls: {
enabled: false
},
web_videos: {
webVideos: {
enabled: false
}
}
}
})
await makePutBodyRequest({
url: server.url,
@ -154,7 +167,7 @@ describe('Test config API validators', function () {
})
it('Should fail with a disabled http upload & enabled sync', async function () {
const newUpdateParams: CustomConfig = merge({}, updateParams, {
const newUpdateParams: CustomConfig = merge({}, {}, updateParams, {
import: {
videos: {
http: { enabled: false }
@ -184,7 +197,6 @@ describe('Test config API validators', function () {
})
describe('When deleting the configuration', function () {
it('Should fail without token', async function () {
await makeDeleteRequest({
url: server.url,

View file

@ -146,6 +146,14 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
expect(data.export.users.enabled).to.be.true
expect(data.export.users.exportExpiration).to.equal(1000 * 3600 * 48)
expect(data.export.users.maxUserVideoQuota).to.equal(10737418240)
expect(data.defaults.publish.commentsPolicy).to.equal(VideoCommentPolicy.ENABLED)
expect(data.defaults.publish.downloadEnabled).to.be.true
expect(data.defaults.publish.licence).to.be.null
expect(data.defaults.publish.privacy).to.equal(VideoPrivacy.PUBLIC)
expect(data.defaults.p2p.embed.enabled).to.be.true
expect(data.defaults.p2p.webapp.enabled).to.be.true
expect(data.defaults.player.autoPlay).to.be.true
}
function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {

View file

@ -537,12 +537,13 @@ function convertCustomConfigBody (body: CustomConfig) {
// Transcoding resolutions exception
if (/^\d{3,4}p$/.exec(k)) return k
if (k === '0p') return k
if (k === 'p2p') return k
return snakeCase(k)
}
function valueConverter (v: any) {
if (validator.default.isNumeric(v + '')) return parseInt('' + v, 10)
if (validator.isNumeric(v + '')) return parseInt('' + v, 10)
return v
}

View file

@ -138,17 +138,29 @@ const CONFIG = {
DEFAULTS: {
PUBLISH: {
DOWNLOAD_ENABLED: config.get<boolean>('defaults.publish.download_enabled'),
COMMENTS_POLICY: config.get<VideoCommentPolicyType>('defaults.publish.comments_policy'),
PRIVACY: config.get<VideoPrivacyType>('defaults.publish.privacy'),
LICENCE: config.get<number>('defaults.publish.licence')
get DOWNLOAD_ENABLED () {
return config.get<boolean>('defaults.publish.download_enabled')
},
get COMMENTS_POLICY () {
return config.get<VideoCommentPolicyType>('defaults.publish.comments_policy')
},
get PRIVACY () {
return config.get<VideoPrivacyType>('defaults.publish.privacy')
},
get LICENCE () {
return config.get<number>('defaults.publish.licence')
}
},
P2P: {
WEBAPP: {
ENABLED: config.get<boolean>('defaults.p2p.webapp.enabled')
get ENABLED () {
return config.get<boolean>('defaults.p2p.webapp.enabled')
}
},
EMBED: {
ENABLED: config.get<boolean>('defaults.p2p.embed.enabled')
get ENABLED () {
return config.get<boolean>('defaults.p2p.embed.enabled')
}
}
},
PLAYER: {

View file

@ -8,6 +8,7 @@ import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaVali
import { isThemeRegistered } from '../../lib/plugins/theme-utils.js'
import { areValidationErrors } from './shared/index.js'
import { isNumberArray, isStringArray } from '@server/helpers/custom-validators/search.js'
import { isVideoCommentsPolicyValid, isVideoLicenceValid, isVideoPrivacyValid } from '@server/helpers/custom-validators/videos.js'
const customConfigUpdateValidator = [
body('instance.name').exists(),
@ -135,6 +136,13 @@ const customConfigUpdateValidator = [
body('search.searchIndex.disableLocalSearch').isBoolean(),
body('search.searchIndex.isDefaultSearch').isBoolean(),
body('defaults.publish.commentsPolicy').custom(isVideoCommentsPolicyValid),
body('defaults.publish.privacy').custom(isVideoPrivacyValid),
body('defaults.publish.licence').custom(isVideoLicenceValid),
body('defaults.p2p.webapp.enabled').isBoolean(),
body('defaults.p2p.embed.enabled').isBoolean(),
body('defaults.player.autoPlay').isBoolean(),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
if (!checkInvalidConfigIfEmailDisabled(req.body, res)) return