aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html53
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.scss19
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.ts27
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html135
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss9
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts68
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts47
-rw-r--r--client/src/app/+my-account/my-account.module.ts4
-rw-r--r--client/src/app/shared/shared.module.ts7
-rw-r--r--config/default.yaml47
-rw-r--r--server/controllers/api/config.ts21
-rw-r--r--server/initializers/config.ts10
-rw-r--r--server/tests/api/check-params/config.ts10
-rw-r--r--server/tests/api/server/config.ts30
-rw-r--r--shared/extra-utils/server/config.ts10
-rw-r--r--shared/models/server/about.model.ts10
-rw-r--r--shared/models/server/custom-config.model.ts10
18 files changed, 437 insertions, 82 deletions
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html
index 7c27ec760..0fd3626b7 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -1,7 +1,8 @@
1<div class="row"> 1<div class="row">
2 <div class="col-md-12 col-xl-6"> 2 <div class="col-md-12 col-xl-6">
3
3 <div class="about-instance-title"> 4 <div class="about-instance-title">
4 <div i18n>About {{ instanceName }} instance</div> 5 <div i18n class="title">About {{ instanceName }} instance</div>
5 6
6 <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div> 7 <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
7 </div> 8 </div>
@@ -12,16 +13,58 @@
12 <div *ngIf="isNSFW" class="dedicated-to-nsfw">This instance is dedicated to sensitive/NSFW content.</div> 13 <div *ngIf="isNSFW" class="dedicated-to-nsfw">This instance is dedicated to sensitive/NSFW content.</div>
13 </div> 14 </div>
14 15
15 <div class="description"> 16 <div class="middle-title" *ngIf="html.administrator || maintenanceLifetime || businessModel">
17 Administrators & sustainability
18 </div>
19
20 <div class="block administrator" *ngIf="html.administrator">
21 <div i18n class="section-title">Instance administrators</div>
22
23 <div [innerHTML]="html.administrator"></div>
24 </div>
25
26 <div class="block maintenance-lifetime" *ngIf="maintenanceLifetime">
27 <div i18n class="section-title">Maintenance lifetime</div>
28
29 <p>{{ maintenanceLifetime }}</p>
30 </div>
31
32 <div class="block business-model" *ngIf="businessModel">
33 <div i18n class="section-title">Business model</div>
34
35 <p>{{ businessModel }}</p>
36 </div>
37
38 <div class="middle-title" *ngIf="html.description">
39 Information
40 </div>
41
42 <div class="block description">
16 <div i18n class="section-title">Description</div> 43 <div i18n class="section-title">Description</div>
17 44
18 <div [innerHTML]="descriptionHTML"></div> 45 <div [innerHTML]="html.description"></div>
46 </div>
47
48 <div class="middle-title" *ngIf="html.moderationInformation || html.codeOfConduct || html.terms">
49 Moderation
50 </div>
51
52 <div class="block moderation-information" *ngIf="html.moderationInformation">
53 <div i18n class="section-title">Moderation information</div>
54
55 <div [innerHTML]="html.moderationInformation"></div>
56 </div>
57
58 <div class="block code-of-conduct" *ngIf="html.codeOfConduct">
59 <div i18n class="section-title">Code of conduct</div>
60
61 <div [innerHTML]="html.codeOfConduct"></div>
19 </div> 62 </div>
20 63
21 <div class="terms" id="terms-section"> 64 <div class="block terms" id="terms-section">
22 <div i18n class="section-title">Terms</div> 65 <div i18n class="section-title">Terms</div>
23 66
24 <div [innerHTML]="termsHTML"></div> 67 <div [innerHTML]="html.terms"></div>
25 </div> 68 </div>
26 </div> 69 </div>
27 70
diff --git a/client/src/app/+about/about-instance/about-instance.component.scss b/client/src/app/+about/about-instance/about-instance.component.scss
index 0296ae8e9..0585ad5f3 100644
--- a/client/src/app/+about/about-instance/about-instance.component.scss
+++ b/client/src/app/+about/about-instance/about-instance.component.scss
@@ -5,13 +5,13 @@
5 display: flex; 5 display: flex;
6 justify-content: space-between; 6 justify-content: space-between;
7 7
8 & > div { 8 .title {
9 font-size: 20px; 9 font-size: 20px;
10 font-weight: bold;
11 margin-bottom: 15px; 10 margin-bottom: 15px;
11 font-weight: $font-semibold;
12 } 12 }
13 13
14 & > .contact-admin { 14 .contact-admin {
15 @include peertube-button; 15 @include peertube-button;
16 @include orange-button; 16 @include orange-button;
17 17
@@ -21,11 +21,20 @@
21 21
22.section-title { 22.section-title {
23 font-weight: $font-semibold; 23 font-weight: $font-semibold;
24 font-size: 20px; 24 font-size: 16px;
25 margin-bottom: 5px; 25 margin-bottom: 5px;
26 display: flex;
27 align-items: center;
28}
29
30.middle-title {
31 @include in-content-small-title;
32
33 margin-top: 45px;
34 margin-bottom: 25px;
26} 35}
27 36
28.short-description, .description, .terms, .signup { 37.block {
29 margin-bottom: 30px; 38 margin-bottom: 30px;
30} 39}
31 40
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts
index a5204de27..b85a6be94 100644
--- a/client/src/app/+about/about-instance/about-instance.component.ts
+++ b/client/src/app/+about/about-instance/about-instance.component.ts
@@ -14,8 +14,20 @@ export class AboutInstanceComponent implements OnInit {
14 @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent 14 @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent
15 15
16 shortDescription = '' 16 shortDescription = ''
17 descriptionHTML = '' 17
18 termsHTML = '' 18 html = {
19 description: '',
20 terms: '',
21 codeOfConduct: '',
22 moderationInformation: '',
23 administrator: ''
24 }
25
26 maintenanceLifetime = ''
27 businessModel = ''
28
29 languages: string[] = []
30 categories: number[] = []
19 31
20 constructor ( 32 constructor (
21 private notifier: Notifier, 33 private notifier: Notifier,
@@ -43,8 +55,15 @@ export class AboutInstanceComponent implements OnInit {
43 async res => { 55 async res => {
44 this.shortDescription = res.instance.shortDescription 56 this.shortDescription = res.instance.shortDescription
45 57
46 this.descriptionHTML = await this.markdownService.textMarkdownToHTML(res.instance.description) 58 this.maintenanceLifetime = res.instance.maintenanceLifetime
47 this.termsHTML = await this.markdownService.textMarkdownToHTML(res.instance.terms) 59 this.businessModel = res.instance.businessModel
60
61 for (const key of [ 'description', 'terms', 'codeOfConduct', 'moderationInformation', 'administrator' ]) {
62 this.html[key] = await this.markdownService.textMarkdownToHTML(res.instance[key])
63 }
64
65 this.languages = res.instance.languages
66 this.categories = res.instance.categories
48 }, 67 },
49 68
50 () => this.notifier.error(this.i18n('Cannot get about information from server')) 69 () => this.notifier.error(this.i18n('Cannot get about information from server'))
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index ec6f879d7..50df8a8ac 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -2,12 +2,13 @@
2 2
3 <ngb-tabset class="root-tabset bootstrap"> 3 <ngb-tabset class="root-tabset bootstrap">
4 4
5 <ngb-tab i18n-title title="Basic configuration"> 5 <ngb-tab i18n-title title="Instance information">
6 <ng-template ngbTabContent> 6 <ng-template ngbTabContent>
7 7
8 <div i18n class="inner-form-title">Instance</div>
9
10 <ng-container formGroupName="instance"> 8 <ng-container formGroupName="instance">
9
10 <div i18n class="inner-form-title">Instance</div>
11
11 <div class="form-group"> 12 <div class="form-group">
12 <label i18n for="instanceName">Name</label> 13 <label i18n for="instanceName">Name</label>
13 <input 14 <input
@@ -36,37 +37,41 @@
36 </div> 37 </div>
37 38
38 <div class="form-group"> 39 <div class="form-group">
39 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> 40 <label i18n for="instanceCategories">Main instance categories</label>
40 <my-markdown-textarea 41
41 id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true" 42 <div>
42 [ngClass]="{ 'input-error': formErrors['instance.terms'] }" 43 <p-multiSelect
43 ></my-markdown-textarea> 44 inputId="instanceCategories" [options]="categoryItems" formControlName="categories" showToggleAll="false"
44 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div> 45 [defaultLabel]="getDefaultCategoryLabel()" [selectedItemsLabel]="getSelectedCategoryLabel()"
46 emptyFilterMessage="No results found" i18n-emptyFilterMessage
47 ></p-multiSelect>
48 </div>
45 </div> 49 </div>
46 50
47 <div class="form-group"> 51 <div class="form-group">
52 <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
53
54 <div>
55 <p-multiSelect
56 inputId="instanceLanguages" [options]="languageItems" formControlName="languages" showToggleAll="false"
57 [defaultLabel]="getDefaultLanguageLabel()" [selectedItemsLabel]="getSelectedLanguageLabel()"
58 emptyFilterMessage="No results found" i18n-emptyFilterMessage
59 ></p-multiSelect>
60 </div>
61 </div>
62
63 <div i18n class="inner-form-title">Moderation & NSFW</div>
64
65 <div class="form-group">
48 <my-peertube-checkbox 66 <my-peertube-checkbox
49 inputName="instanceIsNSFW" formControlName="isNSFW" 67 inputName="instanceIsNSFW" formControlName="isNSFW"
50 i18n-labelText labelText="Dedicated to sensitive or NSFW content" 68 i18n-labelText labelText="This instance is dedicated to sensitive or NSFW content"
51 i18n-helpHtml helpHtml="Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br /> 69 i18n-helpHtml helpHtml="Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
52 Moreover, the NSFW checkbox on video upload will be automatically checked by default." 70 Moreover, the NSFW checkbox on video upload will be automatically checked by default."
53 ></my-peertube-checkbox> 71 ></my-peertube-checkbox>
54 </div> 72 </div>
55 73
56 <div class="form-group"> 74 <div class="form-group">
57 <label i18n for="instanceDefaultClientRoute">Default client route</label>
58 <div class="peertube-select-container">
59 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
60 <option i18n value="/videos/overview">Videos Overview</option>
61 <option i18n value="/videos/trending">Videos Trending</option>
62 <option i18n value="/videos/recently-added">Videos Recently Added</option>
63 <option i18n value="/videos/local">Local videos</option>
64 </select>
65 </div>
66 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
67 </div>
68
69 <div class="form-group">
70 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label> 75 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
71 <my-help 76 <my-help
72 helpType="custom" i18n-customHtml 77 helpType="custom" i18n-customHtml
@@ -82,10 +87,79 @@
82 </div> 87 </div>
83 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div> 88 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
84 </div> 89 </div>
90
91 <div class="form-group">
92 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
93 <my-markdown-textarea
94 id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
95 [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
96 ></my-markdown-textarea>
97 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
98 </div>
99
100 <div class="form-group">
101 <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help>
102 <my-markdown-textarea
103 id="instanceCodeOfConduct" formControlName="codeOfConduct" textareaWidth="500px" [previewColumn]="true"
104 [ngClass]="{ 'input-error': formErrors['instance.codeOfConduct'] }"
105 ></my-markdown-textarea>
106 <div *ngIf="formErrors.instance.codeOfConduct" class="form-error">{{ formErrors.instance.codeOfConduct }}</div>
107 </div>
108
109 <div class="form-group">
110 <label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
111 <div class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
112
113 <my-markdown-textarea
114 id="instanceModerationInformation" formControlName="moderationInformation" textareaWidth="500px" [previewColumn]="true"
115 [ngClass]="{ 'input-error': formErrors['instance.moderationInformation'] }"
116 ></my-markdown-textarea>
117 <div *ngIf="formErrors.instance.moderationInformation" class="form-error">{{ formErrors.instance.moderationInformation }}</div>
118 </div>
119
120 <div i18n class="inner-form-title">You and your instance</div>
121
122 <div class="form-group">
123 <label i18n for="instanceAdministrator">Who is behind the instance? </label>
124 <div class="label-small-info">A single person? A non profit? A company?</div>
125
126 <textarea
127 id="instanceAdministrator" formControlName="administrator"
128 [ngClass]="{ 'input-error': formErrors['instance.administrator'] }"
129 ></textarea>
130 <div *ngIf="formErrors.instance.administrator" class="form-error">{{ formErrors.instance.administrator }}</div>
131 </div>
132
133 <div class="form-group">
134 <label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label>
135 <div class="label-small-info">It's important to know for users who want to register on your instance</div>
136
137 <textarea
138 id="instanceMaintenanceLifetime" formControlName="maintenanceLifetime"
139 [ngClass]="{ 'input-error': formErrors['instance.maintenanceLifetime'] }"
140 ></textarea>
141 <div *ngIf="formErrors.instance.maintenanceLifetime" class="form-error">{{ formErrors.instance.maintenanceLifetime }}</div>
142 </div>
143
144 <div class="form-group">
145 <label i18n for="instanceBusinessModel">How will you pay the PeerTube instance server?</label>
146 <div class="label-small-info">With you own funds? With users donations? Advertising?</div>
147
148 <textarea
149 id="instanceBusinessModel" formControlName="businessModel"
150 [ngClass]="{ 'input-error': formErrors['instance.businessModel'] }"
151 ></textarea>
152 <div *ngIf="formErrors.instance.businessModel" class="form-error">{{ formErrors.instance.businessModel }}</div>
153 </div>
154
85 </ng-container> 155 </ng-container>
156 </ng-template>
157 </ngb-tab>
86 158
159 <ngb-tab i18n-title title="Basic configuration">
160 <ng-template ngbTabContent>
87 161
88 <div i18n class="inner-form-title">Theme</div> 162 <div i18n class="inner-form-title">Theme & Default route</div>
89 163
90 <ng-container formGroupName="theme"> 164 <ng-container formGroupName="theme">
91 <div class="form-group"> 165 <div class="form-group">
@@ -102,6 +176,19 @@
102 </ng-container> 176 </ng-container>
103 177
104 178
179 <div class="form-group" formGroupName="instance">
180 <label i18n for="instanceDefaultClientRoute">Default client route</label>
181 <div class="peertube-select-container">
182 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
183 <option i18n value="/videos/overview">Videos Discover</option>
184 <option i18n value="/videos/trending">Videos Trending</option>
185 <option i18n value="/videos/recently-added">Videos Recently Added</option>
186 <option i18n value="/videos/local">Local videos</option>
187 </select>
188 </div>
189 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
190 </div>
191
105 <div i18n class="inner-form-title">Signup</div> 192 <div i18n class="inner-form-title">Signup</div>
106 193
107 <ng-container formGroupName="signup"> 194 <ng-container formGroupName="signup">
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index c90bd5141..68f1b01b7 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -1,6 +1,10 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.form-group {
5 margin-bottom: 25px;
6}
7
4input[type=text] { 8input[type=text] {
5 @include peertube-input-text(340px); 9 @include peertube-input-text(340px);
6 display: block; 10 display: block;
@@ -44,3 +48,8 @@ textarea {
44 height: 100px; 48 height: 100px;
45 } 49 }
46} 50}
51
52.label-small-info {
53 font-style: italic;
54 margin-bottom: 10px;
55}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index d51104569..3119ab040 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -6,6 +6,9 @@ import { Notifier } from '@app/core'
6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' 6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 8import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
9import { SelectItem } from 'primeng/api'
10import { forkJoin } from 'rxjs'
11import { first } from 'rxjs/operators'
9 12
10@Component({ 13@Component({
11 selector: 'my-edit-custom-config', 14 selector: 'my-edit-custom-config',
@@ -18,6 +21,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
18 resolutions: { id: string, label: string }[] = [] 21 resolutions: { id: string, label: string }[] = []
19 transcodingThreadOptions: { label: string, value: number }[] = [] 22 transcodingThreadOptions: { label: string, value: number }[] = []
20 23
24 languageItems: SelectItem[] = []
25 categoryItems: SelectItem[] = []
26
21 constructor ( 27 constructor (
22 protected formValidatorService: FormValidatorService, 28 protected formValidatorService: FormValidatorService,
23 private customConfigValidatorsService: CustomConfigValidatorsService, 29 private customConfigValidatorsService: CustomConfigValidatorsService,
@@ -88,10 +94,22 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
88 name: this.customConfigValidatorsService.INSTANCE_NAME, 94 name: this.customConfigValidatorsService.INSTANCE_NAME,
89 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, 95 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
90 description: null, 96 description: null,
91 terms: null, 97
92 defaultClientRoute: null,
93 isNSFW: false, 98 isNSFW: false,
94 defaultNSFWPolicy: null, 99 defaultNSFWPolicy: null,
100
101 terms: null,
102 codeOfConduct: null,
103 moderationInformation: null,
104 administrator: null,
105 maintenanceLifetime: null,
106 businessModel: null,
107
108 categories: null,
109 languages: null,
110
111 defaultClientRoute: null,
112
95 customizations: { 113 customizations: {
96 javascript: null, 114 javascript: null,
97 css: null 115 css: null
@@ -184,18 +202,27 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
184 202
185 this.buildForm(formGroupData) 203 this.buildForm(formGroupData)
186 204
187 this.configService.getCustomConfig() 205 forkJoin([
188 .subscribe( 206 this.configService.getCustomConfig(),
189 res => { 207 this.serverService.videoLanguagesLoaded.pipe(first()), // First so the observable completes
190 this.customConfig = res 208 this.serverService.videoCategoriesLoaded.pipe(first())
209 ]).subscribe(
210 ([ config ]) => {
211 this.customConfig = config
191 212
192 this.updateForm() 213 const languages = this.serverService.getVideoLanguages()
193 // Force form validation 214 this.languageItems = languages.map(l => ({ label: l.label, value: l.id }))
194 this.forceCheck()
195 },
196 215
197 err => this.notifier.error(err.message) 216 const categories = this.serverService.getVideoCategories()
198 ) 217 this.categoryItems = categories.map(l => ({ label: l.label, value: l.id }))
218
219 this.updateForm()
220 // Force form validation
221 this.forceCheck()
222 },
223
224 err => this.notifier.error(err.message)
225 )
199 } 226 }
200 227
201 isTranscodingEnabled () { 228 isTranscodingEnabled () {
@@ -224,8 +251,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
224 ) 251 )
225 } 252 }
226 253
254 getSelectedLanguageLabel () {
255 return this.i18n('{{\'{0} languages selected')
256 }
257
258 getDefaultLanguageLabel () {
259 return this.i18n('No language')
260 }
261
262 getSelectedCategoryLabel () {
263 return this.i18n('{{\'{0} categories selected')
264 }
265
266 getDefaultCategoryLabel () {
267 return this.i18n('No category')
268 }
269
227 private updateForm () { 270 private updateForm () {
228 this.form.patchValue(this.customConfig) 271 this.form.patchValue(this.customConfig)
229 } 272 }
230
231} 273}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
index 2796dd2db..caa032149 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
@@ -23,7 +23,7 @@
23 23
24 <div> 24 <div>
25 <p-multiSelect 25 <p-multiSelect
26 [options]="languageItems" formControlName="videoLanguages" showToggleAll="true" 26 inputId="videoLanguages" [options]="languageItems" formControlName="videoLanguages" showToggleAll="true"
27 [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()" 27 [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()"
28 emptyFilterMessage="No results found" i18n-emptyFilterMessage 28 emptyFilterMessage="No results found" i18n-emptyFilterMessage
29 ></p-multiSelect> 29 ></p-multiSelect>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
index 77febf179..4fb828082 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
@@ -5,9 +5,9 @@ import { AuthService } from '../../../core'
5import { FormReactive, User, UserService } from '../../../shared' 5import { FormReactive, User, UserService } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8import { Subject } from 'rxjs' 8import { forkJoin, Subject } from 'rxjs'
9import { SelectItem } from 'primeng/api' 9import { SelectItem } from 'primeng/api'
10import { switchMap } from 'rxjs/operators' 10import { first } from 'rxjs/operators'
11 11
12@Component({ 12@Component({
13 selector: 'my-account-video-settings', 13 selector: 'my-account-video-settings',
@@ -39,30 +39,31 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
39 videoLanguages: null 39 videoLanguages: null
40 }) 40 })
41 41
42 this.serverService.videoLanguagesLoaded 42 forkJoin([
43 .pipe(switchMap(() => this.userInformationLoaded)) 43 this.serverService.videoLanguagesLoaded.pipe(first()),
44 .subscribe(() => { 44 this.userInformationLoaded.pipe(first())
45 const languages = this.serverService.getVideoLanguages() 45 ]).subscribe(() => {
46 46 const languages = this.serverService.getVideoLanguages()
47 this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ] 47
48 this.languageItems = this.languageItems 48 this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ]
49 .concat(languages.map(l => ({ label: l.label, value: l.id }))) 49 this.languageItems = this.languageItems
50 50 .concat(languages.map(l => ({ label: l.label, value: l.id })))
51 const videoLanguages = this.user.videoLanguages 51
52 ? this.user.videoLanguages 52 const videoLanguages = this.user.videoLanguages
53 : this.languageItems.map(l => l.value) 53 ? this.user.videoLanguages
54 54 : this.languageItems.map(l => l.value)
55 this.form.patchValue({ 55
56 nsfwPolicy: this.user.nsfwPolicy, 56 this.form.patchValue({
57 webTorrentEnabled: this.user.webTorrentEnabled, 57 nsfwPolicy: this.user.nsfwPolicy,
58 autoPlayVideo: this.user.autoPlayVideo === true, 58 webTorrentEnabled: this.user.webTorrentEnabled,
59 videoLanguages 59 autoPlayVideo: this.user.autoPlayVideo === true,
60 }) 60 videoLanguages
61 }) 61 })
62 })
62 } 63 }
63 64
64 updateDetails () { 65 updateDetails () {
65 const nsfwPolicy = this.form.value['nsfwPolicy'] 66 const nsfwPolicy = this.form.value[ 'nsfwPolicy' ]
66 const webTorrentEnabled = this.form.value['webTorrentEnabled'] 67 const webTorrentEnabled = this.form.value['webTorrentEnabled']
67 const autoPlayVideo = this.form.value['autoPlayVideo'] 68 const autoPlayVideo = this.form.value['autoPlayVideo']
68 69
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 571f46de9..6cf1499d3 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -37,7 +37,6 @@ import {
37} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component' 37} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
38import { DragDropModule } from '@angular/cdk/drag-drop' 38import { DragDropModule } from '@angular/cdk/drag-drop'
39import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email' 39import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email'
40import { MultiSelectModule } from 'primeng/multiselect'
41import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' 40import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
42 41
43@NgModule({ 42@NgModule({
@@ -48,8 +47,7 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account
48 SharedModule, 47 SharedModule,
49 TableModule, 48 TableModule,
50 InputSwitchModule, 49 InputSwitchModule,
51 DragDropModule, 50 DragDropModule
52 MultiSelectModule
53 ], 51 ],
54 52
55 declarations: [ 53 declarations: [
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index eb57a2fff..d71f6357b 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -6,10 +6,8 @@ import { RouterModule } from '@angular/router'
6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' 6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
7import { HelpComponent } from '@app/shared/misc/help.component' 7import { HelpComponent } from '@app/shared/misc/help.component'
8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
9
10import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' 9import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
11import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' 10import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
12
13import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 11import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
14import { ButtonComponent } from './buttons/button.component' 12import { ButtonComponent } from './buttons/button.component'
15import { DeleteButtonComponent } from './buttons/delete-button.component' 13import { DeleteButtonComponent } from './buttons/delete-button.component'
@@ -93,6 +91,7 @@ import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.
93import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' 91import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
94import { ClipboardModule } from 'ngx-clipboard' 92import { ClipboardModule } from 'ngx-clipboard'
95import { FollowService } from '@app/shared/instance/follow.service' 93import { FollowService } from '@app/shared/instance/follow.service'
94import { MultiSelectModule } from 'primeng/multiselect'
96 95
97@NgModule({ 96@NgModule({
98 imports: [ 97 imports: [
@@ -113,7 +112,8 @@ import { FollowService } from '@app/shared/instance/follow.service'
113 112
114 PrimeSharedModule, 113 PrimeSharedModule,
115 InputMaskModule, 114 InputMaskModule,
116 NgPipesModule 115 NgPipesModule,
116 MultiSelectModule
117 ], 117 ],
118 118
119 declarations: [ 119 declarations: [
@@ -186,6 +186,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
186 InputMaskModule, 186 InputMaskModule,
187 BytesPipe, 187 BytesPipe,
188 KeysPipe, 188 KeysPipe,
189 MultiSelectModule,
189 190
190 LoaderComponent, 191 LoaderComponent,
191 SmallLoaderComponent, 192 SmallLoaderComponent,
diff --git a/config/default.yaml b/config/default.yaml
index 5a935fede..f84ecfcf9 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -238,7 +238,53 @@ instance:
238 short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.' 238 short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.'
239 description: 'Welcome to this PeerTube instance!' # Support markdown 239 description: 'Welcome to this PeerTube instance!' # Support markdown
240 terms: 'No terms for now.' # Support markdown 240 terms: 'No terms for now.' # Support markdown
241 code_of_conduct: '' # Supports markdown
242
243 # Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc
244 moderation_information: '' # Supports markdown
245
246 # Who is behind the instance? A single person? A non profit?
247 administrator: ''
248
249 # How long do you plan to maintain this instance?
250 maintenance_lifetime: ''
251
252 # How will you pay the PeerTube instance server? With you own funds? With users donations? Advertising?
253 business_model: ''
254
255 # What are the main languages of your instance? To interact with your users for example
256 # Uncomment or add the languages you want
257 # List of supported languages: https://peertube.cpy.re/api/v1/videos/languages
258 languages:
259# - en
260# - es
261# - fr
262
263 # You can specify the main categories of your instance (dedicated to music, gaming or politics etc)
264 # Uncomment or add the category ids you want
265 # List of supported categories: https://peertube.cpy.re/api/v1/videos/categories
266 categories:
267# - 1 # Music
268# - 2 # Films
269# - 3 # Vehicles
270# - 4 # Art
271# - 5 # Sports
272# - 6 # Travels
273# - 7 # Gaming
274# - 8 # People
275# - 9 # Comedy
276# - 10 # Entertainment
277# - 11 # News & Politics
278# - 12 # How To
279# - 13 # Education
280# - 14 # Activism
281# - 15 # Science & Technology
282# - 16 # Animals
283# - 17 # Kids
284# - 18 # Food
285
241 default_client_route: '/videos/trending' 286 default_client_route: '/videos/trending'
287
242 # Whether or not the instance is dedicated to NSFW content 288 # Whether or not the instance is dedicated to NSFW content
243 # Enabling it will allow other administrators to know that you are mainly federating sensitive content 289 # Enabling it will allow other administrators to know that you are mainly federating sensitive content
244 # Moreover, the NSFW checkbox on video upload will be automatically checked by default 290 # Moreover, the NSFW checkbox on video upload will be automatically checked by default
@@ -246,6 +292,7 @@ instance:
246 # By default, "do_not_list" or "blur" or "display" NSFW videos 292 # By default, "do_not_list" or "blur" or "display" NSFW videos
247 # Could be overridden per user with a setting 293 # Could be overridden per user with a setting
248 default_nsfw_policy: 'do_not_list' 294 default_nsfw_policy: 'do_not_list'
295
249 customizations: 296 customizations:
250 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime 297 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
251 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime 298 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 0c52bfa7a..b5244756d 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -158,7 +158,16 @@ function getAbout (req: express.Request, res: express.Response) {
158 name: CONFIG.INSTANCE.NAME, 158 name: CONFIG.INSTANCE.NAME,
159 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 159 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
160 description: CONFIG.INSTANCE.DESCRIPTION, 160 description: CONFIG.INSTANCE.DESCRIPTION,
161 terms: CONFIG.INSTANCE.TERMS 161 terms: CONFIG.INSTANCE.TERMS,
162 codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
163
164 moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
165 administrator: CONFIG.INSTANCE.ADMINISTRATOR,
166 maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
167 businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
168
169 languages: CONFIG.INSTANCE.LANGUAGES,
170 categories: CONFIG.INSTANCE.CATEGORIES
162 } 171 }
163 } 172 }
164 173
@@ -221,6 +230,16 @@ function customConfig (): CustomConfig {
221 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 230 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
222 description: CONFIG.INSTANCE.DESCRIPTION, 231 description: CONFIG.INSTANCE.DESCRIPTION,
223 terms: CONFIG.INSTANCE.TERMS, 232 terms: CONFIG.INSTANCE.TERMS,
233 codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
234
235 moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
236 administrator: CONFIG.INSTANCE.ADMINISTRATOR,
237 maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
238 businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
239
240 languages: CONFIG.INSTANCE.LANGUAGES,
241 categories: CONFIG.INSTANCE.CATEGORIES,
242
224 isNSFW: CONFIG.INSTANCE.IS_NSFW, 243 isNSFW: CONFIG.INSTANCE.IS_NSFW,
225 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, 244 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
226 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, 245 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 599f3f5ac..4e2b07e64 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -209,6 +209,16 @@ const CONFIG = {
209 get SHORT_DESCRIPTION () { return config.get<string>('instance.short_description') }, 209 get SHORT_DESCRIPTION () { return config.get<string>('instance.short_description') },
210 get DESCRIPTION () { return config.get<string>('instance.description') }, 210 get DESCRIPTION () { return config.get<string>('instance.description') },
211 get TERMS () { return config.get<string>('instance.terms') }, 211 get TERMS () { return config.get<string>('instance.terms') },
212 get CODE_OF_CONDUCT () { return config.get<string>('instance.code_of_conduct') },
213
214 get MODERATION_INFORMATION () { return config.get<string>('instance.moderation_information') },
215 get ADMINISTRATOR () { return config.get<string>('instance.administrator') },
216 get MAINTENANCE_LIFETIME () { return config.get<string>('instance.maintenance_lifetime') },
217 get BUSINESS_MODEL () { return config.get<string>('instance.business_model') },
218
219 get LANGUAGES () { return config.get<string[]>('instance.languages') || [] },
220 get CATEGORIES () { return config.get<number[]>('instance.categories') || [] },
221
212 get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') }, 222 get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') },
213 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') }, 223 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
214 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') }, 224 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index 1221735c5..f716dc673 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -27,6 +27,16 @@ describe('Test config API validators', function () {
27 shortDescription: 'my short description', 27 shortDescription: 'my short description',
28 description: 'my super description', 28 description: 'my super description',
29 terms: 'my super terms', 29 terms: 'my super terms',
30 codeOfConduct: 'my super coc',
31
32 moderationInformation: 'my super moderation information',
33 administrator: 'Kuja',
34 maintenanceLifetime: 'forever',
35 businessModel: 'my super business model',
36
37 languages: [ 'en', 'es' ],
38 categories: [ 1, 2 ],
39
30 isNSFW: true, 40 isNSFW: true,
31 defaultClientRoute: '/videos/recently-added', 41 defaultClientRoute: '/videos/recently-added',
32 defaultNSFWPolicy: 'blur', 42 defaultNSFWPolicy: 'blur',
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index b2f1933d1..da75495a5 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -28,7 +28,17 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
28 'with WebTorrent and Angular.' 28 'with WebTorrent and Angular.'
29 ) 29 )
30 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') 30 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
31
31 expect(data.instance.terms).to.equal('No terms for now.') 32 expect(data.instance.terms).to.equal('No terms for now.')
33 expect(data.instance.codeOfConduct).to.be.empty
34 expect(data.instance.moderationInformation).to.be.empty
35 expect(data.instance.administrator).to.be.empty
36 expect(data.instance.maintenanceLifetime).to.be.empty
37 expect(data.instance.businessModel).to.be.empty
38
39 expect(data.instance.languages).to.have.lengthOf(0)
40 expect(data.instance.categories).to.have.lengthOf(0)
41
32 expect(data.instance.defaultClientRoute).to.equal('/videos/trending') 42 expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
33 expect(data.instance.isNSFW).to.be.false 43 expect(data.instance.isNSFW).to.be.false
34 expect(data.instance.defaultNSFWPolicy).to.equal('display') 44 expect(data.instance.defaultNSFWPolicy).to.equal('display')
@@ -78,7 +88,17 @@ function checkUpdatedConfig (data: CustomConfig) {
78 expect(data.instance.name).to.equal('PeerTube updated') 88 expect(data.instance.name).to.equal('PeerTube updated')
79 expect(data.instance.shortDescription).to.equal('my short description') 89 expect(data.instance.shortDescription).to.equal('my short description')
80 expect(data.instance.description).to.equal('my super description') 90 expect(data.instance.description).to.equal('my super description')
91
81 expect(data.instance.terms).to.equal('my super terms') 92 expect(data.instance.terms).to.equal('my super terms')
93 expect(data.instance.codeOfConduct).to.equal('my super coc')
94 expect(data.instance.moderationInformation).to.equal('my super moderation information')
95 expect(data.instance.administrator).to.equal('Kuja')
96 expect(data.instance.maintenanceLifetime).to.equal('forever')
97 expect(data.instance.businessModel).to.equal('my super business model')
98
99 expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
100 expect(data.instance.categories).to.deep.equal([ 1, 2 ])
101
82 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') 102 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
83 expect(data.instance.isNSFW).to.be.true 103 expect(data.instance.isNSFW).to.be.true
84 expect(data.instance.defaultNSFWPolicy).to.equal('blur') 104 expect(data.instance.defaultNSFWPolicy).to.equal('blur')
@@ -190,6 +210,16 @@ describe('Test config', function () {
190 shortDescription: 'my short description', 210 shortDescription: 'my short description',
191 description: 'my super description', 211 description: 'my super description',
192 terms: 'my super terms', 212 terms: 'my super terms',
213 codeOfConduct: 'my super coc',
214
215 moderationInformation: 'my super moderation information',
216 administrator: 'Kuja',
217 maintenanceLifetime: 'forever',
218 businessModel: 'my super business model',
219
220 languages: [ 'en', 'es' ],
221 categories: [ 1, 2 ],
222
193 defaultClientRoute: '/videos/recently-added', 223 defaultClientRoute: '/videos/recently-added',
194 isNSFW: true, 224 isNSFW: true,
195 defaultNSFWPolicy: 'blur' as 'blur', 225 defaultNSFWPolicy: 'blur' as 'blur',
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
index d784af9a9..785421c98 100644
--- a/shared/extra-utils/server/config.ts
+++ b/shared/extra-utils/server/config.ts
@@ -53,6 +53,16 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
53 shortDescription: 'my short description', 53 shortDescription: 'my short description',
54 description: 'my super description', 54 description: 'my super description',
55 terms: 'my super terms', 55 terms: 'my super terms',
56 codeOfConduct: 'my super coc',
57
58 moderationInformation: 'my super moderation information',
59 administrator: 'Kuja',
60 maintenanceLifetime: 'forever',
61 businessModel: 'my super business model',
62
63 languages: [ 'en', 'es' ],
64 categories: [ 1, 2 ],
65
56 defaultClientRoute: '/videos/recently-added', 66 defaultClientRoute: '/videos/recently-added',
57 isNSFW: true, 67 isNSFW: true,
58 defaultNSFWPolicy: 'blur', 68 defaultNSFWPolicy: 'blur',
diff --git a/shared/models/server/about.model.ts b/shared/models/server/about.model.ts
index 10dff8b8f..e32ed26ee 100644
--- a/shared/models/server/about.model.ts
+++ b/shared/models/server/about.model.ts
@@ -4,5 +4,15 @@ export interface About {
4 shortDescription: string 4 shortDescription: string
5 description: string 5 description: string
6 terms: string 6 terms: string
7
8 codeOfConduct: string
9
10 moderationInformation: string
11 administrator: string
12 maintenanceLifetime: string
13 businessModel: string
14
15 languages: string[]
16 categories: number[]
7 } 17 }
8} 18}
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 1073ba32c..0c331a820 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -6,6 +6,16 @@ export interface CustomConfig {
6 shortDescription: string 6 shortDescription: string
7 description: string 7 description: string
8 terms: string 8 terms: string
9 codeOfConduct: string
10
11 moderationInformation: string
12 administrator: string
13 maintenanceLifetime: string
14 businessModel: string
15
16 languages: string[]
17 categories: number[]
18
9 isNSFW: boolean 19 isNSFW: boolean
10 defaultClientRoute: string 20 defaultClientRoute: string
11 defaultNSFWPolicy: NSFWPolicyType 21 defaultNSFWPolicy: NSFWPolicyType