import omit from 'lodash-es/omit' import { forkJoin } from 'rxjs' import { SelectOptionsItem } from 'src/types/select-options-item.model' import { Component, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { ConfigService } from '@app/+admin/config/shared/config.service' import { Notifier } from '@app/core' import { ServerService } from '@app/core/server/server.service' import { ADMIN_EMAIL_VALIDATOR, CACHE_SIZE_VALIDATOR, CONCURRENCY_VALIDATOR, INDEX_URL_VALIDATOR, INSTANCE_NAME_VALIDATOR, INSTANCE_SHORT_DESCRIPTION_VALIDATOR, MAX_INSTANCE_LIVES_VALIDATOR, MAX_LIVE_DURATION_VALIDATOR, MAX_USER_LIVES_VALIDATOR, MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR, SEARCH_INDEX_URL_VALIDATOR, SERVICES_TWITTER_USERNAME_VALIDATOR, SIGNUP_LIMIT_VALIDATOR, SIGNUP_MINIMUM_AGE_VALIDATOR, TRANSCODING_THREADS_VALIDATOR } from '@app/shared/form-validators/custom-config-validators' import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' import { CustomPageService } from '@app/shared/shared-main/custom-page' import { CustomConfig, CustomPage, HTMLServerConfig } from '@peertube/peertube-models' import { EditConfigurationService } from './edit-configuration.service' type ComponentCustomConfig = CustomConfig & { instanceCustomHomepage: CustomPage } @Component({ selector: 'my-edit-custom-config', templateUrl: './edit-custom-config.component.html', styleUrls: [ './edit-custom-config.component.scss' ] }) export class EditCustomConfigComponent extends FormReactive implements OnInit { activeNav: string customConfig: ComponentCustomConfig serverConfig: HTMLServerConfig homepage: CustomPage languageItems: SelectOptionsItem[] = [] categoryItems: SelectOptionsItem[] = [] constructor ( protected formReactiveService: FormReactiveService, private router: Router, private route: ActivatedRoute, private notifier: Notifier, private configService: ConfigService, private customPage: CustomPageService, private serverService: ServerService, private editConfigurationService: EditConfigurationService ) { super() } ngOnInit () { this.serverConfig = this.serverService.getHTMLConfig() const formGroupData: { [key in keyof ComponentCustomConfig ]: any } = { instance: { name: INSTANCE_NAME_VALIDATOR, shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR, description: null, isNSFW: false, defaultNSFWPolicy: null, terms: null, codeOfConduct: null, creationReason: null, moderationInformation: null, administrator: null, maintenanceLifetime: null, businessModel: null, hardwareInformation: null, categories: null, languages: null, defaultClientRoute: null, customizations: { javascript: null, css: null } }, theme: { default: null }, services: { twitter: { username: SERVICES_TWITTER_USERNAME_VALIDATOR, whitelisted: null } }, client: { videos: { miniature: { preferAuthorDisplayName: null } }, menu: { login: { redirectOnSingleExternalAuth: null } } }, cache: { previews: { size: CACHE_SIZE_VALIDATOR }, captions: { size: CACHE_SIZE_VALIDATOR }, torrents: { size: CACHE_SIZE_VALIDATOR }, storyboards: { size: CACHE_SIZE_VALIDATOR } }, signup: { enabled: null, limit: SIGNUP_LIMIT_VALIDATOR, requiresApproval: null, requiresEmailVerification: null, minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR }, import: { videos: { concurrency: CONCURRENCY_VALIDATOR, http: { enabled: null }, torrent: { enabled: null } }, videoChannelSynchronization: { enabled: null } }, trending: { videos: { algorithms: { enabled: null, default: null } } }, admin: { email: ADMIN_EMAIL_VALIDATOR }, contactForm: { enabled: null }, user: { history: { videos: { enabled: null } }, videoQuota: USER_VIDEO_QUOTA_VALIDATOR, videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR }, videoChannels: { maxPerUser: MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR }, transcoding: { enabled: null, threads: TRANSCODING_THREADS_VALIDATOR, allowAdditionalExtensions: null, allowAudioFiles: null, profile: null, concurrency: CONCURRENCY_VALIDATOR, resolutions: {}, alwaysTranscodeOriginalResolution: null, hls: { enabled: null }, webVideos: { enabled: null }, remoteRunners: { enabled: null } }, live: { enabled: null, maxDuration: MAX_LIVE_DURATION_VALIDATOR, maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR, maxUserLives: MAX_USER_LIVES_VALIDATOR, allowReplay: null, latencySetting: { enabled: null }, transcoding: { enabled: null, threads: TRANSCODING_THREADS_VALIDATOR, profile: null, resolutions: {}, alwaysTranscodeOriginalResolution: null, remoteRunners: { enabled: null } } }, videoStudio: { enabled: null, remoteRunners: { enabled: null } }, videoFile: { update: { enabled: null } }, autoBlacklist: { videos: { ofUsers: { enabled: null } } }, followers: { instance: { enabled: null, manualApproval: null } }, followings: { instance: { autoFollowBack: { enabled: null }, autoFollowIndex: { enabled: null, indexUrl: INDEX_URL_VALIDATOR } } }, broadcastMessage: { enabled: null, level: null, dismissable: null, message: null }, search: { remoteUri: { users: null, anonymous: null }, searchIndex: { enabled: null, url: SEARCH_INDEX_URL_VALIDATOR, disableLocalSearch: null, isDefaultSearch: null } }, instanceCustomHomepage: { content: null } } const defaultValues = { transcoding: { resolutions: {} as { [id: string]: string } }, live: { transcoding: { resolutions: {} as { [id: string]: string } } } } for (const resolution of this.editConfigurationService.getVODResolutions()) { defaultValues.transcoding.resolutions[resolution.id] = 'false' formGroupData.transcoding.resolutions[resolution.id] = null } for (const resolution of this.editConfigurationService.getLiveResolutions()) { defaultValues.live.transcoding.resolutions[resolution.id] = 'false' formGroupData.live.transcoding.resolutions[resolution.id] = null } this.buildForm(formGroupData) if (this.route.snapshot.fragment) { this.onNavChange(this.route.snapshot.fragment) } this.loadConfigAndUpdateForm() this.loadCategoriesAndLanguages() if (!this.isUpdateAllowed()) { this.form.disable() } } formValidated () { this.forceCheck() if (!this.form.valid) return const value: ComponentCustomConfig = this.form.getRawValue() forkJoin([ this.configService.updateCustomConfig(omit(value, 'instanceCustomHomepage')), this.customPage.updateInstanceHomepage(value.instanceCustomHomepage.content) ]) .subscribe({ next: ([ resConfig ]) => { const instanceCustomHomepage = { content: value.instanceCustomHomepage.content } this.customConfig = { ...resConfig, instanceCustomHomepage } // Reload general configuration this.serverService.resetConfig() .subscribe(config => { this.serverConfig = config }) this.updateForm() this.notifier.success($localize`Configuration updated.`) }, error: err => this.notifier.error(err.message) }) } isUpdateAllowed () { return this.serverConfig.webadmin.configuration.edition.allowed === true } hasConsistentOptions () { if (this.hasLiveAllowReplayConsistentOptions()) return true return false } hasLiveAllowReplayConsistentOptions () { if ( this.editConfigurationService.isTranscodingEnabled(this.form) === false && this.editConfigurationService.isLiveEnabled(this.form) && this.form.value['live']['allowReplay'] === true ) { return false } return true } onNavChange (newActiveNav: string) { this.activeNav = newActiveNav this.router.navigate([], { fragment: this.activeNav }) } grabAllErrors (errorObjectArg?: any) { const errorObject = errorObjectArg || this.formErrors let acc: string[] = [] for (const key of Object.keys(errorObject)) { const value = errorObject[key] if (!value) continue if (typeof value === 'string') { acc.push(value) } else { acc = acc.concat(this.grabAllErrors(value)) } } return acc } private updateForm () { this.form.patchValue(this.customConfig) } private loadConfigAndUpdateForm () { forkJoin([ this.configService.getCustomConfig(), this.customPage.getInstanceHomepage() ]).subscribe({ next: ([ config, homepage ]) => { this.customConfig = { ...config, instanceCustomHomepage: homepage } this.updateForm() this.markAllAsDirty() }, error: err => this.notifier.error(err.message) }) } private loadCategoriesAndLanguages () { forkJoin([ this.serverService.getVideoLanguages(), this.serverService.getVideoCategories() ]).subscribe({ next: ([ languages, categories ]) => { this.languageItems = languages.map(l => ({ label: l.label, id: l.id })) this.categoryItems = categories.map(l => ({ label: l.label, id: l.id + '' })) }, error: err => this.notifier.error(err.message) }) } }