diff options
-rw-r--r-- | client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html | 471 | ||||
-rw-r--r-- | client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | 14 | ||||
-rw-r--r-- | client/src/app/shared/forms/form-validators/custom-config.ts | 7 | ||||
-rw-r--r-- | client/src/polyfills.ts | 2 | ||||
-rw-r--r-- | config/default.yaml | 9 | ||||
-rw-r--r-- | config/production.yaml.example | 9 | ||||
-rw-r--r-- | server/controllers/api/config.ts | 6 | ||||
-rw-r--r-- | server/controllers/client.ts | 4 | ||||
-rw-r--r-- | server/initializers/checker.ts | 3 | ||||
-rw-r--r-- | server/initializers/constants.ts | 6 | ||||
-rw-r--r-- | server/tests/api/check-params/config.ts | 6 | ||||
-rw-r--r-- | server/tests/api/server/config.ts | 14 | ||||
-rw-r--r-- | server/tests/client.ts | 30 | ||||
-rw-r--r-- | shared/models/server/custom-config.model.ts | 7 |
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"> | 155 | If 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"> | 156 | Check 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> |
199 | body { | 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' | |||
8 | import { | 8 | import { |
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 | ||
17 | export const SERVICES_TWITTER_USERNAME = { | ||
18 | VALIDATORS: [ Validators.required ], | ||
19 | MESSAGES: { | ||
20 | 'required': 'Twitter username is required.' | ||
21 | } | ||
22 | } | ||
23 | |||
17 | export const CACHE_PREVIEWS_SIZE = { | 24 | export 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'; | |||
35 | import 'core-js/es6/map'; | 35 | import 'core-js/es6/map'; |
36 | import 'core-js/es6/weak-map'; | 36 | import 'core-js/es6/weak-map'; |
37 | import 'core-js/es6/set'; | 37 | import 'core-js/es6/set'; |
38 | import '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 |
45 | import 'core-js/es6/reflect'; | 46 | import '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. |
50 | import 'core-js/es7/reflect' | 50 | import '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 | |||
94 | services: | ||
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 | |||
110 | services: | ||
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 | ||
17 | describe('Test a client controllers', function () { | 17 | describe('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 |