Instance homepage support (#4007)
* Prepare homepage parsers * Add ability to update instance hompage * Add ability to set homepage as landing page * Add homepage preview in admin * Dynamically update left menu for homepage * Inject home content in homepage * Add videos list and channel miniature custom markup * Remove unused elements in markup service
This commit is contained in:
		
							parent
							
								
									eb34ec30e0
								
							
						
					
					
						commit
						2539932e16
					
				
					 84 changed files with 1761 additions and 407 deletions
				
			
		| 
						 | 
				
			
			@ -14,6 +14,6 @@ export class AboutPeertubeContributorsComponent implements OnInit {
 | 
			
		|||
  constructor (private markdownService: MarkdownService) { }
 | 
			
		||||
 | 
			
		||||
  async ngOnInit () {
 | 
			
		||||
    this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown)
 | 
			
		||||
    this.creditsHtml = await this.markdownService.unsafeMarkdownToHTML(this.markdown, true)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,12 +4,13 @@ import { TableModule } from 'primeng/table'
 | 
			
		|||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
 | 
			
		||||
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
 | 
			
		||||
import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
 | 
			
		||||
import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
 | 
			
		||||
import { SharedFormModule } from '@app/shared/shared-forms'
 | 
			
		||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
 | 
			
		||||
import { SharedMainModule } from '@app/shared/shared-main'
 | 
			
		||||
import { SharedModerationModule } from '@app/shared/shared-moderation'
 | 
			
		||||
import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
 | 
			
		||||
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
 | 
			
		||||
import { AdminRoutingModule } from './admin-routing.module'
 | 
			
		||||
import { AdminComponent } from './admin.component'
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +19,7 @@ import {
 | 
			
		|||
  EditBasicConfigurationComponent,
 | 
			
		||||
  EditConfigurationService,
 | 
			
		||||
  EditCustomConfigComponent,
 | 
			
		||||
  EditHomepageComponent,
 | 
			
		||||
  EditInstanceInformationComponent,
 | 
			
		||||
  EditLiveConfigurationComponent,
 | 
			
		||||
  EditVODTranscodingComponent
 | 
			
		||||
| 
						 | 
				
			
			@ -53,6 +55,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
 | 
			
		|||
    SharedVideoCommentModule,
 | 
			
		||||
    SharedActorImageModule,
 | 
			
		||||
    SharedActorImageEditModule,
 | 
			
		||||
    SharedCustomMarkupModule,
 | 
			
		||||
 | 
			
		||||
    TableModule,
 | 
			
		||||
    SelectButtonModule,
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +103,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
 | 
			
		|||
    EditVODTranscodingComponent,
 | 
			
		||||
    EditLiveConfigurationComponent,
 | 
			
		||||
    EditAdvancedConfigurationComponent,
 | 
			
		||||
    EditInstanceInformationComponent
 | 
			
		||||
    EditInstanceInformationComponent,
 | 
			
		||||
    EditHomepageComponent
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  exports: [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,22 +26,13 @@
 | 
			
		|||
      <div class="form-group" formGroupName="instance">
 | 
			
		||||
        <label i18n for="instanceDefaultClientRoute">Landing page</label>
 | 
			
		||||
 | 
			
		||||
        <div class="peertube-select-container">
 | 
			
		||||
          <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
 | 
			
		||||
            <option i18n value="/videos/overview">Discover videos</option>
 | 
			
		||||
 | 
			
		||||
            <optgroup i18n-label label="Trending pages">
 | 
			
		||||
              <option i18n value="/videos/trending">Default trending page</option>
 | 
			
		||||
              <option i18n value="/videos/trending?alg=best" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('best')">Best videos</option>
 | 
			
		||||
              <option i18n value="/videos/trending?alg=hot" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('hot')">Hot videos</option>
 | 
			
		||||
              <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-viewed')">Most viewed videos</option>
 | 
			
		||||
              <option i18n value="/videos/trending?alg=most-liked" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-liked')">Most liked videos</option>
 | 
			
		||||
            </optgroup>
 | 
			
		||||
 | 
			
		||||
            <option i18n value="/videos/recently-added">Recently added videos</option>
 | 
			
		||||
            <option i18n value="/videos/local">Local videos</option>
 | 
			
		||||
          </select>
 | 
			
		||||
        </div>
 | 
			
		||||
        <my-select-custom-value
 | 
			
		||||
          id="instanceDefaultClientRoute"
 | 
			
		||||
          [items]="defaultLandingPageOptions"
 | 
			
		||||
          formControlName="defaultClientRoute"
 | 
			
		||||
          inputType="text"
 | 
			
		||||
          [clearable]="false"
 | 
			
		||||
        ></my-select-custom-value>
 | 
			
		||||
 | 
			
		||||
        <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,9 @@
 | 
			
		|||
 | 
			
		||||
import { pairwise } from 'rxjs/operators'
 | 
			
		||||
import { Component, Input, OnInit } from '@angular/core'
 | 
			
		||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
 | 
			
		||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
 | 
			
		||||
import { FormGroup } from '@angular/forms'
 | 
			
		||||
import { MenuService } from '@app/core'
 | 
			
		||||
import { ServerConfig } from '@shared/models'
 | 
			
		||||
import { ConfigService } from '../shared/config.service'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -10,22 +12,31 @@ import { ConfigService } from '../shared/config.service'
 | 
			
		|||
  templateUrl: './edit-basic-configuration.component.html',
 | 
			
		||||
  styleUrls: [ './edit-custom-config.component.scss' ]
 | 
			
		||||
})
 | 
			
		||||
export class EditBasicConfigurationComponent implements OnInit {
 | 
			
		||||
export class EditBasicConfigurationComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() form: FormGroup
 | 
			
		||||
  @Input() formErrors: any
 | 
			
		||||
 | 
			
		||||
  @Input() serverConfig: ServerConfig
 | 
			
		||||
 | 
			
		||||
  signupAlertMessage: string
 | 
			
		||||
  defaultLandingPageOptions: SelectOptionsItem[] = []
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    private configService: ConfigService
 | 
			
		||||
    private configService: ConfigService,
 | 
			
		||||
    private menuService: MenuService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit () {
 | 
			
		||||
    this.buildLandingPageOptions()
 | 
			
		||||
    this.checkSignupField()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges (changes: SimpleChanges) {
 | 
			
		||||
    if (changes['serverConfig']) {
 | 
			
		||||
      this.buildLandingPageOptions()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getVideoQuotaOptions () {
 | 
			
		||||
    return this.configService.videoQuotaOptions
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +81,15 @@ export class EditBasicConfigurationComponent implements OnInit {
 | 
			
		|||
    return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buildLandingPageOptions () {
 | 
			
		||||
    this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig)
 | 
			
		||||
      .map(o => ({
 | 
			
		||||
        id: o.path,
 | 
			
		||||
        label: o.label,
 | 
			
		||||
        description: o.path
 | 
			
		||||
      }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private checkSignupField () {
 | 
			
		||||
    const signupControl = this.form.get('signup.enabled')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,8 +3,16 @@
 | 
			
		|||
 | 
			
		||||
  <div ngbNav #nav="ngbNav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" class="nav-tabs">
 | 
			
		||||
 | 
			
		||||
    <ng-container ngbNavItem="instance-homepage">
 | 
			
		||||
      <a ngbNavLink i18n>Homepage</a>
 | 
			
		||||
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
        <my-edit-homepage [form]="form" [formErrors]="formErrors"></my-edit-homepage>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <ng-container ngbNavItem="instance-information">
 | 
			
		||||
      <a ngbNavLink i18n>Instance information</a>
 | 
			
		||||
      <a ngbNavLink i18n>Information</a>
 | 
			
		||||
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
        <my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +21,7 @@
 | 
			
		|||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <ng-container ngbNavItem="basic-configuration">
 | 
			
		||||
      <a ngbNavLink i18n>Basic configuration</a>
 | 
			
		||||
      <a ngbNavLink i18n>Basic</a>
 | 
			
		||||
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
        <my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +48,7 @@
 | 
			
		|||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <ng-container ngbNavItem="advanced-configuration">
 | 
			
		||||
      <a ngbNavLink i18n>Advanced configuration</a>
 | 
			
		||||
      <a ngbNavLink i18n>Advanced</a>
 | 
			
		||||
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
        <my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
 | 
			
		||||
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'
 | 
			
		||||
| 
						 | 
				
			
			@ -24,9 +25,14 @@ import {
 | 
			
		|||
} 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, FormValidatorService } from '@app/shared/shared-forms'
 | 
			
		||||
import { CustomConfig, ServerConfig } from '@shared/models'
 | 
			
		||||
import { CustomPageService } from '@app/shared/shared-main/custom-page'
 | 
			
		||||
import { CustomConfig, CustomPage, ServerConfig } from '@shared/models'
 | 
			
		||||
import { EditConfigurationService } from './edit-configuration.service'
 | 
			
		||||
 | 
			
		||||
type ComponentCustomConfig = CustomConfig & {
 | 
			
		||||
  instanceCustomHomepage: CustomPage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'my-edit-custom-config',
 | 
			
		||||
  templateUrl: './edit-custom-config.component.html',
 | 
			
		||||
| 
						 | 
				
			
			@ -35,9 +41,11 @@ import { EditConfigurationService } from './edit-configuration.service'
 | 
			
		|||
export class EditCustomConfigComponent extends FormReactive implements OnInit {
 | 
			
		||||
  activeNav: string
 | 
			
		||||
 | 
			
		||||
  customConfig: CustomConfig
 | 
			
		||||
  customConfig: ComponentCustomConfig
 | 
			
		||||
  serverConfig: ServerConfig
 | 
			
		||||
 | 
			
		||||
  homepage: CustomPage
 | 
			
		||||
 | 
			
		||||
  languageItems: SelectOptionsItem[] = []
 | 
			
		||||
  categoryItems: SelectOptionsItem[] = []
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +55,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
 | 
			
		|||
    protected formValidatorService: FormValidatorService,
 | 
			
		||||
    private notifier: Notifier,
 | 
			
		||||
    private configService: ConfigService,
 | 
			
		||||
    private customPage: CustomPageService,
 | 
			
		||||
    private serverService: ServerService,
 | 
			
		||||
    private editConfigurationService: EditConfigurationService
 | 
			
		||||
  ) {
 | 
			
		||||
| 
						 | 
				
			
			@ -56,11 +65,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
 | 
			
		|||
  ngOnInit () {
 | 
			
		||||
    this.serverConfig = this.serverService.getTmpConfig()
 | 
			
		||||
    this.serverService.getConfig()
 | 
			
		||||
        .subscribe(config => {
 | 
			
		||||
          this.serverConfig = config
 | 
			
		||||
        })
 | 
			
		||||
        .subscribe(config => this.serverConfig = config)
 | 
			
		||||
 | 
			
		||||
    const formGroupData: { [key in keyof CustomConfig ]: any } = {
 | 
			
		||||
    const formGroupData: { [key in keyof ComponentCustomConfig ]: any } = {
 | 
			
		||||
      instance: {
 | 
			
		||||
        name: INSTANCE_NAME_VALIDATOR,
 | 
			
		||||
        shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
 | 
			
		||||
| 
						 | 
				
			
			@ -215,6 +222,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
 | 
			
		|||
          disableLocalSearch: null,
 | 
			
		||||
          isDefaultSearch: null
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      instanceCustomHomepage: {
 | 
			
		||||
        content: null
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -250,15 +261,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  async formValidated () {
 | 
			
		||||
    const value: CustomConfig = this.form.getRawValue()
 | 
			
		||||
    const value: ComponentCustomConfig = this.form.getRawValue()
 | 
			
		||||
 | 
			
		||||
    this.configService.updateCustomConfig(value)
 | 
			
		||||
    forkJoin([
 | 
			
		||||
      this.configService.updateCustomConfig(omit(value, 'instanceCustomHomepage')),
 | 
			
		||||
      this.customPage.updateInstanceHomepage(value.instanceCustomHomepage.content)
 | 
			
		||||
    ])
 | 
			
		||||
      .subscribe(
 | 
			
		||||
        res => {
 | 
			
		||||
          this.customConfig = res
 | 
			
		||||
        ([ resConfig ]) => {
 | 
			
		||||
          const instanceCustomHomepage = {
 | 
			
		||||
            content: value.instanceCustomHomepage.content
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.customConfig = { ...resConfig, instanceCustomHomepage }
 | 
			
		||||
 | 
			
		||||
          // Reload general configuration
 | 
			
		||||
          this.serverService.resetConfig()
 | 
			
		||||
            .subscribe(config => this.serverConfig = config)
 | 
			
		||||
 | 
			
		||||
          this.updateForm()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -317,9 +336,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  private loadConfigAndUpdateForm () {
 | 
			
		||||
    this.configService.getCustomConfig()
 | 
			
		||||
      .subscribe(config => {
 | 
			
		||||
        this.customConfig = config
 | 
			
		||||
    forkJoin([
 | 
			
		||||
      this.configService.getCustomConfig(),
 | 
			
		||||
      this.customPage.getInstanceHomepage()
 | 
			
		||||
    ])
 | 
			
		||||
      .subscribe(([ config, homepage ]) => {
 | 
			
		||||
        this.customConfig = { ...config, instanceCustomHomepage: homepage }
 | 
			
		||||
 | 
			
		||||
        this.updateForm()
 | 
			
		||||
        // Force form validation
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
<ng-container [formGroup]="form">
 | 
			
		||||
 | 
			
		||||
  <ng-container formGroupName="instanceCustomHomepage">
 | 
			
		||||
 | 
			
		||||
    <div class="form-row mt-5"> <!-- homepage grid -->
 | 
			
		||||
      <div class="form-group col-12 col-lg-4 col-xl-3">
 | 
			
		||||
        <div i18n class="inner-form-title">INSTANCE HOMEPAGE</div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
 | 
			
		||||
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
          <label i18n for="instanceCustomHomepageContent">Homepage</label>
 | 
			
		||||
 | 
			
		||||
          <my-markdown-textarea
 | 
			
		||||
            name="instanceCustomHomepageContent" formControlName="content" textareaMaxWidth="90%" textareaHeight="300px"
 | 
			
		||||
            [customMarkdownRenderer]="customMarkdownRenderer"
 | 
			
		||||
            [classes]="{ 'input-error': formErrors['instanceCustomHomepage.content'] }"
 | 
			
		||||
          ></my-markdown-textarea>
 | 
			
		||||
 | 
			
		||||
          <div *ngIf="formErrors.instanceCustomHomepage.content" class="form-error">{{ formErrors.instanceCustomHomepage.content }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
</ng-container>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import { Component, Input, OnInit } from '@angular/core'
 | 
			
		||||
import { FormGroup } from '@angular/forms'
 | 
			
		||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'my-edit-homepage',
 | 
			
		||||
  templateUrl: './edit-homepage.component.html',
 | 
			
		||||
  styleUrls: [ './edit-custom-config.component.scss' ]
 | 
			
		||||
})
 | 
			
		||||
export class EditHomepageComponent implements OnInit {
 | 
			
		||||
  @Input() form: FormGroup
 | 
			
		||||
  @Input() formErrors: any
 | 
			
		||||
 | 
			
		||||
  customMarkdownRenderer: (text: string) => Promise<HTMLElement>
 | 
			
		||||
 | 
			
		||||
  constructor (private customMarkup: CustomMarkupService) {
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit () {
 | 
			
		||||
    this.customMarkdownRenderer = async (text: string) => {
 | 
			
		||||
      return this.customMarkup.buildElement(text)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ export * from './edit-advanced-configuration.component'
 | 
			
		|||
export * from './edit-basic-configuration.component'
 | 
			
		||||
export * from './edit-configuration.service'
 | 
			
		||||
export * from './edit-custom-config.component'
 | 
			
		||||
export * from './edit-homepage.component'
 | 
			
		||||
export * from './edit-instance-information.component'
 | 
			
		||||
export * from './edit-live-configuration.component'
 | 
			
		||||
export * from './edit-vod-transcoding.component'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								client/src/app/+home/home-routing.module.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								client/src/app/+home/home-routing.module.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router'
 | 
			
		||||
import { MetaGuard } from '@ngx-meta/core'
 | 
			
		||||
import { HomeComponent } from './home.component'
 | 
			
		||||
 | 
			
		||||
const homeRoutes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: '',
 | 
			
		||||
    component: HomeComponent,
 | 
			
		||||
    canActivateChild: [ MetaGuard ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  imports: [ RouterModule.forChild(homeRoutes) ],
 | 
			
		||||
  exports: [ RouterModule ]
 | 
			
		||||
})
 | 
			
		||||
export class HomeRoutingModule {}
 | 
			
		||||
							
								
								
									
										4
									
								
								client/src/app/+home/home.component.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								client/src/app/+home/home.component.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
<div class="root margin-content">
 | 
			
		||||
  <div #contentWrapper></div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								client/src/app/+home/home.component.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								client/src/app/+home/home.component.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
.root {
 | 
			
		||||
  padding-top: 20px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								client/src/app/+home/home.component.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								client/src/app/+home/home.component.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
 | 
			
		||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
 | 
			
		||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
 | 
			
		||||
import { CustomPageService } from '@app/shared/shared-main/custom-page'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  templateUrl: './home.component.html',
 | 
			
		||||
  styleUrls: [ './home.component.scss' ]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export class HomeComponent implements OnInit {
 | 
			
		||||
  @ViewChild('contentWrapper') contentWrapper: ElementRef<HTMLInputElement>
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    private customMarkupService: CustomMarkupService,
 | 
			
		||||
    private customPageService: CustomPageService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  async ngOnInit () {
 | 
			
		||||
    this.customPageService.getInstanceHomepage()
 | 
			
		||||
      .subscribe(async ({ content }) => {
 | 
			
		||||
        const element = await this.customMarkupService.buildElement(content)
 | 
			
		||||
        this.contentWrapper.nativeElement.appendChild(element)
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								client/src/app/+home/home.module.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								client/src/app/+home/home.module.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
 | 
			
		||||
import { SharedMainModule } from '@app/shared/shared-main'
 | 
			
		||||
import { HomeRoutingModule } from './home-routing.module'
 | 
			
		||||
import { HomeComponent } from './home.component'
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  imports: [
 | 
			
		||||
    HomeRoutingModule,
 | 
			
		||||
 | 
			
		||||
    SharedMainModule,
 | 
			
		||||
    SharedCustomMarkupModule
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  declarations: [
 | 
			
		||||
    HomeComponent
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  exports: [
 | 
			
		||||
    HomeComponent
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  providers: [ ]
 | 
			
		||||
})
 | 
			
		||||
export class HomeModule { }
 | 
			
		||||
							
								
								
									
										3
									
								
								client/src/app/+home/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								client/src/app/+home/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
export * from './home-routing.module'
 | 
			
		||||
export * from './home.component'
 | 
			
		||||
export * from './home.module'
 | 
			
		||||
| 
						 | 
				
			
			@ -161,7 +161,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
 | 
			
		|||
    // Before HTML rendering restore line feed for markdown list compatibility
 | 
			
		||||
    const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n')
 | 
			
		||||
    const html = await this.markdownService.textMarkdownToHTML(commentText, true, true)
 | 
			
		||||
    this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html)
 | 
			
		||||
    this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(html)
 | 
			
		||||
    this.newParentComments = this.parentComments.concat([ this.comment ])
 | 
			
		||||
 | 
			
		||||
    if (this.comment.account) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -509,7 +509,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 | 
			
		|||
 | 
			
		||||
  private async setVideoDescriptionHTML () {
 | 
			
		||||
    const html = await this.markdownService.textMarkdownToHTML(this.video.description)
 | 
			
		||||
    this.videoHTMLDescription = await this.markdownService.processVideoTimestamps(html)
 | 
			
		||||
    this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setVideoLikesBarTooltipText () {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,10 @@ const routes: Routes = [
 | 
			
		|||
    canDeactivate: [ MenuGuards.open() ],
 | 
			
		||||
    loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'home',
 | 
			
		||||
    loadChildren: () => import('./+home/home.module').then(m => m.HomeModule)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: 'my-account',
 | 
			
		||||
    loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -231,7 +231,7 @@ export class AppComponent implements OnInit, AfterViewInit {
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        this.broadcastMessage = {
 | 
			
		||||
          message: await this.markdownService.completeMarkdownToHTML(messageConfig.message),
 | 
			
		||||
          message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true),
 | 
			
		||||
          dismissable: messageConfig.dismissable,
 | 
			
		||||
          class: classes[messageConfig.level]
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,19 @@
 | 
			
		|||
import { fromEvent } from 'rxjs'
 | 
			
		||||
import { debounceTime } from 'rxjs/operators'
 | 
			
		||||
import { Injectable } from '@angular/core'
 | 
			
		||||
import { GlobalIconName } from '@app/shared/shared-icons'
 | 
			
		||||
import { sortObjectComparator } from '@shared/core-utils/miscs/miscs'
 | 
			
		||||
import { ServerConfig } from '@shared/models/server'
 | 
			
		||||
import { ScreenService } from '../wrappers'
 | 
			
		||||
 | 
			
		||||
export type MenuLink = {
 | 
			
		||||
  icon: GlobalIconName
 | 
			
		||||
  label: string
 | 
			
		||||
  menuLabel: string
 | 
			
		||||
  path: string
 | 
			
		||||
  priority: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MenuService {
 | 
			
		||||
  isMenuDisplayed = true
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +59,53 @@ export class MenuService {
 | 
			
		|||
    this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buildCommonLinks (config: ServerConfig) {
 | 
			
		||||
    let entries: MenuLink[] = [
 | 
			
		||||
      {
 | 
			
		||||
        icon: 'globe' as 'globe',
 | 
			
		||||
        label: $localize`Discover videos`,
 | 
			
		||||
        menuLabel: $localize`Discover`,
 | 
			
		||||
        path: '/videos/overview',
 | 
			
		||||
        priority: 150
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        icon: 'trending' as 'trending',
 | 
			
		||||
        label: $localize`Trending videos`,
 | 
			
		||||
        menuLabel: $localize`Trending`,
 | 
			
		||||
        path: '/videos/trending',
 | 
			
		||||
        priority: 140
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        icon: 'recently-added' as 'recently-added',
 | 
			
		||||
        label: $localize`Recently added videos`,
 | 
			
		||||
        menuLabel: $localize`Recently added`,
 | 
			
		||||
        path: '/videos/recently-added',
 | 
			
		||||
        priority: 130
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        icon: 'octagon' as 'octagon',
 | 
			
		||||
        label: $localize`Local videos`,
 | 
			
		||||
        menuLabel: $localize`Local videos`,
 | 
			
		||||
        path: '/videos/local',
 | 
			
		||||
        priority: 120
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if (config.homepage.enabled) {
 | 
			
		||||
      entries.push({
 | 
			
		||||
        icon: 'home' as 'home',
 | 
			
		||||
        label: $localize`Home`,
 | 
			
		||||
        menuLabel: $localize`Home`,
 | 
			
		||||
        path: '/home',
 | 
			
		||||
        priority: 160
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entries = entries.sort(sortObjectComparator('priority', 'desc'))
 | 
			
		||||
 | 
			
		||||
    return entries
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private handleWindowResize () {
 | 
			
		||||
    // On touch screens, do not handle window resize event since opened menu is handled with a content overlay
 | 
			
		||||
    if (this.screenService.isInTouchScreen()) return
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { Injectable } from '@angular/core'
 | 
			
		||||
import { LinkifierService } from './linkifier.service'
 | 
			
		||||
import { SANITIZE_OPTIONS } from '@shared/core-utils/renderer/html'
 | 
			
		||||
import { getCustomMarkupSanitizeOptions, getSanitizeOptions } from '@shared/core-utils/renderer/html'
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class HtmlRendererService {
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ export class HtmlRendererService {
 | 
			
		|||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async toSafeHtml (text: string) {
 | 
			
		||||
  async toSafeHtml (text: string, additionalAllowedTags: string[] = []) {
 | 
			
		||||
    const [ html ] = await Promise.all([
 | 
			
		||||
      // Convert possible markdown to html
 | 
			
		||||
      this.linkifier.linkify(text),
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +28,11 @@ export class HtmlRendererService {
 | 
			
		|||
      this.loadSanitizeHtml()
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    return this.sanitizeHtml(html, SANITIZE_OPTIONS)
 | 
			
		||||
    const options = additionalAllowedTags.length !== 0
 | 
			
		||||
      ? getCustomMarkupSanitizeOptions(additionalAllowedTags)
 | 
			
		||||
      : getSanitizeOptions()
 | 
			
		||||
 | 
			
		||||
    return this.sanitizeHtml(html, options)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async loadSanitizeHtml () {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,12 +17,15 @@ type MarkdownParsers = {
 | 
			
		|||
  enhancedMarkdownIt: MarkdownIt
 | 
			
		||||
  enhancedWithHTMLMarkdownIt: MarkdownIt
 | 
			
		||||
 | 
			
		||||
  completeMarkdownIt: MarkdownIt
 | 
			
		||||
  unsafeMarkdownIt: MarkdownIt
 | 
			
		||||
 | 
			
		||||
  customPageMarkdownIt: MarkdownIt
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MarkdownConfig = {
 | 
			
		||||
  rules: string[]
 | 
			
		||||
  html: boolean
 | 
			
		||||
  breaks: boolean
 | 
			
		||||
  escape?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -35,18 +38,24 @@ export class MarkdownService {
 | 
			
		|||
  private markdownParsers: MarkdownParsers = {
 | 
			
		||||
    textMarkdownIt: null,
 | 
			
		||||
    textWithHTMLMarkdownIt: null,
 | 
			
		||||
 | 
			
		||||
    enhancedMarkdownIt: null,
 | 
			
		||||
    enhancedWithHTMLMarkdownIt: null,
 | 
			
		||||
    completeMarkdownIt: null
 | 
			
		||||
 | 
			
		||||
    unsafeMarkdownIt: null,
 | 
			
		||||
 | 
			
		||||
    customPageMarkdownIt: null
 | 
			
		||||
  }
 | 
			
		||||
  private parsersConfig: MarkdownParserConfigs = {
 | 
			
		||||
    textMarkdownIt: { rules: TEXT_RULES, html: false },
 | 
			
		||||
    textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, html: true, escape: true },
 | 
			
		||||
    textMarkdownIt: { rules: TEXT_RULES, breaks: true, html: false },
 | 
			
		||||
    textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, breaks: true, html: true, escape: true },
 | 
			
		||||
 | 
			
		||||
    enhancedMarkdownIt: { rules: ENHANCED_RULES, html: false },
 | 
			
		||||
    enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, html: true, escape: true },
 | 
			
		||||
    enhancedMarkdownIt: { rules: ENHANCED_RULES, breaks: true, html: false },
 | 
			
		||||
    enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, breaks: true, html: true, escape: true },
 | 
			
		||||
 | 
			
		||||
    completeMarkdownIt: { rules: COMPLETE_RULES, html: true }
 | 
			
		||||
    unsafeMarkdownIt: { rules: COMPLETE_RULES, breaks: true, html: true, escape: false },
 | 
			
		||||
 | 
			
		||||
    customPageMarkdownIt: { rules: COMPLETE_RULES, breaks: false, html: true, escape: true }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private emojiModule: any
 | 
			
		||||
| 
						 | 
				
			
			@ -54,22 +63,26 @@ export class MarkdownService {
 | 
			
		|||
  constructor (private htmlRenderer: HtmlRendererService) {}
 | 
			
		||||
 | 
			
		||||
  textMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) {
 | 
			
		||||
    if (withHtml) return this.render('textWithHTMLMarkdownIt', markdown, withEmoji)
 | 
			
		||||
    if (withHtml) return this.render({ name: 'textWithHTMLMarkdownIt', markdown, withEmoji })
 | 
			
		||||
 | 
			
		||||
    return this.render('textMarkdownIt', markdown, withEmoji)
 | 
			
		||||
    return this.render({ name: 'textMarkdownIt', markdown, withEmoji })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  enhancedMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) {
 | 
			
		||||
    if (withHtml) return this.render('enhancedWithHTMLMarkdownIt', markdown, withEmoji)
 | 
			
		||||
    if (withHtml) return this.render({ name: 'enhancedWithHTMLMarkdownIt', markdown, withEmoji })
 | 
			
		||||
 | 
			
		||||
    return this.render('enhancedMarkdownIt', markdown, withEmoji)
 | 
			
		||||
    return this.render({ name: 'enhancedMarkdownIt', markdown, withEmoji })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  completeMarkdownToHTML (markdown: string) {
 | 
			
		||||
    return this.render('completeMarkdownIt', markdown, true)
 | 
			
		||||
  unsafeMarkdownToHTML (markdown: string, _trustedInput: true) {
 | 
			
		||||
    return this.render({ name: 'unsafeMarkdownIt', markdown, withEmoji: true })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async processVideoTimestamps (html: string) {
 | 
			
		||||
  customPageMarkdownToHTML (markdown: string, additionalAllowedTags: string[]) {
 | 
			
		||||
    return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  processVideoTimestamps (html: string) {
 | 
			
		||||
    return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) {
 | 
			
		||||
      const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
 | 
			
		||||
      const url = buildVideoLink({ startTime: t })
 | 
			
		||||
| 
						 | 
				
			
			@ -77,7 +90,13 @@ export class MarkdownService {
 | 
			
		|||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async render (name: keyof MarkdownParsers, markdown: string, withEmoji = false) {
 | 
			
		||||
  private async render (options: {
 | 
			
		||||
    name: keyof MarkdownParsers
 | 
			
		||||
    markdown: string
 | 
			
		||||
    withEmoji: boolean
 | 
			
		||||
    additionalAllowedTags?: string[]
 | 
			
		||||
  }) {
 | 
			
		||||
    const { name, markdown, withEmoji, additionalAllowedTags } = options
 | 
			
		||||
    if (!markdown) return ''
 | 
			
		||||
 | 
			
		||||
    const config = this.parsersConfig[ name ]
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +115,7 @@ export class MarkdownService {
 | 
			
		|||
    let html = this.markdownParsers[ name ].render(markdown)
 | 
			
		||||
    html = this.avoidTruncatedTags(html)
 | 
			
		||||
 | 
			
		||||
    if (config.escape) return this.htmlRenderer.toSafeHtml(html)
 | 
			
		||||
    if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags)
 | 
			
		||||
 | 
			
		||||
    return html
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +124,7 @@ export class MarkdownService {
 | 
			
		|||
    // FIXME: import('...') returns a struct module, containing a "default" field
 | 
			
		||||
    const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
 | 
			
		||||
 | 
			
		||||
    const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html })
 | 
			
		||||
    const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: config.breaks, html: config.html })
 | 
			
		||||
 | 
			
		||||
    for (const rule of config.rules) {
 | 
			
		||||
      markdownIt.enable(rule)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -173,6 +173,9 @@ export class ServerService {
 | 
			
		|||
        disableLocalSearch: false,
 | 
			
		||||
        isDefaultSearch: false
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    homepage: {
 | 
			
		||||
      enabled: false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -198,9 +201,7 @@ export class ServerService {
 | 
			
		|||
    this.configReset = true
 | 
			
		||||
 | 
			
		||||
    // Notify config update
 | 
			
		||||
    this.getConfig().subscribe(() => {
 | 
			
		||||
      // empty, to fire a reset config event
 | 
			
		||||
    })
 | 
			
		||||
    return this.getConfig()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getConfig () {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -123,24 +123,9 @@
 | 
			
		|||
      <div class="on-instance">
 | 
			
		||||
        <div i18n class="block-title">ON {{instanceName}}</div>
 | 
			
		||||
 | 
			
		||||
        <a class="menu-link" routerLink="/videos/overview" routerLinkActive="active">
 | 
			
		||||
          <my-global-icon iconName="globe" aria-hidden="true"></my-global-icon>
 | 
			
		||||
          <ng-container i18n>Discover</ng-container>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a class="menu-link" routerLink="/videos/trending" routerLinkActive="active">
 | 
			
		||||
          <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
 | 
			
		||||
          <ng-container i18n>Trending</ng-container>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a class="menu-link" routerLink="/videos/recently-added" routerLinkActive="active">
 | 
			
		||||
          <my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
 | 
			
		||||
          <ng-container i18n>Recently added</ng-container>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a class="menu-link" routerLink="/videos/local" routerLinkActive="active">
 | 
			
		||||
          <my-global-icon iconName="home" aria-hidden="true"></my-global-icon>
 | 
			
		||||
          <ng-container i18n>Local videos</ng-container>
 | 
			
		||||
        <a class="menu-link" *ngFor="let commonLink of commonMenuLinks" [routerLink]="commonLink.path" routerLinkActive="active">
 | 
			
		||||
          <my-global-icon [iconName]="commonLink.icon" aria-hidden="true"></my-global-icon>
 | 
			
		||||
          <ng-container>{{ commonLink.menuLabel }}</ng-container>
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,17 @@ import { switchMap } from 'rxjs/operators'
 | 
			
		|||
import { ViewportScroller } from '@angular/common'
 | 
			
		||||
import { Component, OnInit, ViewChild } from '@angular/core'
 | 
			
		||||
import { Router } from '@angular/router'
 | 
			
		||||
import { AuthService, AuthStatus, AuthUser, MenuService, RedirectService, ScreenService, ServerService, UserService } from '@app/core'
 | 
			
		||||
import {
 | 
			
		||||
  AuthService,
 | 
			
		||||
  AuthStatus,
 | 
			
		||||
  AuthUser,
 | 
			
		||||
  MenuLink,
 | 
			
		||||
  MenuService,
 | 
			
		||||
  RedirectService,
 | 
			
		||||
  ScreenService,
 | 
			
		||||
  ServerService,
 | 
			
		||||
  UserService
 | 
			
		||||
} from '@app/core'
 | 
			
		||||
import { scrollToTop } from '@app/helpers'
 | 
			
		||||
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
 | 
			
		||||
import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +45,8 @@ export class MenuComponent implements OnInit {
 | 
			
		|||
 | 
			
		||||
  currentInterfaceLanguage: string
 | 
			
		||||
 | 
			
		||||
  commonMenuLinks: MenuLink[] = []
 | 
			
		||||
 | 
			
		||||
  private languages: VideoConstant<string>[] = []
 | 
			
		||||
  private serverConfig: ServerConfig
 | 
			
		||||
  private routesPerRight: { [role in UserRight]?: string } = {
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +92,10 @@ export class MenuComponent implements OnInit {
 | 
			
		|||
  ngOnInit () {
 | 
			
		||||
    this.serverConfig = this.serverService.getTmpConfig()
 | 
			
		||||
    this.serverService.getConfig()
 | 
			
		||||
      .subscribe(config => this.serverConfig = config)
 | 
			
		||||
      .subscribe(config => {
 | 
			
		||||
        this.serverConfig = config
 | 
			
		||||
        this.buildMenuLinks()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    this.isLoggedIn = this.authService.isLoggedIn()
 | 
			
		||||
    if (this.isLoggedIn === true) {
 | 
			
		||||
| 
						 | 
				
			
			@ -241,6 +256,10 @@ export class MenuComponent implements OnInit {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private buildMenuLinks () {
 | 
			
		||||
    this.commonMenuLinks = this.menuService.buildCommonLinks(this.serverConfig)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private buildUserLanguages () {
 | 
			
		||||
    if (!this.user) {
 | 
			
		||||
      this.videoLanguages = []
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
<div *ngIf="channel" class="channel">
 | 
			
		||||
  <my-actor-avatar [channel]="channel" size="34"></my-actor-avatar>
 | 
			
		||||
 | 
			
		||||
  <div class="display-name">{{ channel.displayName }}</div>
 | 
			
		||||
  <div class="username">{{ channel.name }}</div>
 | 
			
		||||
 | 
			
		||||
  <div class="description">{{ channel.description }}</div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
@import '_variables';
 | 
			
		||||
@import '_mixins';
 | 
			
		||||
 | 
			
		||||
.channel {
 | 
			
		||||
  border-radius: 15px;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  width: min-content;
 | 
			
		||||
  border: 1px solid pvar(--mainColor);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
import { Component, Input, OnInit } from '@angular/core'
 | 
			
		||||
import { VideoChannel, VideoChannelService } from '../shared-main'
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Markup component that creates a channel miniature only
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'my-channel-miniature-markup',
 | 
			
		||||
  templateUrl: 'channel-miniature-markup.component.html',
 | 
			
		||||
  styleUrls: [ 'channel-miniature-markup.component.scss' ]
 | 
			
		||||
})
 | 
			
		||||
export class ChannelMiniatureMarkupComponent implements OnInit {
 | 
			
		||||
  @Input() name: string
 | 
			
		||||
 | 
			
		||||
  channel: VideoChannel
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    private channelService: VideoChannelService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit () {
 | 
			
		||||
    this.channelService.getVideoChannel(this.name)
 | 
			
		||||
      .subscribe(channel => this.channel = channel)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,136 @@
 | 
			
		|||
import { ComponentRef, Injectable } from '@angular/core'
 | 
			
		||||
import { MarkdownService } from '@app/core'
 | 
			
		||||
import {
 | 
			
		||||
  ChannelMiniatureMarkupData,
 | 
			
		||||
  EmbedMarkupData,
 | 
			
		||||
  PlaylistMiniatureMarkupData,
 | 
			
		||||
  VideoMiniatureMarkupData,
 | 
			
		||||
  VideosListMarkupData
 | 
			
		||||
} from '@shared/models'
 | 
			
		||||
import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
 | 
			
		||||
import { DynamicElementService } from './dynamic-element.service'
 | 
			
		||||
import { EmbedMarkupComponent } from './embed-markup.component'
 | 
			
		||||
import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
 | 
			
		||||
import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
 | 
			
		||||
import { VideosListMarkupComponent } from './videos-list-markup.component'
 | 
			
		||||
 | 
			
		||||
type BuilderFunction = (el: HTMLElement) => ComponentRef<any>
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CustomMarkupService {
 | 
			
		||||
  private builders: { [ selector: string ]: BuilderFunction } = {
 | 
			
		||||
    'peertube-video-embed': el => this.embedBuilder(el, 'video'),
 | 
			
		||||
    'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
 | 
			
		||||
    'peertube-video-miniature': el => this.videoMiniatureBuilder(el),
 | 
			
		||||
    'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el),
 | 
			
		||||
    'peertube-channel-miniature': el => this.channelMiniatureBuilder(el),
 | 
			
		||||
    'peertube-videos-list': el => this.videosListBuilder(el)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    private dynamicElementService: DynamicElementService,
 | 
			
		||||
    private markdown: MarkdownService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  async buildElement (text: string) {
 | 
			
		||||
    const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags())
 | 
			
		||||
 | 
			
		||||
    const rootElement = document.createElement('div')
 | 
			
		||||
    rootElement.innerHTML = html
 | 
			
		||||
 | 
			
		||||
    for (const selector of this.getSupportedTags()) {
 | 
			
		||||
      rootElement.querySelectorAll(selector)
 | 
			
		||||
        .forEach((e: HTMLElement) => {
 | 
			
		||||
          try {
 | 
			
		||||
            const component = this.execBuilder(selector, e)
 | 
			
		||||
 | 
			
		||||
            this.dynamicElementService.injectElement(e, component)
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            console.error('Cannot inject component %s.', selector, err)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return rootElement
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getSupportedTags () {
 | 
			
		||||
    return Object.keys(this.builders)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private execBuilder (selector: string, el: HTMLElement) {
 | 
			
		||||
    return this.builders[selector](el)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
 | 
			
		||||
    const data = el.dataset as EmbedMarkupData
 | 
			
		||||
    const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
 | 
			
		||||
 | 
			
		||||
    this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
 | 
			
		||||
 | 
			
		||||
    return component
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private videoMiniatureBuilder (el: HTMLElement) {
 | 
			
		||||
    const data = el.dataset as VideoMiniatureMarkupData
 | 
			
		||||
    const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
 | 
			
		||||
 | 
			
		||||
    this.dynamicElementService.setModel(component, { uuid: data.uuid })
 | 
			
		||||
 | 
			
		||||
    return component
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private playlistMiniatureBuilder (el: HTMLElement) {
 | 
			
		||||
    const data = el.dataset as PlaylistMiniatureMarkupData
 | 
			
		||||
    const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
 | 
			
		||||
 | 
			
		||||
    this.dynamicElementService.setModel(component, { uuid: data.uuid })
 | 
			
		||||
 | 
			
		||||
    return component
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private channelMiniatureBuilder (el: HTMLElement) {
 | 
			
		||||
    const data = el.dataset as ChannelMiniatureMarkupData
 | 
			
		||||
    const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
 | 
			
		||||
 | 
			
		||||
    this.dynamicElementService.setModel(component, { name: data.name })
 | 
			
		||||
 | 
			
		||||
    return component
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private videosListBuilder (el: HTMLElement) {
 | 
			
		||||
    const data = el.dataset as VideosListMarkupData
 | 
			
		||||
    const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
 | 
			
		||||
 | 
			
		||||
    const model = {
 | 
			
		||||
      title: data.title,
 | 
			
		||||
      description: data.description,
 | 
			
		||||
      sort: data.sort,
 | 
			
		||||
      categoryOneOf: this.buildArrayNumber(data.categoryOneOf),
 | 
			
		||||
      languageOneOf: this.buildArrayString(data.languageOneOf),
 | 
			
		||||
      count: this.buildNumber(data.count) || 10
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.dynamicElementService.setModel(component, model)
 | 
			
		||||
 | 
			
		||||
    return component
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private buildNumber (value: string) {
 | 
			
		||||
    if (!value) return undefined
 | 
			
		||||
 | 
			
		||||
    return parseInt(value, 10)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private buildArrayNumber (value: string) {
 | 
			
		||||
    if (!value) return undefined
 | 
			
		||||
 | 
			
		||||
    return value.split(',').map(v => parseInt(v, 10))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private buildArrayString (value: string) {
 | 
			
		||||
    if (!value) return undefined
 | 
			
		||||
 | 
			
		||||
    return value.split(',')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
import {
 | 
			
		||||
  ApplicationRef,
 | 
			
		||||
  ComponentFactoryResolver,
 | 
			
		||||
  ComponentRef,
 | 
			
		||||
  EmbeddedViewRef,
 | 
			
		||||
  Injectable,
 | 
			
		||||
  Injector,
 | 
			
		||||
  OnChanges,
 | 
			
		||||
  SimpleChange,
 | 
			
		||||
  SimpleChanges,
 | 
			
		||||
  Type
 | 
			
		||||
} from '@angular/core'
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class DynamicElementService {
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    private injector: Injector,
 | 
			
		||||
    private applicationRef: ApplicationRef,
 | 
			
		||||
    private componentFactoryResolver: ComponentFactoryResolver
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  createElement <T> (ofComponent: Type<T>) {
 | 
			
		||||
    const div = document.createElement('div')
 | 
			
		||||
 | 
			
		||||
    const component = this.componentFactoryResolver.resolveComponentFactory(ofComponent)
 | 
			
		||||
      .create(this.injector, [], div)
 | 
			
		||||
 | 
			
		||||
    return component
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  injectElement <T> (wrapper: HTMLElement, componentRef: ComponentRef<T>) {
 | 
			
		||||
    const hostView = componentRef.hostView as EmbeddedViewRef<any>
 | 
			
		||||
 | 
			
		||||
    this.applicationRef.attachView(hostView)
 | 
			
		||||
    wrapper.appendChild(hostView.rootNodes[0])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setModel <T> (componentRef: ComponentRef<T>, attributes: Partial<T>) {
 | 
			
		||||
    const changes: SimpleChanges = {}
 | 
			
		||||
 | 
			
		||||
    for (const key of Object.keys(attributes)) {
 | 
			
		||||
      const previousValue = componentRef.instance[key]
 | 
			
		||||
      const newValue = attributes[key]
 | 
			
		||||
 | 
			
		||||
      componentRef.instance[key] = newValue
 | 
			
		||||
      changes[key] = new SimpleChange(previousValue, newValue, previousValue === undefined)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const component = componentRef.instance
 | 
			
		||||
    if (typeof (component as unknown as OnChanges).ngOnChanges === 'function') {
 | 
			
		||||
      (component as unknown as OnChanges).ngOnChanges(changes)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentRef.changeDetectorRef.detectChanges()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
import { Component, ElementRef, Input, OnInit } from '@angular/core'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'my-embed-markup',
 | 
			
		||||
  template: ''
 | 
			
		||||
})
 | 
			
		||||
export class EmbedMarkupComponent implements OnInit {
 | 
			
		||||
  @Input() uuid: string
 | 
			
		||||
  @Input() type: 'video' | 'playlist' = 'video'
 | 
			
		||||
 | 
			
		||||
  constructor (private el: ElementRef) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit () {
 | 
			
		||||
    const link = this.type === 'video'
 | 
			
		||||
      ? buildVideoLink({ baseUrl: `${environment.originServerUrl}/videos/embed/${this.uuid}` })
 | 
			
		||||
      : buildPlaylistLink({ baseUrl: `${environment.originServerUrl}/video-playlists/embed/${this.uuid}` })
 | 
			
		||||
 | 
			
		||||
    this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								client/src/app/shared/shared-custom-markup/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								client/src/app/shared/shared-custom-markup/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
export * from './custom-markup.service'
 | 
			
		||||
export * from './dynamic-element.service'
 | 
			
		||||
export * from './shared-custom-markup.module'
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
<my-video-playlist-miniature *ngIf="playlist" [playlist]="playlist">
 | 
			
		||||
</my-video-playlist-miniature>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
@import '_variables';
 | 
			
		||||
@import '_mixins';
 | 
			
		||||
 | 
			
		||||
my-video-playlist-miniature {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  width: $video-thumbnail-width;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
import { Component, Input, OnInit } from '@angular/core'
 | 
			
		||||
import { MiniatureDisplayOptions } from '../shared-video-miniature'
 | 
			
		||||
import { VideoPlaylist, VideoPlaylistService } from '../shared-video-playlist'
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Markup component that creates a playlist miniature only
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'my-playlist-miniature-markup',
 | 
			
		||||
  templateUrl: 'playlist-miniature-markup.component.html',
 | 
			
		||||
  styleUrls: [ 'playlist-miniature-markup.component.scss' ]
 | 
			
		||||
})
 | 
			
		||||
export class PlaylistMiniatureMarkupComponent implements OnInit {
 | 
			
		||||
  @Input() uuid: string
 | 
			
		||||
 | 
			
		||||
  playlist: VideoPlaylist
 | 
			
		||||
 | 
			
		||||
  displayOptions: MiniatureDisplayOptions = {
 | 
			
		||||
    date: true,
 | 
			
		||||
    views: true,
 | 
			
		||||
    by: true,
 | 
			
		||||
    avatar: false,
 | 
			
		||||
    privacyLabel: false,
 | 
			
		||||
    privacyText: false,
 | 
			
		||||
    state: false,
 | 
			
		||||
    blacklistInfo: false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    private playlistService: VideoPlaylistService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit () {
 | 
			
		||||
    this.playlistService.getVideoPlaylist(this.uuid)
 | 
			
		||||
      .subscribe(playlist => this.playlist = playlist)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
 | 
			
		||||
import { CommonModule } from '@angular/common'
 | 
			
		||||
import { NgModule } from '@angular/core'
 | 
			
		||||
import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
 | 
			
		||||
import { SharedGlobalIconModule } from '../shared-icons'
 | 
			
		||||
import { SharedMainModule } from '../shared-main'
 | 
			
		||||
import { SharedVideoMiniatureModule } from '../shared-video-miniature'
 | 
			
		||||
import { SharedVideoPlaylistModule } from '../shared-video-playlist'
 | 
			
		||||
import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
 | 
			
		||||
import { CustomMarkupService } from './custom-markup.service'
 | 
			
		||||
import { DynamicElementService } from './dynamic-element.service'
 | 
			
		||||
import { EmbedMarkupComponent } from './embed-markup.component'
 | 
			
		||||
import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
 | 
			
		||||
import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
 | 
			
		||||
import { VideosListMarkupComponent } from './videos-list-markup.component'
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
 | 
			
		||||
    SharedMainModule,
 | 
			
		||||
    SharedGlobalIconModule,
 | 
			
		||||
    SharedVideoMiniatureModule,
 | 
			
		||||
    SharedVideoPlaylistModule,
 | 
			
		||||
    SharedActorImageModule
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  declarations: [
 | 
			
		||||
    VideoMiniatureMarkupComponent,
 | 
			
		||||
    PlaylistMiniatureMarkupComponent,
 | 
			
		||||
    ChannelMiniatureMarkupComponent,
 | 
			
		||||
    EmbedMarkupComponent,
 | 
			
		||||
    VideosListMarkupComponent
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  exports: [
 | 
			
		||||
    VideoMiniatureMarkupComponent,
 | 
			
		||||
    PlaylistMiniatureMarkupComponent,
 | 
			
		||||
    ChannelMiniatureMarkupComponent,
 | 
			
		||||
    VideosListMarkupComponent,
 | 
			
		||||
    EmbedMarkupComponent
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  providers: [
 | 
			
		||||
    CustomMarkupService,
 | 
			
		||||
    DynamicElementService
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class SharedCustomMarkupModule { }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
<my-video-miniature
 | 
			
		||||
  *ngIf="video"
 | 
			
		||||
  [video]="video" [user]="getUser()" [displayAsRow]="false"
 | 
			
		||||
  [displayVideoActions]="false" [displayOptions]="displayOptions"
 | 
			
		||||
>
 | 
			
		||||
</my-video-miniature>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
@import '_variables';
 | 
			
		||||
@import '_mixins';
 | 
			
		||||
 | 
			
		||||
my-video-miniature {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  width: $video-thumbnail-width;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
import { Component, Input, OnInit } from '@angular/core'
 | 
			
		||||
import { AuthService } from '@app/core'
 | 
			
		||||
import { Video, VideoService } from '../shared-main'
 | 
			
		||||
import { MiniatureDisplayOptions } from '../shared-video-miniature'
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Markup component that creates a video miniature only
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'my-video-miniature-markup',
 | 
			
		||||
  templateUrl: 'video-miniature-markup.component.html',
 | 
			
		||||
  styleUrls: [ 'video-miniature-markup.component.scss' ]
 | 
			
		||||
})
 | 
			
		||||
export class VideoMiniatureMarkupComponent implements OnInit {
 | 
			
		||||
  @Input() uuid: string
 | 
			
		||||
 | 
			
		||||
  video: Video
 | 
			
		||||
 | 
			
		||||
  displayOptions: MiniatureDisplayOptions = {
 | 
			
		||||
    date: true,
 | 
			
		||||
    views: true,
 | 
			
		||||
    by: true,
 | 
			
		||||
    avatar: false,
 | 
			
		||||
    privacyLabel: false,
 | 
			
		||||
    privacyText: false,
 | 
			
		||||
    state: false,
 | 
			
		||||
    blacklistInfo: false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    private auth: AuthService,
 | 
			
		||||
    private videoService: VideoService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  getUser () {
 | 
			
		||||
    return this.auth.getUser()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit () {
 | 
			
		||||
    this.videoService.getVideo({ videoId: this.uuid })
 | 
			
		||||
      .subscribe(video => this.video = video)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<div class="root">
 | 
			
		||||
  <h4 *ngIf="title">{{ title }}</h4>
 | 
			
		||||
  <div *ngIf="description" class="description">{{ description }}</div>
 | 
			
		||||
 | 
			
		||||
  <div class="videos">
 | 
			
		||||
    <my-video-miniature
 | 
			
		||||
      *ngFor="let video of videos"
 | 
			
		||||
      [video]="video" [user]="getUser()" [displayAsRow]="false"
 | 
			
		||||
      [displayVideoActions]="false" [displayOptions]="displayOptions"
 | 
			
		||||
    >
 | 
			
		||||
    </my-video-miniature>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
@import '_variables';
 | 
			
		||||
@import '_mixins';
 | 
			
		||||
 | 
			
		||||
my-video-miniature {
 | 
			
		||||
  margin-right: 15px;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  min-width: $video-thumbnail-width;
 | 
			
		||||
  max-width: $video-thumbnail-width;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,60 @@
 | 
			
		|||
import { Component, Input, OnInit } from '@angular/core'
 | 
			
		||||
import { AuthService } from '@app/core'
 | 
			
		||||
import { VideoSortField } from '@shared/models'
 | 
			
		||||
import { Video, VideoService } from '../shared-main'
 | 
			
		||||
import { MiniatureDisplayOptions } from '../shared-video-miniature'
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Markup component list videos depending on criterias
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'my-videos-list-markup',
 | 
			
		||||
  templateUrl: 'videos-list-markup.component.html',
 | 
			
		||||
  styleUrls: [ 'videos-list-markup.component.scss' ]
 | 
			
		||||
})
 | 
			
		||||
export class VideosListMarkupComponent implements OnInit {
 | 
			
		||||
  @Input() title: string
 | 
			
		||||
  @Input() description: string
 | 
			
		||||
  @Input() sort = '-publishedAt'
 | 
			
		||||
  @Input() categoryOneOf: number[]
 | 
			
		||||
  @Input() languageOneOf: string[]
 | 
			
		||||
  @Input() count = 10
 | 
			
		||||
 | 
			
		||||
  videos: Video[]
 | 
			
		||||
 | 
			
		||||
  displayOptions: MiniatureDisplayOptions = {
 | 
			
		||||
    date: true,
 | 
			
		||||
    views: true,
 | 
			
		||||
    by: true,
 | 
			
		||||
    avatar: false,
 | 
			
		||||
    privacyLabel: false,
 | 
			
		||||
    privacyText: false,
 | 
			
		||||
    state: false,
 | 
			
		||||
    blacklistInfo: false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    private auth: AuthService,
 | 
			
		||||
    private videoService: VideoService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  getUser () {
 | 
			
		||||
    return this.auth.getUser()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit () {
 | 
			
		||||
    const options = {
 | 
			
		||||
      videoPagination: {
 | 
			
		||||
        currentPage: 1,
 | 
			
		||||
        itemsPerPage: this.count
 | 
			
		||||
      },
 | 
			
		||||
      categoryOneOf: this.categoryOneOf,
 | 
			
		||||
      languageOneOf: this.languageOneOf,
 | 
			
		||||
      sort: this.sort as VideoSortField
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.videoService.getVideos(options)
 | 
			
		||||
      .subscribe(({ data }) => this.videos = data)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +19,7 @@
 | 
			
		|||
      <a ngbNavLink i18n>Complete preview</a>
 | 
			
		||||
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
        <div #previewElement></div>
 | 
			
		||||
        <div [innerHTML]="previewHTML"></div>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,10 @@
 | 
			
		|||
import { ViewportScroller } from '@angular/common'
 | 
			
		||||
import truncate from 'lodash-es/truncate'
 | 
			
		||||
import { Subject } from 'rxjs'
 | 
			
		||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
 | 
			
		||||
import { ViewportScroller } from '@angular/common'
 | 
			
		||||
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'
 | 
			
		||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
 | 
			
		||||
import { SafeHtml } from '@angular/platform-browser'
 | 
			
		||||
import { MarkdownService, ScreenService } from '@app/core'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
| 
						 | 
				
			
			@ -21,18 +22,27 @@ import { MarkdownService, ScreenService } from '@app/core'
 | 
			
		|||
 | 
			
		||||
export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
 | 
			
		||||
  @Input() content = ''
 | 
			
		||||
 | 
			
		||||
  @Input() classes: string[] | { [klass: string]: any[] | any } = []
 | 
			
		||||
 | 
			
		||||
  @Input() textareaMaxWidth = '100%'
 | 
			
		||||
  @Input() textareaHeight = '150px'
 | 
			
		||||
 | 
			
		||||
  @Input() truncate: number
 | 
			
		||||
 | 
			
		||||
  @Input() markdownType: 'text' | 'enhanced' = 'text'
 | 
			
		||||
  @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement>
 | 
			
		||||
 | 
			
		||||
  @Input() markdownVideo = false
 | 
			
		||||
 | 
			
		||||
  @Input() name = 'description'
 | 
			
		||||
 | 
			
		||||
  @ViewChild('textarea') textareaElement: ElementRef
 | 
			
		||||
  @ViewChild('previewElement') previewElement: ElementRef
 | 
			
		||||
 | 
			
		||||
  truncatedPreviewHTML: SafeHtml | string = ''
 | 
			
		||||
  previewHTML: SafeHtml | string = ''
 | 
			
		||||
 | 
			
		||||
  truncatedPreviewHTML = ''
 | 
			
		||||
  previewHTML = ''
 | 
			
		||||
  isMaximized = false
 | 
			
		||||
 | 
			
		||||
  maximizeInText = $localize`Maximize editor`
 | 
			
		||||
| 
						 | 
				
			
			@ -115,10 +125,31 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  private async markdownRender (text: string) {
 | 
			
		||||
    const html = this.markdownType === 'text' ?
 | 
			
		||||
      await this.markdownService.textMarkdownToHTML(text) :
 | 
			
		||||
      await this.markdownService.enhancedMarkdownToHTML(text)
 | 
			
		||||
    let html: string
 | 
			
		||||
 | 
			
		||||
    return this.markdownVideo ? this.markdownService.processVideoTimestamps(html) : html
 | 
			
		||||
    if (this.customMarkdownRenderer) {
 | 
			
		||||
      const result = await this.customMarkdownRenderer(text)
 | 
			
		||||
 | 
			
		||||
      if (result instanceof HTMLElement) {
 | 
			
		||||
        html = ''
 | 
			
		||||
 | 
			
		||||
        const wrapperElement = this.previewElement.nativeElement as HTMLElement
 | 
			
		||||
        wrapperElement.innerHTML = ''
 | 
			
		||||
        wrapperElement.appendChild(result)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      html = result
 | 
			
		||||
    } else if (this.markdownType === 'text') {
 | 
			
		||||
      html = await this.markdownService.textMarkdownToHTML(text)
 | 
			
		||||
    } else {
 | 
			
		||||
      html = await this.markdownService.enhancedMarkdownToHTML(text)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.markdownVideo) {
 | 
			
		||||
      html = this.markdownService.processVideoTimestamps(html)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,6 +72,7 @@ const icons = {
 | 
			
		|||
  'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
 | 
			
		||||
  'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default,
 | 
			
		||||
  'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default,
 | 
			
		||||
  'octagon': require('!!raw-loader?!../../../assets/images/feather/octagon.svg').default,
 | 
			
		||||
  'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
import { of } from 'rxjs'
 | 
			
		||||
import { catchError, map } from 'rxjs/operators'
 | 
			
		||||
import { HttpClient } from '@angular/common/http'
 | 
			
		||||
import { Injectable } from '@angular/core'
 | 
			
		||||
import { RestExtractor } from '@app/core'
 | 
			
		||||
import { CustomPage } from '@shared/models'
 | 
			
		||||
import { environment } from '../../../../environments/environment'
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class CustomPageService {
 | 
			
		||||
  static BASE_INSTANCE_HOMEPAGE_URL = environment.apiUrl + '/api/v1/custom-pages/homepage/instance'
 | 
			
		||||
 | 
			
		||||
  constructor (
 | 
			
		||||
    private authHttp: HttpClient,
 | 
			
		||||
    private restExtractor: RestExtractor
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  getInstanceHomepage () {
 | 
			
		||||
    return this.authHttp.get<CustomPage>(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL)
 | 
			
		||||
                        .pipe(
 | 
			
		||||
                          catchError(err => {
 | 
			
		||||
                            if (err.status === 404) {
 | 
			
		||||
                              return of({ content: '' })
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            this.restExtractor.handleError(err)
 | 
			
		||||
                          })
 | 
			
		||||
                        )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateInstanceHomepage (content: string) {
 | 
			
		||||
    return this.authHttp.put(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL, { content })
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map(this.restExtractor.extractDataBool),
 | 
			
		||||
        catchError(err => this.restExtractor.handleError(err))
 | 
			
		||||
      )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								client/src/app/shared/shared-main/custom-page/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								client/src/app/shared/shared-main/custom-page/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export * from './custom-page.service'
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +29,7 @@ import {
 | 
			
		|||
} from './angular'
 | 
			
		||||
import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
 | 
			
		||||
import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons'
 | 
			
		||||
import { CustomPageService } from './custom-page'
 | 
			
		||||
import { DateToggleComponent } from './date'
 | 
			
		||||
import { FeedComponent } from './feeds'
 | 
			
		||||
import { LoaderComponent, SmallLoaderComponent } from './loaders'
 | 
			
		||||
| 
						 | 
				
			
			@ -171,7 +172,9 @@ import { VideoChannelService } from './video-channel'
 | 
			
		|||
 | 
			
		||||
    VideoCaptionService,
 | 
			
		||||
 | 
			
		||||
    VideoChannelService
 | 
			
		||||
    VideoChannelService,
 | 
			
		||||
 | 
			
		||||
    CustomPageService
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class SharedMainModule { }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								client/src/assets/images/feather/octagon.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								client/src/assets/images/feather/octagon.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-octagon">
 | 
			
		||||
  <polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 323 B  | 
| 
						 | 
				
			
			@ -95,7 +95,7 @@ function buildVideoLink (options: {
 | 
			
		|||
function buildPlaylistLink (options: {
 | 
			
		||||
  baseUrl?: string
 | 
			
		||||
 | 
			
		||||
  playlistPosition: number
 | 
			
		||||
  playlistPosition?: number
 | 
			
		||||
}) {
 | 
			
		||||
  const { baseUrl } = options
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -127,6 +127,7 @@ import { PluginManager } from './server/lib/plugins/plugin-manager'
 | 
			
		|||
import { LiveManager } from './server/lib/live-manager'
 | 
			
		||||
import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
 | 
			
		||||
import { ServerConfigManager } from '@server/lib/server-config-manager'
 | 
			
		||||
 | 
			
		||||
// ----------- Command line -----------
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -262,7 +263,8 @@ async function startApplication () {
 | 
			
		|||
 | 
			
		||||
  await Promise.all([
 | 
			
		||||
    Emailer.Instance.checkConnection(),
 | 
			
		||||
    JobQueue.Instance.init()
 | 
			
		||||
    JobQueue.Instance.init(),
 | 
			
		||||
    ServerConfigManager.Instance.init()
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  // Caches initializations
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
import { ServerConfigManager } from '@server/lib/server-config-manager'
 | 
			
		||||
import * as express from 'express'
 | 
			
		||||
import { remove, writeJSON } from 'fs-extra'
 | 
			
		||||
import { snakeCase } from 'lodash'
 | 
			
		||||
import validator from 'validator'
 | 
			
		||||
import { getServerConfig } from '@server/lib/config'
 | 
			
		||||
import { UserRight } from '../../../shared'
 | 
			
		||||
import { About } from '../../../shared/models/server/about.model'
 | 
			
		||||
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ configRouter.delete('/custom',
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
async function getConfig (req: express.Request, res: express.Response) {
 | 
			
		||||
  const json = await getServerConfig(req.ip)
 | 
			
		||||
  const json = await ServerConfigManager.Instance.getServerConfig(req.ip)
 | 
			
		||||
 | 
			
		||||
  return res.json(json)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										42
									
								
								server/controllers/api/custom-page.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								server/controllers/api/custom-page.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
import * as express from 'express'
 | 
			
		||||
import { ServerConfigManager } from '@server/lib/server-config-manager'
 | 
			
		||||
import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
 | 
			
		||||
import { HttpStatusCode } from '@shared/core-utils'
 | 
			
		||||
import { UserRight } from '@shared/models'
 | 
			
		||||
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
 | 
			
		||||
 | 
			
		||||
const customPageRouter = express.Router()
 | 
			
		||||
 | 
			
		||||
customPageRouter.get('/homepage/instance',
 | 
			
		||||
  asyncMiddleware(getInstanceHomepage)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
customPageRouter.put('/homepage/instance',
 | 
			
		||||
  authenticate,
 | 
			
		||||
  ensureUserHasRight(UserRight.MANAGE_INSTANCE_CUSTOM_PAGE),
 | 
			
		||||
  asyncMiddleware(updateInstanceHomepage)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  customPageRouter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
async function getInstanceHomepage (req: express.Request, res: express.Response) {
 | 
			
		||||
  const page = await ActorCustomPageModel.loadInstanceHomepage()
 | 
			
		||||
  if (!page) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
 | 
			
		||||
 | 
			
		||||
  return res.json(page.toFormattedJSON())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function updateInstanceHomepage (req: express.Request, res: express.Response) {
 | 
			
		||||
  const content = req.body.content
 | 
			
		||||
 | 
			
		||||
  await ActorCustomPageModel.updateInstanceHomepage(content)
 | 
			
		||||
  ServerConfigManager.Instance.updateHomepageState(content)
 | 
			
		||||
 | 
			
		||||
  return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import { abuseRouter } from './abuse'
 | 
			
		|||
import { accountsRouter } from './accounts'
 | 
			
		||||
import { bulkRouter } from './bulk'
 | 
			
		||||
import { configRouter } from './config'
 | 
			
		||||
import { customPageRouter } from './custom-page'
 | 
			
		||||
import { jobsRouter } from './jobs'
 | 
			
		||||
import { oauthClientsRouter } from './oauth-clients'
 | 
			
		||||
import { overviewsRouter } from './overviews'
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +48,7 @@ apiRouter.use('/jobs', jobsRouter)
 | 
			
		|||
apiRouter.use('/search', searchRouter)
 | 
			
		||||
apiRouter.use('/overviews', overviewsRouter)
 | 
			
		||||
apiRouter.use('/plugins', pluginRouter)
 | 
			
		||||
apiRouter.use('/custom-pages', customPageRouter)
 | 
			
		||||
apiRouter.use('/ping', pong)
 | 
			
		||||
apiRouter.use('/*', badRequest)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import { move, readFile } from 'fs-extra'
 | 
			
		|||
import * as magnetUtil from 'magnet-uri'
 | 
			
		||||
import * as parseTorrent from 'parse-torrent'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { getEnabledResolutions } from '@server/lib/config'
 | 
			
		||||
import { ServerConfigManager } from '@server/lib/server-config-manager'
 | 
			
		||||
import { setVideoTags } from '@server/lib/video'
 | 
			
		||||
import { FilteredModelAttributes } from '@server/types'
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -134,7 +134,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
 | 
			
		|||
  const targetUrl = body.targetUrl
 | 
			
		||||
  const user = res.locals.oauth.token.User
 | 
			
		||||
 | 
			
		||||
  const youtubeDL = new YoutubeDL(targetUrl, getEnabledResolutions('vod'))
 | 
			
		||||
  const youtubeDL = new YoutubeDL(targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
 | 
			
		||||
 | 
			
		||||
  // Get video infos
 | 
			
		||||
  let youtubeDLInfo: YoutubeDLInfo
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import * as cors from 'cors'
 | 
			
		|||
import * as express from 'express'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { serveIndexHTML } from '@server/lib/client-html'
 | 
			
		||||
import { getEnabledResolutions, getRegisteredPlugins, getRegisteredThemes } from '@server/lib/config'
 | 
			
		||||
import { ServerConfigManager } from '@server/lib/server-config-manager'
 | 
			
		||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo/nodeinfo.model'
 | 
			
		||||
import { root } from '../helpers/core-utils'
 | 
			
		||||
| 
						 | 
				
			
			@ -203,10 +203,10 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
 | 
			
		|||
            }
 | 
			
		||||
          },
 | 
			
		||||
          plugin: {
 | 
			
		||||
            registered: getRegisteredPlugins()
 | 
			
		||||
            registered: ServerConfigManager.Instance.getRegisteredPlugins()
 | 
			
		||||
          },
 | 
			
		||||
          theme: {
 | 
			
		||||
            registered: getRegisteredThemes(),
 | 
			
		||||
            registered: ServerConfigManager.Instance.getRegisteredThemes(),
 | 
			
		||||
            default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
 | 
			
		||||
          },
 | 
			
		||||
          email: {
 | 
			
		||||
| 
						 | 
				
			
			@ -222,13 +222,13 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
 | 
			
		|||
            webtorrent: {
 | 
			
		||||
              enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
 | 
			
		||||
            },
 | 
			
		||||
            enabledResolutions: getEnabledResolutions('vod')
 | 
			
		||||
            enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('vod')
 | 
			
		||||
          },
 | 
			
		||||
          live: {
 | 
			
		||||
            enabled: CONFIG.LIVE.ENABLED,
 | 
			
		||||
            transcoding: {
 | 
			
		||||
              enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
 | 
			
		||||
              enabledResolutions: getEnabledResolutions('live')
 | 
			
		||||
              enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('live')
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          import: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
 | 
			
		||||
import { getSanitizeOptions, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
 | 
			
		||||
 | 
			
		||||
const sanitizeOptions = getSanitizeOptions()
 | 
			
		||||
 | 
			
		||||
const sanitizeHtml = require('sanitize-html')
 | 
			
		||||
const markdownItEmoji = require('markdown-it-emoji/light')
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +20,7 @@ const toSafeHtml = text => {
 | 
			
		|||
  const html = markdownIt.render(textWithLineFeed)
 | 
			
		||||
 | 
			
		||||
  // Convert to safe Html
 | 
			
		||||
  return sanitizeHtml(html, SANITIZE_OPTIONS)
 | 
			
		||||
  return sanitizeHtml(html, sanitizeOptions)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mdToPlainText = text => {
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +30,7 @@ const mdToPlainText = text => {
 | 
			
		|||
  const html = markdownIt.render(text)
 | 
			
		||||
 | 
			
		||||
  // Convert to safe Html
 | 
			
		||||
  const safeHtml = sanitizeHtml(html, SANITIZE_OPTIONS)
 | 
			
		||||
  const safeHtml = sanitizeHtml(html, sanitizeOptions)
 | 
			
		||||
 | 
			
		||||
  return safeHtml.replace(/<[^>]+>/g, '')
 | 
			
		||||
                 .replace(/\n$/, '')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
 | 
			
		|||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
const LAST_MIGRATION_VERSION = 645
 | 
			
		||||
const LAST_MIGRATION_VERSION = 650
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
 | 
			
		|||
import { VideoTagModel } from '../models/video/video-tag'
 | 
			
		||||
import { VideoViewModel } from '../models/video/video-view'
 | 
			
		||||
import { CONFIG } from './config'
 | 
			
		||||
import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
 | 
			
		||||
 | 
			
		||||
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -141,7 +142,8 @@ async function initDatabaseModels (silent: boolean) {
 | 
			
		|||
    ThumbnailModel,
 | 
			
		||||
    TrackerModel,
 | 
			
		||||
    VideoTrackerModel,
 | 
			
		||||
    PluginModel
 | 
			
		||||
    PluginModel,
 | 
			
		||||
    ActorCustomPageModel
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  // Check extensions exist in the database
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										33
									
								
								server/initializers/migrations/0650-actor-custom-pages.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								server/initializers/migrations/0650-actor-custom-pages.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
import * as Sequelize from 'sequelize'
 | 
			
		||||
 | 
			
		||||
async function up (utils: {
 | 
			
		||||
  transaction: Sequelize.Transaction
 | 
			
		||||
  queryInterface: Sequelize.QueryInterface
 | 
			
		||||
  sequelize: Sequelize.Sequelize
 | 
			
		||||
  db: any
 | 
			
		||||
}): Promise<void> {
 | 
			
		||||
  {
 | 
			
		||||
    const query = `
 | 
			
		||||
    CREATE TABLE IF NOT EXISTS "actorCustomPage" (
 | 
			
		||||
      "id" serial,
 | 
			
		||||
      "content" TEXT,
 | 
			
		||||
      "type" varchar(255) NOT NULL,
 | 
			
		||||
      "actorId" integer NOT NULL REFERENCES "actor" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
 | 
			
		||||
      "createdAt" timestamp WITH time zone NOT NULL,
 | 
			
		||||
      "updatedAt" timestamp WITH time zone NOT NULL,
 | 
			
		||||
      PRIMARY KEY ("id")
 | 
			
		||||
    );
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
    await utils.sequelize.query(query)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function down (options) {
 | 
			
		||||
  throw new Error('Not implemented.')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  up,
 | 
			
		||||
  down
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ import { VideoChannelModel } from '../models/video/video-channel'
 | 
			
		|||
import { getActivityStreamDuration } from '../models/video/video-format-utils'
 | 
			
		||||
import { VideoPlaylistModel } from '../models/video/video-playlist'
 | 
			
		||||
import { MAccountActor, MChannelActor } from '../types/models'
 | 
			
		||||
import { getHTMLServerConfig } from './config'
 | 
			
		||||
import { ServerConfigManager } from './server-config-manager'
 | 
			
		||||
 | 
			
		||||
type Tags = {
 | 
			
		||||
  ogType: string
 | 
			
		||||
| 
						 | 
				
			
			@ -211,7 +211,7 @@ class ClientHtml {
 | 
			
		|||
    if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
 | 
			
		||||
 | 
			
		||||
    const buffer = await readFile(path)
 | 
			
		||||
    const serverConfig = await getHTMLServerConfig()
 | 
			
		||||
    const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig()
 | 
			
		||||
 | 
			
		||||
    let html = buffer.toString()
 | 
			
		||||
    html = await ClientHtml.addAsyncPluginCSS(html)
 | 
			
		||||
| 
						 | 
				
			
			@ -280,7 +280,7 @@ class ClientHtml {
 | 
			
		|||
    if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
 | 
			
		||||
 | 
			
		||||
    const buffer = await readFile(path)
 | 
			
		||||
    const serverConfig = await getHTMLServerConfig()
 | 
			
		||||
    const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig()
 | 
			
		||||
 | 
			
		||||
    let html = buffer.toString()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,274 +0,0 @@
 | 
			
		|||
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '@server/helpers/signup'
 | 
			
		||||
import { getServerCommit } from '@server/helpers/utils'
 | 
			
		||||
import { CONFIG, isEmailEnabled } from '@server/initializers/config'
 | 
			
		||||
import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants'
 | 
			
		||||
import { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
 | 
			
		||||
import { Hooks } from './plugins/hooks'
 | 
			
		||||
import { PluginManager } from './plugins/plugin-manager'
 | 
			
		||||
import { getThemeOrDefault } from './plugins/theme-utils'
 | 
			
		||||
import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles'
 | 
			
		||||
 | 
			
		||||
async function getServerConfig (ip?: string): Promise<ServerConfig> {
 | 
			
		||||
  const { allowed } = await Hooks.wrapPromiseFun(
 | 
			
		||||
    isSignupAllowed,
 | 
			
		||||
    {
 | 
			
		||||
      ip
 | 
			
		||||
    },
 | 
			
		||||
    'filter:api.user.signup.allowed.result'
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
 | 
			
		||||
 | 
			
		||||
  const signup = {
 | 
			
		||||
    allowed,
 | 
			
		||||
    allowedForCurrentIP,
 | 
			
		||||
    requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const htmlConfig = await getHTMLServerConfig()
 | 
			
		||||
 | 
			
		||||
  return { ...htmlConfig, signup }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Config injected in HTML
 | 
			
		||||
let serverCommit: string
 | 
			
		||||
async function getHTMLServerConfig (): Promise<HTMLServerConfig> {
 | 
			
		||||
  if (serverCommit === undefined) serverCommit = await getServerCommit()
 | 
			
		||||
 | 
			
		||||
  const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    instance: {
 | 
			
		||||
      name: CONFIG.INSTANCE.NAME,
 | 
			
		||||
      shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
 | 
			
		||||
      isNSFW: CONFIG.INSTANCE.IS_NSFW,
 | 
			
		||||
      defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
 | 
			
		||||
      defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
 | 
			
		||||
      customizations: {
 | 
			
		||||
        javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
 | 
			
		||||
        css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    search: {
 | 
			
		||||
      remoteUri: {
 | 
			
		||||
        users: CONFIG.SEARCH.REMOTE_URI.USERS,
 | 
			
		||||
        anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
 | 
			
		||||
      },
 | 
			
		||||
      searchIndex: {
 | 
			
		||||
        enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED,
 | 
			
		||||
        url: CONFIG.SEARCH.SEARCH_INDEX.URL,
 | 
			
		||||
        disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
 | 
			
		||||
        isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    plugin: {
 | 
			
		||||
      registered: getRegisteredPlugins(),
 | 
			
		||||
      registeredExternalAuths: getExternalAuthsPlugins(),
 | 
			
		||||
      registeredIdAndPassAuths: getIdAndPassAuthPlugins()
 | 
			
		||||
    },
 | 
			
		||||
    theme: {
 | 
			
		||||
      registered: getRegisteredThemes(),
 | 
			
		||||
      default: defaultTheme
 | 
			
		||||
    },
 | 
			
		||||
    email: {
 | 
			
		||||
      enabled: isEmailEnabled()
 | 
			
		||||
    },
 | 
			
		||||
    contactForm: {
 | 
			
		||||
      enabled: CONFIG.CONTACT_FORM.ENABLED
 | 
			
		||||
    },
 | 
			
		||||
    serverVersion: PEERTUBE_VERSION,
 | 
			
		||||
    serverCommit,
 | 
			
		||||
    transcoding: {
 | 
			
		||||
      hls: {
 | 
			
		||||
        enabled: CONFIG.TRANSCODING.HLS.ENABLED
 | 
			
		||||
      },
 | 
			
		||||
      webtorrent: {
 | 
			
		||||
        enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
 | 
			
		||||
      },
 | 
			
		||||
      enabledResolutions: getEnabledResolutions('vod'),
 | 
			
		||||
      profile: CONFIG.TRANSCODING.PROFILE,
 | 
			
		||||
      availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod')
 | 
			
		||||
    },
 | 
			
		||||
    live: {
 | 
			
		||||
      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,
 | 
			
		||||
        enabledResolutions: getEnabledResolutions('live'),
 | 
			
		||||
        profile: CONFIG.LIVE.TRANSCODING.PROFILE,
 | 
			
		||||
        availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live')
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      rtmp: {
 | 
			
		||||
        port: CONFIG.LIVE.RTMP.PORT
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    import: {
 | 
			
		||||
      videos: {
 | 
			
		||||
        http: {
 | 
			
		||||
          enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
 | 
			
		||||
        },
 | 
			
		||||
        torrent: {
 | 
			
		||||
          enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    autoBlacklist: {
 | 
			
		||||
      videos: {
 | 
			
		||||
        ofUsers: {
 | 
			
		||||
          enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    avatar: {
 | 
			
		||||
      file: {
 | 
			
		||||
        size: {
 | 
			
		||||
          max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
 | 
			
		||||
        },
 | 
			
		||||
        extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    banner: {
 | 
			
		||||
      file: {
 | 
			
		||||
        size: {
 | 
			
		||||
          max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
 | 
			
		||||
        },
 | 
			
		||||
        extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    video: {
 | 
			
		||||
      image: {
 | 
			
		||||
        extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
 | 
			
		||||
        size: {
 | 
			
		||||
          max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      file: {
 | 
			
		||||
        extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    videoCaption: {
 | 
			
		||||
      file: {
 | 
			
		||||
        size: {
 | 
			
		||||
          max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
 | 
			
		||||
        },
 | 
			
		||||
        extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    user: {
 | 
			
		||||
      videoQuota: CONFIG.USER.VIDEO_QUOTA,
 | 
			
		||||
      videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
 | 
			
		||||
    },
 | 
			
		||||
    trending: {
 | 
			
		||||
      videos: {
 | 
			
		||||
        intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS,
 | 
			
		||||
        algorithms: {
 | 
			
		||||
          enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED,
 | 
			
		||||
          default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    tracker: {
 | 
			
		||||
      enabled: CONFIG.TRACKER.ENABLED
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    followings: {
 | 
			
		||||
      instance: {
 | 
			
		||||
        autoFollowIndex: {
 | 
			
		||||
          indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    broadcastMessage: {
 | 
			
		||||
      enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
 | 
			
		||||
      message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
 | 
			
		||||
      level: CONFIG.BROADCAST_MESSAGE.LEVEL,
 | 
			
		||||
      dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRegisteredThemes () {
 | 
			
		||||
  return PluginManager.Instance.getRegisteredThemes()
 | 
			
		||||
                      .map(t => ({
 | 
			
		||||
                        name: t.name,
 | 
			
		||||
                        version: t.version,
 | 
			
		||||
                        description: t.description,
 | 
			
		||||
                        css: t.css,
 | 
			
		||||
                        clientScripts: t.clientScripts
 | 
			
		||||
                      }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRegisteredPlugins () {
 | 
			
		||||
  return PluginManager.Instance.getRegisteredPlugins()
 | 
			
		||||
                      .map(p => ({
 | 
			
		||||
                        name: p.name,
 | 
			
		||||
                        version: p.version,
 | 
			
		||||
                        description: p.description,
 | 
			
		||||
                        clientScripts: p.clientScripts
 | 
			
		||||
                      }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getEnabledResolutions (type: 'vod' | 'live') {
 | 
			
		||||
  const transcoding = type === 'vod'
 | 
			
		||||
    ? CONFIG.TRANSCODING
 | 
			
		||||
    : CONFIG.LIVE.TRANSCODING
 | 
			
		||||
 | 
			
		||||
  return Object.keys(transcoding.RESOLUTIONS)
 | 
			
		||||
               .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
 | 
			
		||||
               .map(r => parseInt(r, 10))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  getServerConfig,
 | 
			
		||||
  getRegisteredThemes,
 | 
			
		||||
  getEnabledResolutions,
 | 
			
		||||
  getRegisteredPlugins,
 | 
			
		||||
  getHTMLServerConfig
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
function getIdAndPassAuthPlugins () {
 | 
			
		||||
  const result: RegisteredIdAndPassAuthConfig[] = []
 | 
			
		||||
 | 
			
		||||
  for (const p of PluginManager.Instance.getIdAndPassAuths()) {
 | 
			
		||||
    for (const auth of p.idAndPassAuths) {
 | 
			
		||||
      result.push({
 | 
			
		||||
        npmName: p.npmName,
 | 
			
		||||
        name: p.name,
 | 
			
		||||
        version: p.version,
 | 
			
		||||
        authName: auth.authName,
 | 
			
		||||
        weight: auth.getWeight()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getExternalAuthsPlugins () {
 | 
			
		||||
  const result: RegisteredExternalAuthConfig[] = []
 | 
			
		||||
 | 
			
		||||
  for (const p of PluginManager.Instance.getExternalAuths()) {
 | 
			
		||||
    for (const auth of p.externalAuths) {
 | 
			
		||||
      result.push({
 | 
			
		||||
        npmName: p.npmName,
 | 
			
		||||
        name: p.name,
 | 
			
		||||
        version: p.version,
 | 
			
		||||
        authName: auth.authName,
 | 
			
		||||
        authDisplayName: auth.authDisplayName()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,8 +2,10 @@ import * as Bull from 'bull'
 | 
			
		|||
import { move, remove, stat } from 'fs-extra'
 | 
			
		||||
import { extname } from 'path'
 | 
			
		||||
import { retryTransactionWrapper } from '@server/helpers/database-utils'
 | 
			
		||||
import { YoutubeDL } from '@server/helpers/youtube-dl'
 | 
			
		||||
import { isPostImportVideoAccepted } from '@server/lib/moderation'
 | 
			
		||||
import { Hooks } from '@server/lib/plugins/hooks'
 | 
			
		||||
import { ServerConfigManager } from '@server/lib/server-config-manager'
 | 
			
		||||
import { isAbleToUploadVideo } from '@server/lib/user'
 | 
			
		||||
import { addOptimizeOrMergeAudioJob } from '@server/lib/video'
 | 
			
		||||
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 | 
			
		||||
| 
						 | 
				
			
			@ -33,8 +35,6 @@ import { MThumbnail } from '../../../types/models/video/thumbnail'
 | 
			
		|||
import { federateVideoIfNeeded } from '../../activitypub/videos'
 | 
			
		||||
import { Notifier } from '../../notifier'
 | 
			
		||||
import { generateVideoMiniature } from '../../thumbnail'
 | 
			
		||||
import { YoutubeDL } from '@server/helpers/youtube-dl'
 | 
			
		||||
import { getEnabledResolutions } from '@server/lib/config'
 | 
			
		||||
 | 
			
		||||
async function processVideoImport (job: Bull.Job) {
 | 
			
		||||
  const payload = job.data as VideoImportPayload
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +76,7 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub
 | 
			
		|||
    videoImportId: videoImport.id
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const youtubeDL = new YoutubeDL(videoImport.targetUrl, getEnabledResolutions('vod'))
 | 
			
		||||
  const youtubeDL = new YoutubeDL(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
 | 
			
		||||
 | 
			
		||||
  return processFile(
 | 
			
		||||
    () => youtubeDL.downloadYoutubeDLVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ import { MPlugin } from '@server/types/models'
 | 
			
		|||
import { PeerTubeHelpers } from '@server/types/plugins'
 | 
			
		||||
import { VideoBlacklistCreate } from '@shared/models'
 | 
			
		||||
import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist'
 | 
			
		||||
import { getServerConfig } from '../config'
 | 
			
		||||
import { ServerConfigManager } from '../server-config-manager'
 | 
			
		||||
import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
 | 
			
		||||
import { UserModel } from '@server/models/user/user'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +147,7 @@ function buildConfigHelpers () {
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    getServerConfig () {
 | 
			
		||||
      return getServerConfig()
 | 
			
		||||
      return ServerConfigManager.Instance.getServerConfig()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										303
									
								
								server/lib/server-config-manager.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								server/lib/server-config-manager.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,303 @@
 | 
			
		|||
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '@server/helpers/signup'
 | 
			
		||||
import { getServerCommit } from '@server/helpers/utils'
 | 
			
		||||
import { CONFIG, isEmailEnabled } from '@server/initializers/config'
 | 
			
		||||
import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants'
 | 
			
		||||
import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
 | 
			
		||||
import { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
 | 
			
		||||
import { Hooks } from './plugins/hooks'
 | 
			
		||||
import { PluginManager } from './plugins/plugin-manager'
 | 
			
		||||
import { getThemeOrDefault } from './plugins/theme-utils'
 | 
			
		||||
import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * Used to send the server config to clients (using REST/API or plugins API)
 | 
			
		||||
 * We need a singleton class to manage config state depending on external events (to build menu entries etc)
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
class ServerConfigManager {
 | 
			
		||||
 | 
			
		||||
  private static instance: ServerConfigManager
 | 
			
		||||
 | 
			
		||||
  private serverCommit: string
 | 
			
		||||
 | 
			
		||||
  private homepageEnabled = false
 | 
			
		||||
 | 
			
		||||
  private constructor () {}
 | 
			
		||||
 | 
			
		||||
  async init () {
 | 
			
		||||
    const instanceHomepage = await ActorCustomPageModel.loadInstanceHomepage()
 | 
			
		||||
 | 
			
		||||
    this.updateHomepageState(instanceHomepage?.content)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateHomepageState (content: string) {
 | 
			
		||||
    this.homepageEnabled = !!content
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getHTMLServerConfig (): Promise<HTMLServerConfig> {
 | 
			
		||||
    if (this.serverCommit === undefined) this.serverCommit = await getServerCommit()
 | 
			
		||||
 | 
			
		||||
    const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      instance: {
 | 
			
		||||
        name: CONFIG.INSTANCE.NAME,
 | 
			
		||||
        shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
 | 
			
		||||
        isNSFW: CONFIG.INSTANCE.IS_NSFW,
 | 
			
		||||
        defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
 | 
			
		||||
        defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
 | 
			
		||||
        customizations: {
 | 
			
		||||
          javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
 | 
			
		||||
          css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      search: {
 | 
			
		||||
        remoteUri: {
 | 
			
		||||
          users: CONFIG.SEARCH.REMOTE_URI.USERS,
 | 
			
		||||
          anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
 | 
			
		||||
        },
 | 
			
		||||
        searchIndex: {
 | 
			
		||||
          enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED,
 | 
			
		||||
          url: CONFIG.SEARCH.SEARCH_INDEX.URL,
 | 
			
		||||
          disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
 | 
			
		||||
          isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      plugin: {
 | 
			
		||||
        registered: this.getRegisteredPlugins(),
 | 
			
		||||
        registeredExternalAuths: this.getExternalAuthsPlugins(),
 | 
			
		||||
        registeredIdAndPassAuths: this.getIdAndPassAuthPlugins()
 | 
			
		||||
      },
 | 
			
		||||
      theme: {
 | 
			
		||||
        registered: this.getRegisteredThemes(),
 | 
			
		||||
        default: defaultTheme
 | 
			
		||||
      },
 | 
			
		||||
      email: {
 | 
			
		||||
        enabled: isEmailEnabled()
 | 
			
		||||
      },
 | 
			
		||||
      contactForm: {
 | 
			
		||||
        enabled: CONFIG.CONTACT_FORM.ENABLED
 | 
			
		||||
      },
 | 
			
		||||
      serverVersion: PEERTUBE_VERSION,
 | 
			
		||||
      serverCommit: this.serverCommit,
 | 
			
		||||
      transcoding: {
 | 
			
		||||
        hls: {
 | 
			
		||||
          enabled: CONFIG.TRANSCODING.HLS.ENABLED
 | 
			
		||||
        },
 | 
			
		||||
        webtorrent: {
 | 
			
		||||
          enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
 | 
			
		||||
        },
 | 
			
		||||
        enabledResolutions: this.getEnabledResolutions('vod'),
 | 
			
		||||
        profile: CONFIG.TRANSCODING.PROFILE,
 | 
			
		||||
        availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod')
 | 
			
		||||
      },
 | 
			
		||||
      live: {
 | 
			
		||||
        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,
 | 
			
		||||
          enabledResolutions: this.getEnabledResolutions('live'),
 | 
			
		||||
          profile: CONFIG.LIVE.TRANSCODING.PROFILE,
 | 
			
		||||
          availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live')
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        rtmp: {
 | 
			
		||||
          port: CONFIG.LIVE.RTMP.PORT
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      import: {
 | 
			
		||||
        videos: {
 | 
			
		||||
          http: {
 | 
			
		||||
            enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
 | 
			
		||||
          },
 | 
			
		||||
          torrent: {
 | 
			
		||||
            enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      autoBlacklist: {
 | 
			
		||||
        videos: {
 | 
			
		||||
          ofUsers: {
 | 
			
		||||
            enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      avatar: {
 | 
			
		||||
        file: {
 | 
			
		||||
          size: {
 | 
			
		||||
            max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
 | 
			
		||||
          },
 | 
			
		||||
          extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      banner: {
 | 
			
		||||
        file: {
 | 
			
		||||
          size: {
 | 
			
		||||
            max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
 | 
			
		||||
          },
 | 
			
		||||
          extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      video: {
 | 
			
		||||
        image: {
 | 
			
		||||
          extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
 | 
			
		||||
          size: {
 | 
			
		||||
            max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        file: {
 | 
			
		||||
          extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      videoCaption: {
 | 
			
		||||
        file: {
 | 
			
		||||
          size: {
 | 
			
		||||
            max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
 | 
			
		||||
          },
 | 
			
		||||
          extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      user: {
 | 
			
		||||
        videoQuota: CONFIG.USER.VIDEO_QUOTA,
 | 
			
		||||
        videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
 | 
			
		||||
      },
 | 
			
		||||
      trending: {
 | 
			
		||||
        videos: {
 | 
			
		||||
          intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS,
 | 
			
		||||
          algorithms: {
 | 
			
		||||
            enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED,
 | 
			
		||||
            default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      tracker: {
 | 
			
		||||
        enabled: CONFIG.TRACKER.ENABLED
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      followings: {
 | 
			
		||||
        instance: {
 | 
			
		||||
          autoFollowIndex: {
 | 
			
		||||
            indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      broadcastMessage: {
 | 
			
		||||
        enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
 | 
			
		||||
        message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
 | 
			
		||||
        level: CONFIG.BROADCAST_MESSAGE.LEVEL,
 | 
			
		||||
        dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      homepage: {
 | 
			
		||||
        enabled: this.homepageEnabled
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getServerConfig (ip?: string): Promise<ServerConfig> {
 | 
			
		||||
    const { allowed } = await Hooks.wrapPromiseFun(
 | 
			
		||||
      isSignupAllowed,
 | 
			
		||||
      {
 | 
			
		||||
        ip
 | 
			
		||||
      },
 | 
			
		||||
      'filter:api.user.signup.allowed.result'
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
 | 
			
		||||
 | 
			
		||||
    const signup = {
 | 
			
		||||
      allowed,
 | 
			
		||||
      allowedForCurrentIP,
 | 
			
		||||
      requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const htmlConfig = await this.getHTMLServerConfig()
 | 
			
		||||
 | 
			
		||||
    return { ...htmlConfig, signup }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRegisteredThemes () {
 | 
			
		||||
    return PluginManager.Instance.getRegisteredThemes()
 | 
			
		||||
                        .map(t => ({
 | 
			
		||||
                          name: t.name,
 | 
			
		||||
                          version: t.version,
 | 
			
		||||
                          description: t.description,
 | 
			
		||||
                          css: t.css,
 | 
			
		||||
                          clientScripts: t.clientScripts
 | 
			
		||||
                        }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRegisteredPlugins () {
 | 
			
		||||
    return PluginManager.Instance.getRegisteredPlugins()
 | 
			
		||||
                        .map(p => ({
 | 
			
		||||
                          name: p.name,
 | 
			
		||||
                          version: p.version,
 | 
			
		||||
                          description: p.description,
 | 
			
		||||
                          clientScripts: p.clientScripts
 | 
			
		||||
                        }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getEnabledResolutions (type: 'vod' | 'live') {
 | 
			
		||||
    const transcoding = type === 'vod'
 | 
			
		||||
      ? CONFIG.TRANSCODING
 | 
			
		||||
      : CONFIG.LIVE.TRANSCODING
 | 
			
		||||
 | 
			
		||||
    return Object.keys(transcoding.RESOLUTIONS)
 | 
			
		||||
                 .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
 | 
			
		||||
                 .map(r => parseInt(r, 10))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getIdAndPassAuthPlugins () {
 | 
			
		||||
    const result: RegisteredIdAndPassAuthConfig[] = []
 | 
			
		||||
 | 
			
		||||
    for (const p of PluginManager.Instance.getIdAndPassAuths()) {
 | 
			
		||||
      for (const auth of p.idAndPassAuths) {
 | 
			
		||||
        result.push({
 | 
			
		||||
          npmName: p.npmName,
 | 
			
		||||
          name: p.name,
 | 
			
		||||
          version: p.version,
 | 
			
		||||
          authName: auth.authName,
 | 
			
		||||
          weight: auth.getWeight()
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getExternalAuthsPlugins () {
 | 
			
		||||
    const result: RegisteredExternalAuthConfig[] = []
 | 
			
		||||
 | 
			
		||||
    for (const p of PluginManager.Instance.getExternalAuths()) {
 | 
			
		||||
      for (const auth of p.externalAuths) {
 | 
			
		||||
        result.push({
 | 
			
		||||
          npmName: p.npmName,
 | 
			
		||||
          name: p.name,
 | 
			
		||||
          version: p.version,
 | 
			
		||||
          authName: auth.authName,
 | 
			
		||||
          authDisplayName: auth.authDisplayName()
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get Instance () {
 | 
			
		||||
    return this.instance || (this.instance = new this())
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  ServerConfigManager
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										69
									
								
								server/models/account/actor-custom-page.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								server/models/account/actor-custom-page.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,69 @@
 | 
			
		|||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
 | 
			
		||||
import { CustomPage } from '@shared/models'
 | 
			
		||||
import { ActorModel } from '../actor/actor'
 | 
			
		||||
import { getServerActor } from '../application/application'
 | 
			
		||||
 | 
			
		||||
@Table({
 | 
			
		||||
  tableName: 'actorCustomPage',
 | 
			
		||||
  indexes: [
 | 
			
		||||
    {
 | 
			
		||||
      fields: [ 'actorId', 'type' ],
 | 
			
		||||
      unique: true
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class ActorCustomPageModel extends Model {
 | 
			
		||||
 | 
			
		||||
  @AllowNull(true)
 | 
			
		||||
  @Column(DataType.TEXT)
 | 
			
		||||
  content: string
 | 
			
		||||
 | 
			
		||||
  @AllowNull(false)
 | 
			
		||||
  @Column
 | 
			
		||||
  type: 'homepage'
 | 
			
		||||
 | 
			
		||||
  @CreatedAt
 | 
			
		||||
  createdAt: Date
 | 
			
		||||
 | 
			
		||||
  @UpdatedAt
 | 
			
		||||
  updatedAt: Date
 | 
			
		||||
 | 
			
		||||
  @ForeignKey(() => ActorModel)
 | 
			
		||||
  @Column
 | 
			
		||||
  actorId: number
 | 
			
		||||
 | 
			
		||||
  @BelongsTo(() => ActorModel, {
 | 
			
		||||
    foreignKey: {
 | 
			
		||||
      name: 'actorId',
 | 
			
		||||
      allowNull: false
 | 
			
		||||
    },
 | 
			
		||||
    onDelete: 'cascade'
 | 
			
		||||
  })
 | 
			
		||||
  Actor: ActorModel
 | 
			
		||||
 | 
			
		||||
  static async updateInstanceHomepage (content: string) {
 | 
			
		||||
    const serverActor = await getServerActor()
 | 
			
		||||
 | 
			
		||||
    return ActorCustomPageModel.upsert({
 | 
			
		||||
      content,
 | 
			
		||||
      actorId: serverActor.id,
 | 
			
		||||
      type: 'homepage'
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async loadInstanceHomepage () {
 | 
			
		||||
    const serverActor = await getServerActor()
 | 
			
		||||
 | 
			
		||||
    return ActorCustomPageModel.findOne({
 | 
			
		||||
      where: {
 | 
			
		||||
        actorId: serverActor.id
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toFormattedJSON (): CustomPage {
 | 
			
		||||
    return {
 | 
			
		||||
      content: this.content
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								server/tests/api/check-params/custom-pages.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								server/tests/api/check-params/custom-pages.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 | 
			
		||||
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import {
 | 
			
		||||
  cleanupTests,
 | 
			
		||||
  createUser,
 | 
			
		||||
  flushAndRunServer,
 | 
			
		||||
  ServerInfo,
 | 
			
		||||
  setAccessTokensToServers,
 | 
			
		||||
  userLogin
 | 
			
		||||
} from '../../../../shared/extra-utils'
 | 
			
		||||
import { makeGetRequest, makePutBodyRequest } from '../../../../shared/extra-utils/requests/requests'
 | 
			
		||||
 | 
			
		||||
describe('Test custom pages validators', function () {
 | 
			
		||||
  const path = '/api/v1/custom-pages/homepage/instance'
 | 
			
		||||
 | 
			
		||||
  let server: ServerInfo
 | 
			
		||||
  let userAccessToken: string
 | 
			
		||||
 | 
			
		||||
  // ---------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
  before(async function () {
 | 
			
		||||
    this.timeout(120000)
 | 
			
		||||
 | 
			
		||||
    server = await flushAndRunServer(1)
 | 
			
		||||
    await setAccessTokensToServers([ server ])
 | 
			
		||||
 | 
			
		||||
    const user = { username: 'user1', password: 'password' }
 | 
			
		||||
    await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
 | 
			
		||||
 | 
			
		||||
    userAccessToken = await userLogin(server, user)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('When updating instance homepage', function () {
 | 
			
		||||
 | 
			
		||||
    it('Should fail with an unauthenticated user', async function () {
 | 
			
		||||
      await makePutBodyRequest({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        path,
 | 
			
		||||
        fields: { content: 'super content' },
 | 
			
		||||
        statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should fail with a non admin user', async function () {
 | 
			
		||||
      await makePutBodyRequest({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        path,
 | 
			
		||||
        token: userAccessToken,
 | 
			
		||||
        fields: { content: 'super content' },
 | 
			
		||||
        statusCodeExpected: HttpStatusCode.FORBIDDEN_403
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('Should succeed with the correct params', async function () {
 | 
			
		||||
      await makePutBodyRequest({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        path,
 | 
			
		||||
        token: server.accessToken,
 | 
			
		||||
        fields: { content: 'super content' },
 | 
			
		||||
        statusCodeExpected: HttpStatusCode.NO_CONTENT_204
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('When getting instance homapage', function () {
 | 
			
		||||
 | 
			
		||||
    it('Should succeed with the correct params', async function () {
 | 
			
		||||
      await makeGetRequest({
 | 
			
		||||
        url: server.url,
 | 
			
		||||
        path,
 | 
			
		||||
        statusCodeExpected: HttpStatusCode.OK_200
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  after(async function () {
 | 
			
		||||
    await cleanupTests([ server ])
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import './accounts'
 | 
			
		|||
import './blocklist'
 | 
			
		||||
import './bulk'
 | 
			
		||||
import './config'
 | 
			
		||||
import './custom-pages'
 | 
			
		||||
import './contact-form'
 | 
			
		||||
import './debug'
 | 
			
		||||
import './follows'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										85
									
								
								server/tests/api/server/homepage.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								server/tests/api/server/homepage.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 | 
			
		||||
 | 
			
		||||
import 'mocha'
 | 
			
		||||
import * as chai from 'chai'
 | 
			
		||||
import { HttpStatusCode } from '@shared/core-utils'
 | 
			
		||||
import { CustomPage, ServerConfig } from '@shared/models'
 | 
			
		||||
import {
 | 
			
		||||
  cleanupTests,
 | 
			
		||||
  flushAndRunServer,
 | 
			
		||||
  getConfig,
 | 
			
		||||
  getInstanceHomepage,
 | 
			
		||||
  killallServers,
 | 
			
		||||
  reRunServer,
 | 
			
		||||
  ServerInfo,
 | 
			
		||||
  setAccessTokensToServers,
 | 
			
		||||
  updateInstanceHomepage
 | 
			
		||||
} from '../../../../shared/extra-utils/index'
 | 
			
		||||
 | 
			
		||||
const expect = chai.expect
 | 
			
		||||
 | 
			
		||||
async function getHomepageState (server: ServerInfo) {
 | 
			
		||||
  const res = await getConfig(server.url)
 | 
			
		||||
 | 
			
		||||
  const config = res.body as ServerConfig
 | 
			
		||||
  return config.homepage.enabled
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('Test instance homepage actions', function () {
 | 
			
		||||
  let server: ServerInfo
 | 
			
		||||
 | 
			
		||||
  before(async function () {
 | 
			
		||||
    this.timeout(30000)
 | 
			
		||||
 | 
			
		||||
    server = await flushAndRunServer(1)
 | 
			
		||||
    await setAccessTokensToServers([ server ])
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should not have a homepage', async function () {
 | 
			
		||||
    const state = await getHomepageState(server)
 | 
			
		||||
    expect(state).to.be.false
 | 
			
		||||
 | 
			
		||||
    await getInstanceHomepage(server.url, HttpStatusCode.NOT_FOUND_404)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should set a homepage', async function () {
 | 
			
		||||
    await updateInstanceHomepage(server.url, server.accessToken, '<picsou-magazine></picsou-magazine>')
 | 
			
		||||
 | 
			
		||||
    const res = await getInstanceHomepage(server.url)
 | 
			
		||||
    const page: CustomPage = res.body
 | 
			
		||||
    expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
 | 
			
		||||
 | 
			
		||||
    const state = await getHomepageState(server)
 | 
			
		||||
    expect(state).to.be.true
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should have the same homepage after a restart', async function () {
 | 
			
		||||
    this.timeout(30000)
 | 
			
		||||
 | 
			
		||||
    killallServers([ server ])
 | 
			
		||||
 | 
			
		||||
    await reRunServer(server)
 | 
			
		||||
 | 
			
		||||
    const res = await getInstanceHomepage(server.url)
 | 
			
		||||
    const page: CustomPage = res.body
 | 
			
		||||
    expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
 | 
			
		||||
 | 
			
		||||
    const state = await getHomepageState(server)
 | 
			
		||||
    expect(state).to.be.true
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should empty the homepage', async function () {
 | 
			
		||||
    await updateInstanceHomepage(server.url, server.accessToken, '')
 | 
			
		||||
 | 
			
		||||
    const res = await getInstanceHomepage(server.url)
 | 
			
		||||
    const page: CustomPage = res.body
 | 
			
		||||
    expect(page.content).to.be.empty
 | 
			
		||||
 | 
			
		||||
    const state = await getHomepageState(server)
 | 
			
		||||
    expect(state).to.be.false
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  after(async function () {
 | 
			
		||||
    await cleanupTests([ server ])
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import './email'
 | 
			
		|||
import './follow-constraints'
 | 
			
		||||
import './follows'
 | 
			
		||||
import './follows-moderation'
 | 
			
		||||
import './homepage'
 | 
			
		||||
import './handle-down'
 | 
			
		||||
import './jobs'
 | 
			
		||||
import './logs'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								server/types/models/account/actor-custom-page.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								server/types/models/account/actor-custom-page.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
 | 
			
		||||
import { ActorCustomPageModel } from '../../../models/account/actor-custom-page'
 | 
			
		||||
 | 
			
		||||
export type MActorCustomPage = Omit<ActorCustomPageModel, 'Actor'>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,2 +1,3 @@
 | 
			
		|||
export * from './account'
 | 
			
		||||
export * from './actor-custom-page'
 | 
			
		||||
export * from './account-blocklist'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,9 +28,24 @@ function isCatchable (value: any) {
 | 
			
		|||
  return value && typeof value.catch === 'function'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sortObjectComparator (key: string, order: 'asc' | 'desc') {
 | 
			
		||||
  return (a: any, b: any) => {
 | 
			
		||||
    if (a[key] < b[key]) {
 | 
			
		||||
      return order === 'asc' ? -1 : 1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (a[key] > b[key]) {
 | 
			
		||||
      return order === 'asc' ? 1 : -1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return 0
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  randomInt,
 | 
			
		||||
  compareSemVer,
 | 
			
		||||
  isPromise,
 | 
			
		||||
  isCatchable
 | 
			
		||||
  isCatchable,
 | 
			
		||||
  sortObjectComparator
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,25 +1,45 @@
 | 
			
		|||
export const SANITIZE_OPTIONS = {
 | 
			
		||||
  allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
 | 
			
		||||
  allowedSchemes: [ 'http', 'https' ],
 | 
			
		||||
  allowedAttributes: {
 | 
			
		||||
    a: [ 'href', 'class', 'target', 'rel' ]
 | 
			
		||||
  },
 | 
			
		||||
  transformTags: {
 | 
			
		||||
    a: (tagName: string, attribs: any) => {
 | 
			
		||||
      let rel = 'noopener noreferrer'
 | 
			
		||||
      if (attribs.rel === 'me') rel += ' me'
 | 
			
		||||
export function getSanitizeOptions () {
 | 
			
		||||
  return {
 | 
			
		||||
    allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
 | 
			
		||||
    allowedSchemes: [ 'http', 'https' ],
 | 
			
		||||
    allowedAttributes: {
 | 
			
		||||
      'a': [ 'href', 'class', 'target', 'rel' ],
 | 
			
		||||
      '*': [ 'data-*' ]
 | 
			
		||||
    },
 | 
			
		||||
    transformTags: {
 | 
			
		||||
      a: (tagName: string, attribs: any) => {
 | 
			
		||||
        let rel = 'noopener noreferrer'
 | 
			
		||||
        if (attribs.rel === 'me') rel += ' me'
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        tagName,
 | 
			
		||||
        attribs: Object.assign(attribs, {
 | 
			
		||||
          target: '_blank',
 | 
			
		||||
          rel
 | 
			
		||||
        })
 | 
			
		||||
        return {
 | 
			
		||||
          tagName,
 | 
			
		||||
          attribs: Object.assign(attribs, {
 | 
			
		||||
            target: '_blank',
 | 
			
		||||
            rel
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) {
 | 
			
		||||
  const base = getSanitizeOptions()
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    allowedTags: [
 | 
			
		||||
      ...base.allowedTags,
 | 
			
		||||
      ...additionalAllowedTags,
 | 
			
		||||
      'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
 | 
			
		||||
    ],
 | 
			
		||||
    allowedSchemes: base.allowedSchemes,
 | 
			
		||||
    allowedAttributes: {
 | 
			
		||||
      ...base.allowedAttributes,
 | 
			
		||||
      '*': [ 'data-*', 'style' ]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Thanks: https://stackoverflow.com/a/12034334
 | 
			
		||||
export function escapeHTML (stringParam: string) {
 | 
			
		||||
  if (!stringParam) return ''
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										31
									
								
								shared/extra-utils/custom-pages/custom-pages.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								shared/extra-utils/custom-pages/custom-pages.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 | 
			
		||||
import { makeGetRequest, makePutBodyRequest } from '../requests/requests'
 | 
			
		||||
 | 
			
		||||
function getInstanceHomepage (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
 | 
			
		||||
  const path = '/api/v1/custom-pages/homepage/instance'
 | 
			
		||||
 | 
			
		||||
  return makeGetRequest({
 | 
			
		||||
    url,
 | 
			
		||||
    path,
 | 
			
		||||
    statusCodeExpected
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateInstanceHomepage (url: string, token: string, content: string) {
 | 
			
		||||
  const path = '/api/v1/custom-pages/homepage/instance'
 | 
			
		||||
 | 
			
		||||
  return makePutBodyRequest({
 | 
			
		||||
    url,
 | 
			
		||||
    path,
 | 
			
		||||
    token,
 | 
			
		||||
    fields: { content },
 | 
			
		||||
    statusCodeExpected: HttpStatusCode.NO_CONTENT_204
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  getInstanceHomepage,
 | 
			
		||||
  updateInstanceHomepage
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,8 @@ export * from './bulk/bulk'
 | 
			
		|||
 | 
			
		||||
export * from './cli/cli'
 | 
			
		||||
 | 
			
		||||
export * from './custom-pages/custom-pages'
 | 
			
		||||
 | 
			
		||||
export * from './feeds/feeds'
 | 
			
		||||
 | 
			
		||||
export * from './mock-servers/mock-instances-index'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								shared/models/actors/custom-page.model.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								shared/models/actors/custom-page.model.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
export interface CustomPage {
 | 
			
		||||
  content: string
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,4 +2,5 @@ export * from './account.model'
 | 
			
		|||
export * from './actor-image.model'
 | 
			
		||||
export * from './actor-image.type'
 | 
			
		||||
export * from './actor.model'
 | 
			
		||||
export * from './custom-page.model'
 | 
			
		||||
export * from './follow.model'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										28
									
								
								shared/models/custom-markup/custom-markup-data.model.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								shared/models/custom-markup/custom-markup-data.model.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
export type EmbedMarkupData = {
 | 
			
		||||
  // Video or playlist uuid
 | 
			
		||||
  uuid: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type VideoMiniatureMarkupData = {
 | 
			
		||||
  // Video uuid
 | 
			
		||||
  uuid: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type PlaylistMiniatureMarkupData = {
 | 
			
		||||
  // Playlist uuid
 | 
			
		||||
  uuid: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ChannelMiniatureMarkupData = {
 | 
			
		||||
  // Channel name (username)
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type VideosListMarkupData = {
 | 
			
		||||
  title: string
 | 
			
		||||
  description: string
 | 
			
		||||
  sort: string
 | 
			
		||||
  categoryOneOf: string // coma separated values
 | 
			
		||||
  languageOneOf: string // coma separated values
 | 
			
		||||
  count: string
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								shared/models/custom-markup/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								shared/models/custom-markup/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export * from './custom-markup-data.model'
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
export * from './activitypub'
 | 
			
		||||
export * from './actors'
 | 
			
		||||
export * from './moderation'
 | 
			
		||||
export * from './custom-markup'
 | 
			
		||||
export * from './bulk'
 | 
			
		||||
export * from './redundancy'
 | 
			
		||||
export * from './users'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -214,6 +214,10 @@ export interface ServerConfig {
 | 
			
		|||
    level: BroadcastMessageLevel
 | 
			
		||||
    dismissable: boolean
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  homepage: {
 | 
			
		||||
    enabled: boolean
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type HTMLServerConfig = Omit<ServerConfig, 'signup'>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ export const enum UserRight {
 | 
			
		|||
  MANAGE_JOBS,
 | 
			
		||||
 | 
			
		||||
  MANAGE_CONFIGURATION,
 | 
			
		||||
  MANAGE_INSTANCE_CUSTOM_PAGE,
 | 
			
		||||
 | 
			
		||||
  MANAGE_ACCOUNTS_BLOCKLIST,
 | 
			
		||||
  MANAGE_SERVERS_BLOCKLIST,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -247,6 +247,8 @@ tags:
 | 
			
		|||
 | 
			
		||||
      Administrators can also enable the use of a remote search system, indexing
 | 
			
		||||
      videos and channels not could be not federated by the instance.
 | 
			
		||||
  - name: Homepage
 | 
			
		||||
    description: Get and update the custom homepage
 | 
			
		||||
  - name: Video Mirroring
 | 
			
		||||
    description: |
 | 
			
		||||
      PeerTube instances can mirror videos from one another, and help distribute some videos.
 | 
			
		||||
| 
						 | 
				
			
			@ -281,6 +283,9 @@ x-tagGroups:
 | 
			
		|||
  - name: Search
 | 
			
		||||
    tags:
 | 
			
		||||
      - Search
 | 
			
		||||
  - name: Custom pages
 | 
			
		||||
    tags:
 | 
			
		||||
      - Homepage
 | 
			
		||||
  - name: Moderation
 | 
			
		||||
    tags:
 | 
			
		||||
      - Abuses
 | 
			
		||||
| 
						 | 
				
			
			@ -477,6 +482,40 @@ paths:
 | 
			
		|||
        '200':
 | 
			
		||||
          description: successful operation
 | 
			
		||||
 | 
			
		||||
  /custom-pages/homepage/instance:
 | 
			
		||||
    get:
 | 
			
		||||
      summary: Get instance custom homepage
 | 
			
		||||
      tags:
 | 
			
		||||
        - Homepage
 | 
			
		||||
      responses:
 | 
			
		||||
        '404':
 | 
			
		||||
          description: No homepage set
 | 
			
		||||
        '200':
 | 
			
		||||
          description: successful operation
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/CustomHomepage'
 | 
			
		||||
    put:
 | 
			
		||||
      summary: Set instance custom homepage
 | 
			
		||||
      tags:
 | 
			
		||||
        - Homepage
 | 
			
		||||
      security:
 | 
			
		||||
        - OAuth2:
 | 
			
		||||
          - admin
 | 
			
		||||
      requestBody:
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              type: object
 | 
			
		||||
              properties:
 | 
			
		||||
                content:
 | 
			
		||||
                  type: string
 | 
			
		||||
                  description: content of the homepage, that will be injected in the client
 | 
			
		||||
      responses:
 | 
			
		||||
        '204':
 | 
			
		||||
          description: successful operation
 | 
			
		||||
 | 
			
		||||
  /jobs/{state}:
 | 
			
		||||
    get:
 | 
			
		||||
      summary: List instance jobs
 | 
			
		||||
| 
						 | 
				
			
			@ -5740,6 +5779,12 @@ components:
 | 
			
		|||
                    indexUrl:
 | 
			
		||||
                      type: string
 | 
			
		||||
                      format: url
 | 
			
		||||
        homepage:
 | 
			
		||||
          type: object
 | 
			
		||||
          properties:
 | 
			
		||||
            enabled:
 | 
			
		||||
              type: boolean
 | 
			
		||||
 | 
			
		||||
    ServerConfigAbout:
 | 
			
		||||
      properties:
 | 
			
		||||
        instance:
 | 
			
		||||
| 
						 | 
				
			
			@ -5930,6 +5975,12 @@ components:
 | 
			
		|||
                  type: boolean
 | 
			
		||||
                manualApproval:
 | 
			
		||||
                  type: boolean
 | 
			
		||||
 | 
			
		||||
    CustomHomepage:
 | 
			
		||||
      properties:
 | 
			
		||||
        content:
 | 
			
		||||
          type: string
 | 
			
		||||
 | 
			
		||||
    Follow:
 | 
			
		||||
      properties:
 | 
			
		||||
        id:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue