aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html471
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts14
-rw-r--r--client/src/app/shared/forms/form-validators/custom-config.ts7
-rw-r--r--client/src/polyfills.ts2
-rw-r--r--config/default.yaml9
-rw-r--r--config/production.yaml.example9
-rw-r--r--server/controllers/api/config.ts6
-rw-r--r--server/controllers/client.ts4
-rw-r--r--server/initializers/checker.ts3
-rw-r--r--server/initializers/constants.ts6
-rw-r--r--server/tests/api/check-params/config.ts6
-rw-r--r--server/tests/api/server/config.ts14
-rw-r--r--server/tests/client.ts30
-rw-r--r--shared/models/server/custom-config.model.ts7
14 files changed, 365 insertions, 223 deletions
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 021252456..252d43c8f 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
@@ -1,222 +1,259 @@
1<div class="form-sub-title">Update PeerTube configuration</div>
2
3<form role="form" [formGroup]="form"> 1<form role="form" [formGroup]="form">
4 2
5 <div class="inner-form-title">Instance</div> 3 <tabset class="root-tabset bootstrap">
6 4
7 <div class="form-group"> 5 <tab heading="Basic configuration">
8 <label for="instanceName">Name</label> 6
9 <input 7 <div class="inner-form-title">Instance</div>
10 type="text" id="instanceName" 8
11 formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }" 9 <div class="form-group">
12 > 10 <label for="instanceName">Name</label>
13 <div *ngIf="formErrors.instanceName" class="form-error"> 11 <input
14 {{ formErrors.instanceName }} 12 type="text" id="instanceName"
15 </div> 13 formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }"
16 </div> 14 >
17 15 <div *ngIf="formErrors.instanceName" class="form-error">
18 <div class="form-group"> 16 {{ formErrors.instanceName }}
19 <label for="instanceShortDescription">Short description</label> 17 </div>
20 <textarea 18 </div>
21 id="instanceShortDescription" formControlName="instanceShortDescription" 19
22 [ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }" 20 <div class="form-group">
23 ></textarea> 21 <label for="instanceShortDescription">Short description</label>
24 <div *ngIf="formErrors.instanceShortDescription" class="form-error"> 22 <textarea
25 {{ formErrors.instanceShortDescription }} 23 id="instanceShortDescription" formControlName="instanceShortDescription"
26 </div> 24 [ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }"
27 </div> 25 ></textarea>
28 26 <div *ngIf="formErrors.instanceShortDescription" class="form-error">
29 <div class="form-group"> 27 {{ formErrors.instanceShortDescription }}
30 <label for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help> 28 </div>
31 <my-markdown-textarea 29 </div>
32 id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true" 30
33 [classes]="{ 'input-error': formErrors['instanceDescription'] }" 31 <div class="form-group">
34 ></my-markdown-textarea> 32 <label for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
35 <div *ngIf="formErrors.instanceDescription" class="form-error"> 33 <my-markdown-textarea
36 {{ formErrors.instanceDescription }} 34 id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true"
37 </div> 35 [classes]="{ 'input-error': formErrors['instanceDescription'] }"
38 </div> 36 ></my-markdown-textarea>
39 37 <div *ngIf="formErrors.instanceDescription" class="form-error">
40 <div class="form-group"> 38 {{ formErrors.instanceDescription }}
41 <label for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> 39 </div>
42 <my-markdown-textarea 40 </div>
43 id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true" 41
44 [ngClass]="{ 'input-error': formErrors['instanceTerms'] }" 42 <div class="form-group">
45 ></my-markdown-textarea> 43 <label for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
46 <div *ngIf="formErrors.instanceTerms" class="form-error"> 44 <my-markdown-textarea
47 {{ formErrors.instanceTerms }} 45 id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true"
48 </div> 46 [ngClass]="{ 'input-error': formErrors['instanceTerms'] }"
49 </div> 47 ></my-markdown-textarea>
50 48 <div *ngIf="formErrors.instanceTerms" class="form-error">
51 <div class="form-group"> 49 {{ formErrors.instanceTerms }}
52 <label for="instanceDefaultClientRoute">Default client route</label> 50 </div>
53 <div class="peertube-select-container"> 51 </div>
54 <select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute"> 52
55 <option value="/videos/trending">Videos Trending</option> 53 <div class="form-group">
56 <option value="/videos/recently-added">Videos Recently Added</option> 54 <label for="instanceDefaultClientRoute">Default client route</label>
57 <option value="/videos/local">Local videos</option> 55 <div class="peertube-select-container">
58 </select> 56 <select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute">
59 </div> 57 <option value="/videos/trending">Videos Trending</option>
60 <div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error"> 58 <option value="/videos/recently-added">Videos Recently Added</option>
61 {{ formErrors.instanceDefaultClientRoute }} 59 <option value="/videos/local">Local videos</option>
62 </div> 60 </select>
63 </div> 61 </div>
64 62 <div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error">
65 <div class="form-group"> 63 {{ formErrors.instanceDefaultClientRoute }}
66 <label for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label> 64 </div>
67 <my-help helpType="custom" customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."></my-help> 65 </div>
68 66
69 <div class="peertube-select-container"> 67 <div class="form-group">
70 <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy"> 68 <label for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
71 <option value="do_not_list">Do not list</option> 69 <my-help helpType="custom" customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."></my-help>
72 <option value="blur">Blur thumbnails</option> 70
73 <option value="display">Display</option> 71 <div class="peertube-select-container">
74 </select> 72 <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy">
75 </div> 73 <option value="do_not_list">Do not list</option>
76 <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error"> 74 <option value="blur">Blur thumbnails</option>
77 {{ formErrors.instanceDefaultNSFWPolicy }} 75 <option value="display">Display</option>
78 </div> 76 </select>
79 </div> 77 </div>
80 78 <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error">
81 <div class="inner-form-title">Cache</div> 79 {{ formErrors.instanceDefaultNSFWPolicy }}
82 80 </div>
83 <div class="form-group"> 81 </div>
84 <label for="cachePreviewsSize">Preview cache size</label> 82
85 <input 83 <div class="inner-form-title">Signup</div>
86 type="text" id="cachePreviewsSize" 84
87 formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }" 85 <div class="form-group">
88 > 86 <input type="checkbox" id="signupEnabled" formControlName="signupEnabled">
89 <div *ngIf="formErrors.cachePreviewsSize" class="form-error"> 87
90 {{ formErrors.cachePreviewsSize }} 88 <label for="signupEnabled"></label>
91 </div> 89 <label for="signupEnabled">Signup enabled</label>
92 </div> 90 </div>
93 91
94 <div class="inner-form-title">Signup</div> 92 <div *ngIf="isSignupEnabled()" class="form-group">
95 93 <label for="signupLimit">Signup limit</label>
96 <div class="form-group"> 94 <input
97 <input type="checkbox" id="signupEnabled" formControlName="signupEnabled"> 95 type="text" id="signupLimit"
98 96 formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }"
99 <label for="signupEnabled"></label> 97 >
100 <label for="signupEnabled">Signup enabled</label> 98 <div *ngIf="formErrors.signupLimit" class="form-error">
101 </div> 99 {{ formErrors.signupLimit }}
102 100 </div>
103 <div *ngIf="isSignupEnabled()" class="form-group"> 101 </div>
104 <label for="signupLimit">Signup limit</label> 102
105 <input 103 <div class="inner-form-title">Administrator</div>
106 type="text" id="signupLimit" 104
107 formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }" 105 <div class="form-group">
108 > 106 <label for="adminEmail">Admin email</label>
109 <div *ngIf="formErrors.signupLimit" class="form-error"> 107 <input
110 {{ formErrors.signupLimit }} 108 type="text" id="adminEmail"
111 </div> 109 formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }"
112 </div> 110 >
113 111 <div *ngIf="formErrors.adminEmail" class="form-error">
114 <div class="inner-form-title">Administrator</div> 112 {{ formErrors.adminEmail }}
115 113 </div>
116 <div class="form-group"> 114 </div>
117 <label for="adminEmail">Admin email</label> 115
118 <input 116 <div class="inner-form-title">Users</div>
119 type="text" id="adminEmail" 117
120 formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }" 118 <div class="form-group">
121 > 119 <label for="userVideoQuota">User default video quota</label>
122 <div *ngIf="formErrors.adminEmail" class="form-error"> 120 <div class="peertube-select-container">
123 {{ formErrors.adminEmail }} 121 <select id="userVideoQuota" formControlName="userVideoQuota">
124 </div> 122 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
125 </div> 123 {{ videoQuotaOption.label }}
126 124 </option>
127 <div class="inner-form-title">Users</div> 125 </select>
128 126 </div>
129 <div class="form-group"> 127 <div *ngIf="formErrors.userVideoQuota" class="form-error">
130 <label for="userVideoQuota">User default video quota</label> 128 {{ formErrors.userVideoQuota }}
131 <div class="peertube-select-container"> 129 </div>
132 <select id="userVideoQuota" formControlName="userVideoQuota"> 130 </div>
133 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> 131 </tab>
134 {{ videoQuotaOption.label }} 132
135 </option> 133 <tab heading="Services">
136 </select> 134
137 </div> 135 <div class="inner-form-title">Twitter</div>
138 <div *ngIf="formErrors.userVideoQuota" class="form-error"> 136
139 {{ formErrors.userVideoQuota }} 137 <div class="form-group">
140 </div> 138 <label for="signupLimit">Your Twitter username</label>
141 </div> 139 <my-help helpType="custom" customHtml="The Twitter @username the cards (created by PeerTube video shares) should be attributed to."></my-help>
142 140 <input
143 <div class="inner-form-title">Transcoding</div> 141 type="text" id="servicesTwitterUsername"
144 142 formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }"
145 <div class="form-group"> 143 >
146 <input type="checkbox" id="transcodingEnabled" formControlName="transcodingEnabled"> 144 <div *ngIf="formErrors.servicesTwitterUsername" class="form-error">
147 145 {{ formErrors.servicesTwitterUsername }}
148 <label for="transcodingEnabled"></label> 146 </div>
149 <label for="transcodingEnabled">Transcoding enabled</label> 147 </div>
150 </div> 148
151 149 <div class="form-group">
152 <ng-template [ngIf]="isTranscodingEnabled()"> 150 <input type="checkbox" id="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted">
153 151
154 <div class="form-group"> 152 <label for="servicesTwitterWhitelisted"></label>
155 <label for="transcodingThreads">Transcoding threads</label> 153 <label for="servicesTwitterWhitelisted">Instance whitelisted by Twitter</label>
156 <div class="peertube-select-container"> 154 <my-help helpType="custom" customHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
157 <select id="transcodingThreads" formControlName="transcodingThreads"> 155If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
158 <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value"> 156Check this checkbox, save the configuration and test on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."></my-help>
159 {{ transcodingThreadOption.label }} 157
160 </option> 158 </div>
161 </select> 159 </tab>
162 </div> 160
163 <div *ngIf="formErrors.transcodingThreads" class="form-error"> 161 <tab heading="Advanced configuration">
164 {{ formErrors.transcodingThreads }} 162
165 </div> 163 <div class="inner-form-title">Transcoding</div>
166 </div> 164
167 165 <div class="form-group">
168 <div class="form-group" *ngFor="let resolution of resolutions"> 166 <input type="checkbox" id="transcodingEnabled" formControlName="transcodingEnabled">
169 <input 167
170 type="checkbox" [id]="getResolutionKey(resolution)" 168 <label for="transcodingEnabled"></label>
171 [formControlName]="getResolutionKey(resolution)" 169 <label for="transcodingEnabled">Transcoding enabled</label>
172 > 170 </div>
173 <label [for]="getResolutionKey(resolution)"></label> 171
174 <label [for]="getResolutionKey(resolution)">Resolution {{ resolution }} enabled</label> 172 <ng-template [ngIf]="isTranscodingEnabled()">
175 </div> 173
176 </ng-template> 174 <div class="form-group">
177 175 <label for="transcodingThreads">Transcoding threads</label>
178 <div class="inner-form-title">Customizations</div> 176 <div class="peertube-select-container">
179 177 <select id="transcodingThreads" formControlName="transcodingThreads">
180 <div class="form-group"> 178 <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
181 <label for="customizationJavascript">JavaScript</label> 179 {{ transcodingThreadOption.label }}
182 <my-help helpType="custom" customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"></my-help> 180 </option>
183 <textarea 181 </select>
184 id="customizationJavascript" formControlName="customizationJavascript" 182 </div>
185 [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }" 183 <div *ngIf="formErrors.transcodingThreads" class="form-error">
186 ></textarea> 184 {{ formErrors.transcodingThreads }}
187 <div *ngIf="formErrors.customizationJavascript" class="form-error"> 185 </div>
188 {{ formErrors.customizationJavascript }} 186 </div>
189 </div> 187
190 </div> 188 <div class="form-group" *ngFor="let resolution of resolutions">
191 189 <input
192 <div class="form-group"> 190 type="checkbox" [id]="getResolutionKey(resolution)"
193 <label for="customizationCSS">CSS</label> 191 [formControlName]="getResolutionKey(resolution)"
194 <my-help 192 >
195 helpType="custom" 193 <label [for]="getResolutionKey(resolution)"></label>
196 customHtml=" 194 <label [for]="getResolutionKey(resolution)">Resolution {{ resolution }} enabled</label>
197 Write directly CSS code. Example:<br /> 195 </div>
198 <pre> 196 </ng-template>
199body { 197
200 background-color: red; 198 <div class="inner-form-title">Cache</div>
201} 199
202 </pre> 200 <div class="form-group">
203 201 <label for="cachePreviewsSize">Preview cache size</label>
204 Prepend with <em>#custom-css</em> to override styles. Example: 202 <my-help helpType="custom" customHtml="Previews are not federated. We fetch them directly from the origin instance and cache them."></my-help>
205 <pre> 203
206#custom-css .logged-in-email { 204 <input
207 color: red; 205 type="text" id="cachePreviewsSize"
208} 206 formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }"
209 </pre> 207 >
210 " 208 <div *ngIf="formErrors.cachePreviewsSize" class="form-error">
211 ></my-help> 209 {{ formErrors.cachePreviewsSize }}
212 <textarea 210 </div>
213 id="customizationCSS" formControlName="customizationCSS" 211 </div>
214 [ngClass]="{ 'input-error': formErrors['customizationCSS'] }" 212
215 ></textarea> 213 <div class="inner-form-title">Customizations</div>
216 <div *ngIf="formErrors.customizationCSS" class="form-error"> 214
217 {{ formErrors.customizationCSS }} 215 <div class="form-group">
218 </div> 216 <label for="customizationJavascript">JavaScript</label>
219 </div> 217 <my-help helpType="custom" customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"></my-help>
218 <textarea
219 id="customizationJavascript" formControlName="customizationJavascript"
220 [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }"
221 ></textarea>
222 <div *ngIf="formErrors.customizationJavascript" class="form-error">
223 {{ formErrors.customizationJavascript }}
224 </div>
225 </div>
226
227 <div class="form-group">
228 <label for="customizationCSS">CSS</label>
229 <my-help
230 helpType="custom"
231 customHtml="
232 Write directly CSS code. Example:<br />
233 <pre>
234 body {
235 background-color: red;
236 }
237 </pre>
238
239 Prepend with <em>#custom-css</em> to override styles. Example:
240 <pre>
241 #custom-css .logged-in-email {
242 color: red;
243 }
244 </pre>
245 "
246 ></my-help>
247 <textarea
248 id="customizationCSS" formControlName="customizationCSS"
249 [ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
250 ></textarea>
251 <div *ngIf="formErrors.customizationCSS" class="form-error">
252 {{ formErrors.customizationCSS }}
253 </div>
254 </div>
255 </tab>
256 </tabset>
220 257
221 <input (click)="formValidated()" type="submit" value="Update configuration" [disabled]="!form.valid"> 258 <input (click)="formValidated()" type="submit" value="Update configuration" [disabled]="!form.valid">
222</form> 259</form>
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 2ab371cbb..a1e334a74 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
@@ -8,7 +8,7 @@ import { FormReactive, USER_VIDEO_QUOTA } from '@app/shared'
8import { 8import {
9 ADMIN_EMAIL, 9 ADMIN_EMAIL,
10 CACHE_PREVIEWS_SIZE, 10 CACHE_PREVIEWS_SIZE,
11 INSTANCE_NAME, INSTANCE_SHORT_DESCRIPTION, 11 INSTANCE_NAME, INSTANCE_SHORT_DESCRIPTION, SERVICES_TWITTER_USERNAME,
12 SIGNUP_LIMIT, 12 SIGNUP_LIMIT,
13 TRANSCODING_THREADS 13 TRANSCODING_THREADS
14} from '@app/shared/forms/form-validators/custom-config' 14} from '@app/shared/forms/form-validators/custom-config'
@@ -49,6 +49,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
49 instanceTerms: '', 49 instanceTerms: '',
50 instanceDefaultClientRoute: '', 50 instanceDefaultClientRoute: '',
51 instanceDefaultNSFWPolicy: '', 51 instanceDefaultNSFWPolicy: '',
52 servicesTwitterUsername: '',
52 cachePreviewsSize: '', 53 cachePreviewsSize: '',
53 signupLimit: '', 54 signupLimit: '',
54 adminEmail: '', 55 adminEmail: '',
@@ -60,6 +61,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
60 validationMessages = { 61 validationMessages = {
61 instanceShortDescription: INSTANCE_SHORT_DESCRIPTION.MESSAGES, 62 instanceShortDescription: INSTANCE_SHORT_DESCRIPTION.MESSAGES,
62 instanceName: INSTANCE_NAME.MESSAGES, 63 instanceName: INSTANCE_NAME.MESSAGES,
64 servicesTwitterUsername: SERVICES_TWITTER_USERNAME,
63 cachePreviewsSize: CACHE_PREVIEWS_SIZE.MESSAGES, 65 cachePreviewsSize: CACHE_PREVIEWS_SIZE.MESSAGES,
64 signupLimit: SIGNUP_LIMIT.MESSAGES, 66 signupLimit: SIGNUP_LIMIT.MESSAGES,
65 adminEmail: ADMIN_EMAIL.MESSAGES, 67 adminEmail: ADMIN_EMAIL.MESSAGES,
@@ -92,6 +94,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
92 instanceTerms: [ '' ], 94 instanceTerms: [ '' ],
93 instanceDefaultClientRoute: [ '' ], 95 instanceDefaultClientRoute: [ '' ],
94 instanceDefaultNSFWPolicy: [ '' ], 96 instanceDefaultNSFWPolicy: [ '' ],
97 servicesTwitterUsername: [ '', SERVICES_TWITTER_USERNAME.VALIDATORS ],
98 servicesTwitterWhitelisted: [ ],
95 cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ], 99 cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ],
96 signupEnabled: [ ], 100 signupEnabled: [ ],
97 signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ], 101 signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ],
@@ -175,6 +179,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
175 css: this.form.value['customizationCSS'] 179 css: this.form.value['customizationCSS']
176 } 180 }
177 }, 181 },
182 services: {
183 twitter: {
184 username: this.form.value['servicesTwitterUsername'],
185 whitelisted: this.form.value['servicesTwitterWhitelisted']
186 }
187 },
178 cache: { 188 cache: {
179 previews: { 189 previews: {
180 size: this.form.value['cachePreviewsSize'] 190 size: this.form.value['cachePreviewsSize']
@@ -228,6 +238,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
228 instanceTerms: this.customConfig.instance.terms, 238 instanceTerms: this.customConfig.instance.terms,
229 instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute, 239 instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
230 instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy, 240 instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
241 servicesTwitterUsername: this.customConfig.services.twitter.username,
242 servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
231 cachePreviewsSize: this.customConfig.cache.previews.size, 243 cachePreviewsSize: this.customConfig.cache.previews.size,
232 signupEnabled: this.customConfig.signup.enabled, 244 signupEnabled: this.customConfig.signup.enabled,
233 signupLimit: this.customConfig.signup.limit, 245 signupLimit: this.customConfig.signup.limit,
diff --git a/client/src/app/shared/forms/form-validators/custom-config.ts b/client/src/app/shared/forms/form-validators/custom-config.ts
index c9cef2e09..e3d9a4c7b 100644
--- a/client/src/app/shared/forms/form-validators/custom-config.ts
+++ b/client/src/app/shared/forms/form-validators/custom-config.ts
@@ -14,6 +14,13 @@ export const INSTANCE_SHORT_DESCRIPTION = {
14 } 14 }
15} 15}
16 16
17export const SERVICES_TWITTER_USERNAME = {
18 VALIDATORS: [ Validators.required ],
19 MESSAGES: {
20 'required': 'Twitter username is required.'
21 }
22}
23
17export const CACHE_PREVIEWS_SIZE = { 24export const CACHE_PREVIEWS_SIZE = {
18 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], 25 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
19 MESSAGES: { 26 MESSAGES: {
diff --git a/client/src/polyfills.ts b/client/src/polyfills.ts
index 12b317101..423a7b915 100644
--- a/client/src/polyfills.ts
+++ b/client/src/polyfills.ts
@@ -35,6 +35,7 @@ import 'core-js/es6/regexp';
35import 'core-js/es6/map'; 35import 'core-js/es6/map';
36import 'core-js/es6/weak-map'; 36import 'core-js/es6/weak-map';
37import 'core-js/es6/set'; 37import 'core-js/es6/set';
38import 'core-js/es7/object';
38 39
39/** IE10 and IE11 requires the following for NgClass support on SVG elements */ 40/** IE10 and IE11 requires the following for NgClass support on SVG elements */
40// import 'classlist.js'; // Run `npm install --save classlist.js`. 41// import 'classlist.js'; // Run `npm install --save classlist.js`.
@@ -44,7 +45,6 @@ import 'core-js/es6/set';
44// For Google Bot 45// For Google Bot
45import 'core-js/es6/reflect'; 46import 'core-js/es6/reflect';
46 47
47
48/** Evergreen browsers require these. **/ 48/** Evergreen browsers require these. **/
49// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 49// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
50import 'core-js/es7/reflect' 50import 'core-js/es7/reflect'
diff --git a/config/default.yaml b/config/default.yaml
index 25dde72c9..2826e76f8 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -90,3 +90,12 @@ instance:
90 customizations: 90 customizations:
91 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime 91 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
92 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime 92 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
93
94services:
95 # Cards configuration to format video in Twitter
96 twitter:
97 username: '@Chocobozzz' # The Twitter @username the card should be attributed to
98 # If true, a video player will be embedded in the Twitter feed on PeerTube video share
99 # If false, we use an image link card that will redirect on your PeerTube instance
100 # Change it to "true", and then test on https://cards-dev.twitter.com/validator to see if you are whitelisted
101 whitelisted: false
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 1d7d35c9c..a6f1740fe 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -106,3 +106,12 @@ instance:
106 customizations: 106 customizations:
107 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime 107 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
108 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime 108 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
109
110services:
111 # Cards configuration to format video in Twitter
112 twitter:
113 username: '@Chocobozzz' # The Twitter @username the card should be attributed to
114 # If true, a video player will be embedded in the Twitter feed on PeerTube video share
115 # If false, we use an image link card that will redirect on your PeerTube instance
116 # Test on https://cards-dev.twitter.com/validator to see if you are whitelisted
117 whitelisted: false \ No newline at end of file
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index e47b71f44..12074a80e 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -161,6 +161,12 @@ function customConfig (): CustomConfig {
161 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT 161 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
162 } 162 }
163 }, 163 },
164 services: {
165 twitter: {
166 username: CONFIG.SERVICES.TWITTER.USERNAME,
167 whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED
168 }
169 },
164 cache: { 170 cache: {
165 previews: { 171 previews: {
166 size: CONFIG.CACHE.PREVIEWS.SIZE 172 size: CONFIG.CACHE.PREVIEWS.SIZE
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index b5dc7b7ed..20f7e5c9c 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -77,8 +77,8 @@ function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
77 'description': videoDescriptionEscaped, 77 'description': videoDescriptionEscaped,
78 'image': previewUrl, 78 'image': previewUrl,
79 79
80 'twitter:card': 'summary_large_image', 80 'twitter:card': CONFIG.SERVICES.TWITTER.WHITELISTED ? 'player' : 'summary_large_image',
81 'twitter:site': '@Chocobozzz', 81 'twitter:site': CONFIG.SERVICES.TWITTER.USERNAME,
82 'twitter:title': videoNameEscaped, 82 'twitter:title': videoNameEscaped,
83 'twitter:description': videoDescriptionEscaped, 83 'twitter:description': videoDescriptionEscaped,
84 'twitter:image': previewUrl, 84 'twitter:image': previewUrl,
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts
index 739f623c6..9bf53e940 100644
--- a/server/initializers/checker.ts
+++ b/server/initializers/checker.ts
@@ -29,7 +29,8 @@ function checkMissedConfig () {
29 'user.video_quota', 29 'user.video_quota',
30 'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads', 30 'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads',
31 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', 31 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
32 'instance.default_nsfw_policy' 32 'instance.default_nsfw_policy',
33 'services.twitter.username', 'services.twitter.whitelisted'
33 ] 34 ]
34 const miss: string[] = [] 35 const miss: string[] = []
35 36
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index c52c27c78..c4e8522c2 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -174,6 +174,12 @@ const CONFIG = {
174 get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') }, 174 get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
175 get CSS () { return config.get<string>('instance.customizations.css') } 175 get CSS () { return config.get<string>('instance.customizations.css') }
176 } 176 }
177 },
178 SERVICES: {
179 TWITTER: {
180 get USERNAME () { return config.get<string>('services.twitter.username') },
181 get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') }
182 }
177 } 183 }
178} 184}
179 185
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index 58b780f38..6aa31e38d 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -26,6 +26,12 @@ describe('Test config API validators', function () {
26 css: 'body { background-color: red; }' 26 css: 'body { background-color: red; }'
27 } 27 }
28 }, 28 },
29 services: {
30 twitter: {
31 username: '@MySuperUsername',
32 whitelisted: true
33 }
34 },
29 cache: { 35 cache: {
30 previews: { 36 previews: {
31 size: 2 37 size: 2
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index 3f1b1532c..f24961b85 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -62,6 +62,8 @@ describe('Test config', function () {
62 expect(data.instance.defaultNSFWPolicy).to.equal('display') 62 expect(data.instance.defaultNSFWPolicy).to.equal('display')
63 expect(data.instance.customizations.css).to.be.empty 63 expect(data.instance.customizations.css).to.be.empty
64 expect(data.instance.customizations.javascript).to.be.empty 64 expect(data.instance.customizations.javascript).to.be.empty
65 expect(data.services.twitter.username).to.equal('@Chocobozzz')
66 expect(data.services.twitter.whitelisted).to.be.false
65 expect(data.cache.previews.size).to.equal(1) 67 expect(data.cache.previews.size).to.equal(1)
66 expect(data.signup.enabled).to.be.true 68 expect(data.signup.enabled).to.be.true
67 expect(data.signup.limit).to.equal(4) 69 expect(data.signup.limit).to.equal(4)
@@ -90,6 +92,12 @@ describe('Test config', function () {
90 css: 'body { background-color: red; }' 92 css: 'body { background-color: red; }'
91 } 93 }
92 }, 94 },
95 services: {
96 twitter: {
97 username: '@Kuja',
98 whitelisted: true
99 }
100 },
93 cache: { 101 cache: {
94 previews: { 102 previews: {
95 size: 2 103 size: 2
@@ -130,6 +138,8 @@ describe('Test config', function () {
130 expect(data.instance.defaultNSFWPolicy).to.equal('blur') 138 expect(data.instance.defaultNSFWPolicy).to.equal('blur')
131 expect(data.instance.customizations.javascript).to.equal('alert("coucou")') 139 expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
132 expect(data.instance.customizations.css).to.equal('body { background-color: red; }') 140 expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
141 expect(data.services.twitter.username).to.equal('@Kuja')
142 expect(data.services.twitter.whitelisted).to.be.true
133 expect(data.cache.previews.size).to.equal(2) 143 expect(data.cache.previews.size).to.equal(2)
134 expect(data.signup.enabled).to.be.false 144 expect(data.signup.enabled).to.be.false
135 expect(data.signup.limit).to.equal(5) 145 expect(data.signup.limit).to.equal(5)
@@ -162,6 +172,8 @@ describe('Test config', function () {
162 expect(data.instance.defaultNSFWPolicy).to.equal('blur') 172 expect(data.instance.defaultNSFWPolicy).to.equal('blur')
163 expect(data.instance.customizations.javascript).to.equal('alert("coucou")') 173 expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
164 expect(data.instance.customizations.css).to.equal('body { background-color: red; }') 174 expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
175 expect(data.services.twitter.username).to.equal('@Kuja')
176 expect(data.services.twitter.whitelisted).to.be.true
165 expect(data.cache.previews.size).to.equal(2) 177 expect(data.cache.previews.size).to.equal(2)
166 expect(data.signup.enabled).to.be.false 178 expect(data.signup.enabled).to.be.false
167 expect(data.signup.limit).to.equal(5) 179 expect(data.signup.limit).to.equal(5)
@@ -205,6 +217,8 @@ describe('Test config', function () {
205 expect(data.instance.defaultNSFWPolicy).to.equal('display') 217 expect(data.instance.defaultNSFWPolicy).to.equal('display')
206 expect(data.instance.customizations.css).to.be.empty 218 expect(data.instance.customizations.css).to.be.empty
207 expect(data.instance.customizations.javascript).to.be.empty 219 expect(data.instance.customizations.javascript).to.be.empty
220 expect(data.services.twitter.username).to.equal('@Chocobozzz')
221 expect(data.services.twitter.whitelisted).to.be.false
208 expect(data.cache.previews.size).to.equal(1) 222 expect(data.cache.previews.size).to.equal(1)
209 expect(data.signup.enabled).to.be.true 223 expect(data.signup.enabled).to.be.true
210 expect(data.signup.limit).to.equal(4) 224 expect(data.signup.limit).to.equal(4)
diff --git a/server/tests/client.ts b/server/tests/client.ts
index 2be1cf5dd..2adb39c5e 100644
--- a/server/tests/client.ts
+++ b/server/tests/client.ts
@@ -11,7 +11,7 @@ import {
11 runServer, 11 runServer,
12 serverLogin, 12 serverLogin,
13 uploadVideo, 13 uploadVideo,
14 getVideosList 14 getVideosList, updateCustomConfig, getCustomConfig
15} from './utils' 15} from './utils'
16 16
17describe('Test a client controllers', function () { 17describe('Test a client controllers', function () {
@@ -73,6 +73,34 @@ describe('Test a client controllers', function () {
73 expect(res.text).to.contain(expectedLink) 73 expect(res.text).to.contain(expectedLink)
74 }) 74 })
75 75
76 it('Should have valid twitter card', async function () {
77 const res = await request(server.url)
78 .get('/videos/watch/' + server.video.uuid)
79 .set('Accept', 'text/html')
80 .expect(200)
81
82 expect(res.text).to.contain('<meta property="twitter:card" content="summary_large_image" />')
83 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
84 })
85
86 it('Should have valid twitter card if Twitter is whitelisted', async function () {
87 const res1 = await getCustomConfig(server.url, server.accessToken)
88 const config = res1.body
89 config.services.twitter = {
90 username: '@Kuja',
91 whitelisted: true
92 }
93 await updateCustomConfig(server.url, server.accessToken, config)
94
95 const res = await request(server.url)
96 .get('/videos/watch/' + server.video.uuid)
97 .set('Accept', 'text/html')
98 .expect(200)
99
100 expect(res.text).to.contain('<meta property="twitter:card" content="player" />')
101 expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
102 })
103
76 after(async function () { 104 after(async function () {
77 process.kill(-server.app.pid) 105 process.kill(-server.app.pid)
78 106
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 30956bd47..a3a651cd8 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -14,6 +14,13 @@ export interface CustomConfig {
14 } 14 }
15 } 15 }
16 16
17 services: {
18 twitter: {
19 username: string
20 whitelisted: boolean
21 }
22 }
23
17 cache: { 24 cache: {
18 previews: { 25 previews: {
19 size: number 26 size: number