Add ability to embed a video in Twitter
authorChocobozzz <me@florianbigard.com>
Thu, 10 May 2018 10:26:47 +0000 (12:26 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 11 May 2018 06:48:20 +0000 (08:48 +0200)
The instance should be whitelisted first

14 files changed:
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
client/src/app/shared/forms/form-validators/custom-config.ts
client/src/polyfills.ts
config/default.yaml
config/production.yaml.example
server/controllers/api/config.ts
server/controllers/client.ts
server/initializers/checker.ts
server/initializers/constants.ts
server/tests/api/check-params/config.ts
server/tests/api/server/config.ts
server/tests/client.ts
shared/models/server/custom-config.model.ts

index 02125245633067ad6073c62187c42d3f4f98fa96..252d43c8f8bfeba4272faf16290edfee1909b80d 100644 (file)
-<div class="form-sub-title">Update PeerTube configuration</div>
-
 <form role="form" [formGroup]="form">
 
-  <div class="inner-form-title">Instance</div>
-
-  <div class="form-group">
-    <label for="instanceName">Name</label>
-    <input
-        type="text" id="instanceName"
-        formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }"
-    >
-    <div *ngIf="formErrors.instanceName" class="form-error">
-      {{ formErrors.instanceName }}
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label for="instanceShortDescription">Short description</label>
-    <textarea
-        id="instanceShortDescription" formControlName="instanceShortDescription"
-        [ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }"
-    ></textarea>
-    <div *ngIf="formErrors.instanceShortDescription" class="form-error">
-      {{ formErrors.instanceShortDescription }}
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
-    <my-markdown-textarea
-        id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true"
-        [classes]="{ 'input-error': formErrors['instanceDescription'] }"
-    ></my-markdown-textarea>
-    <div *ngIf="formErrors.instanceDescription" class="form-error">
-      {{ formErrors.instanceDescription }}
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
-    <my-markdown-textarea
-        id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true"
-        [ngClass]="{ 'input-error': formErrors['instanceTerms'] }"
-    ></my-markdown-textarea>
-    <div *ngIf="formErrors.instanceTerms" class="form-error">
-      {{ formErrors.instanceTerms }}
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label for="instanceDefaultClientRoute">Default client route</label>
-    <div class="peertube-select-container">
-      <select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute">
-        <option value="/videos/trending">Videos Trending</option>
-        <option value="/videos/recently-added">Videos Recently Added</option>
-        <option value="/videos/local">Local videos</option>
-      </select>
-    </div>
-    <div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error">
-      {{ formErrors.instanceDefaultClientRoute }}
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
-    <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>
-
-    <div class="peertube-select-container">
-      <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy">
-        <option value="do_not_list">Do not list</option>
-        <option value="blur">Blur thumbnails</option>
-        <option value="display">Display</option>
-      </select>
-    </div>
-    <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error">
-      {{ formErrors.instanceDefaultNSFWPolicy }}
-    </div>
-  </div>
-
-  <div class="inner-form-title">Cache</div>
-
-  <div class="form-group">
-    <label for="cachePreviewsSize">Preview cache size</label>
-    <input
-      type="text" id="cachePreviewsSize"
-      formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }"
-    >
-    <div *ngIf="formErrors.cachePreviewsSize" class="form-error">
-      {{ formErrors.cachePreviewsSize }}
-    </div>
-  </div>
-
-  <div class="inner-form-title">Signup</div>
-
-  <div class="form-group">
-    <input type="checkbox" id="signupEnabled" formControlName="signupEnabled">
-
-    <label for="signupEnabled"></label>
-    <label for="signupEnabled">Signup enabled</label>
-  </div>
-
-  <div *ngIf="isSignupEnabled()" class="form-group">
-    <label for="signupLimit">Signup limit</label>
-    <input
-        type="text" id="signupLimit"
-        formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }"
-    >
-    <div *ngIf="formErrors.signupLimit" class="form-error">
-      {{ formErrors.signupLimit }}
-    </div>
-  </div>
-
-  <div class="inner-form-title">Administrator</div>
-
-  <div class="form-group">
-    <label for="adminEmail">Admin email</label>
-    <input
-        type="text" id="adminEmail"
-        formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }"
-    >
-    <div *ngIf="formErrors.adminEmail" class="form-error">
-      {{ formErrors.adminEmail }}
-    </div>
-  </div>
-
-  <div class="inner-form-title">Users</div>
-
-  <div class="form-group">
-    <label for="userVideoQuota">User default video quota</label>
-    <div class="peertube-select-container">
-      <select id="userVideoQuota" formControlName="userVideoQuota">
-        <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
-          {{ videoQuotaOption.label }}
-        </option>
-      </select>
-    </div>
-    <div *ngIf="formErrors.userVideoQuota" class="form-error">
-      {{ formErrors.userVideoQuota }}
-    </div>
-  </div>
-
-  <div class="inner-form-title">Transcoding</div>
-
-  <div class="form-group">
-    <input type="checkbox" id="transcodingEnabled" formControlName="transcodingEnabled">
-
-    <label for="transcodingEnabled"></label>
-    <label for="transcodingEnabled">Transcoding enabled</label>
-  </div>
-
-  <ng-template [ngIf]="isTranscodingEnabled()">
-
-    <div class="form-group">
-      <label for="transcodingThreads">Transcoding threads</label>
-      <div class="peertube-select-container">
-        <select id="transcodingThreads" formControlName="transcodingThreads">
-          <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
-            {{ transcodingThreadOption.label }}
-          </option>
-        </select>
-      </div>
-      <div *ngIf="formErrors.transcodingThreads" class="form-error">
-        {{ formErrors.transcodingThreads }}
-      </div>
-    </div>
-
-    <div class="form-group" *ngFor="let resolution of resolutions">
-      <input
-        type="checkbox" [id]="getResolutionKey(resolution)"
-        [formControlName]="getResolutionKey(resolution)"
-      >
-      <label [for]="getResolutionKey(resolution)"></label>
-      <label [for]="getResolutionKey(resolution)">Resolution {{ resolution }} enabled</label>
-    </div>
-  </ng-template>
-
-  <div class="inner-form-title">Customizations</div>
-
-  <div class="form-group">
-    <label for="customizationJavascript">JavaScript</label>
-    <my-help helpType="custom" customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"></my-help>
-    <textarea
-        id="customizationJavascript" formControlName="customizationJavascript"
-        [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }"
-    ></textarea>
-    <div *ngIf="formErrors.customizationJavascript" class="form-error">
-      {{ formErrors.customizationJavascript }}
-    </div>
-  </div>
-
-  <div class="form-group">
-    <label for="customizationCSS">CSS</label>
-    <my-help
-        helpType="custom"
-        customHtml="
-          Write directly CSS code. Example:<br />
-          <pre>
-body {
-  background-color: red;
-}
-          </pre>
-
-          Prepend with <em>#custom-css</em> to override styles. Example:
-          <pre>
-#custom-css .logged-in-email {
-  color: red;
-}
-          </pre>
-        "
-    ></my-help>
-    <textarea
-        id="customizationCSS" formControlName="customizationCSS"
-        [ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
-    ></textarea>
-    <div *ngIf="formErrors.customizationCSS" class="form-error">
-      {{ formErrors.customizationCSS }}
-    </div>
-  </div>
+  <tabset class="root-tabset bootstrap">
+
+    <tab heading="Basic configuration">
+
+      <div class="inner-form-title">Instance</div>
+
+      <div class="form-group">
+        <label for="instanceName">Name</label>
+        <input
+            type="text" id="instanceName"
+            formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }"
+        >
+        <div *ngIf="formErrors.instanceName" class="form-error">
+          {{ formErrors.instanceName }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label for="instanceShortDescription">Short description</label>
+        <textarea
+            id="instanceShortDescription" formControlName="instanceShortDescription"
+            [ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }"
+        ></textarea>
+        <div *ngIf="formErrors.instanceShortDescription" class="form-error">
+          {{ formErrors.instanceShortDescription }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
+        <my-markdown-textarea
+            id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true"
+            [classes]="{ 'input-error': formErrors['instanceDescription'] }"
+        ></my-markdown-textarea>
+        <div *ngIf="formErrors.instanceDescription" class="form-error">
+          {{ formErrors.instanceDescription }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
+        <my-markdown-textarea
+            id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true"
+            [ngClass]="{ 'input-error': formErrors['instanceTerms'] }"
+        ></my-markdown-textarea>
+        <div *ngIf="formErrors.instanceTerms" class="form-error">
+          {{ formErrors.instanceTerms }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label for="instanceDefaultClientRoute">Default client route</label>
+        <div class="peertube-select-container">
+          <select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute">
+            <option value="/videos/trending">Videos Trending</option>
+            <option value="/videos/recently-added">Videos Recently Added</option>
+            <option value="/videos/local">Local videos</option>
+          </select>
+        </div>
+        <div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error">
+          {{ formErrors.instanceDefaultClientRoute }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
+        <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>
+
+        <div class="peertube-select-container">
+          <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy">
+            <option value="do_not_list">Do not list</option>
+            <option value="blur">Blur thumbnails</option>
+            <option value="display">Display</option>
+          </select>
+        </div>
+        <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error">
+          {{ formErrors.instanceDefaultNSFWPolicy }}
+        </div>
+      </div>
+
+      <div class="inner-form-title">Signup</div>
+
+      <div class="form-group">
+        <input type="checkbox" id="signupEnabled" formControlName="signupEnabled">
+
+        <label for="signupEnabled"></label>
+        <label for="signupEnabled">Signup enabled</label>
+      </div>
+
+      <div *ngIf="isSignupEnabled()" class="form-group">
+        <label for="signupLimit">Signup limit</label>
+        <input
+            type="text" id="signupLimit"
+            formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }"
+        >
+        <div *ngIf="formErrors.signupLimit" class="form-error">
+          {{ formErrors.signupLimit }}
+        </div>
+      </div>
+
+      <div class="inner-form-title">Administrator</div>
+
+      <div class="form-group">
+        <label for="adminEmail">Admin email</label>
+        <input
+            type="text" id="adminEmail"
+            formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }"
+        >
+        <div *ngIf="formErrors.adminEmail" class="form-error">
+          {{ formErrors.adminEmail }}
+        </div>
+      </div>
+
+      <div class="inner-form-title">Users</div>
+
+      <div class="form-group">
+        <label for="userVideoQuota">User default video quota</label>
+        <div class="peertube-select-container">
+          <select id="userVideoQuota" formControlName="userVideoQuota">
+            <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
+              {{ videoQuotaOption.label }}
+            </option>
+          </select>
+        </div>
+        <div *ngIf="formErrors.userVideoQuota" class="form-error">
+          {{ formErrors.userVideoQuota }}
+        </div>
+      </div>
+    </tab>
+
+    <tab heading="Services">
+
+      <div class="inner-form-title">Twitter</div>
+
+      <div class="form-group">
+        <label for="signupLimit">Your Twitter username</label>
+        <my-help helpType="custom" customHtml="The Twitter @username the cards (created by PeerTube video shares) should be attributed to."></my-help>
+        <input
+            type="text" id="servicesTwitterUsername"
+            formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }"
+        >
+        <div *ngIf="formErrors.servicesTwitterUsername" class="form-error">
+          {{ formErrors.servicesTwitterUsername }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <input type="checkbox" id="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted">
+
+        <label for="servicesTwitterWhitelisted"></label>
+        <label for="servicesTwitterWhitelisted">Instance whitelisted by Twitter</label>
+        <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 />
+If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
+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>
+
+      </div>
+    </tab>
+
+    <tab heading="Advanced configuration">
+
+      <div class="inner-form-title">Transcoding</div>
+
+      <div class="form-group">
+        <input type="checkbox" id="transcodingEnabled" formControlName="transcodingEnabled">
+
+        <label for="transcodingEnabled"></label>
+        <label for="transcodingEnabled">Transcoding enabled</label>
+      </div>
+
+      <ng-template [ngIf]="isTranscodingEnabled()">
+
+        <div class="form-group">
+          <label for="transcodingThreads">Transcoding threads</label>
+          <div class="peertube-select-container">
+            <select id="transcodingThreads" formControlName="transcodingThreads">
+              <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
+                {{ transcodingThreadOption.label }}
+              </option>
+            </select>
+          </div>
+          <div *ngIf="formErrors.transcodingThreads" class="form-error">
+            {{ formErrors.transcodingThreads }}
+          </div>
+        </div>
+
+        <div class="form-group" *ngFor="let resolution of resolutions">
+          <input
+              type="checkbox" [id]="getResolutionKey(resolution)"
+              [formControlName]="getResolutionKey(resolution)"
+          >
+          <label [for]="getResolutionKey(resolution)"></label>
+          <label [for]="getResolutionKey(resolution)">Resolution {{ resolution }} enabled</label>
+        </div>
+      </ng-template>
+
+      <div class="inner-form-title">Cache</div>
+
+      <div class="form-group">
+        <label for="cachePreviewsSize">Preview cache size</label>
+        <my-help helpType="custom" customHtml="Previews are not federated. We fetch them directly from the origin instance and cache them."></my-help>
+
+        <input
+            type="text" id="cachePreviewsSize"
+            formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }"
+        >
+        <div *ngIf="formErrors.cachePreviewsSize" class="form-error">
+          {{ formErrors.cachePreviewsSize }}
+        </div>
+      </div>
+
+      <div class="inner-form-title">Customizations</div>
+
+      <div class="form-group">
+        <label for="customizationJavascript">JavaScript</label>
+        <my-help helpType="custom" customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"></my-help>
+        <textarea
+            id="customizationJavascript" formControlName="customizationJavascript"
+            [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }"
+        ></textarea>
+        <div *ngIf="formErrors.customizationJavascript" class="form-error">
+          {{ formErrors.customizationJavascript }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label for="customizationCSS">CSS</label>
+        <my-help
+            helpType="custom"
+            customHtml="
+              Write directly CSS code. Example:<br />
+              <pre>
+    body {
+      background-color: red;
+    }
+              </pre>
+
+              Prepend with <em>#custom-css</em> to override styles. Example:
+              <pre>
+    #custom-css .logged-in-email {
+      color: red;
+    }
+              </pre>
+            "
+        ></my-help>
+        <textarea
+            id="customizationCSS" formControlName="customizationCSS"
+            [ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
+        ></textarea>
+        <div *ngIf="formErrors.customizationCSS" class="form-error">
+          {{ formErrors.customizationCSS }}
+        </div>
+      </div>
+    </tab>
+  </tabset>
 
   <input (click)="formValidated()" type="submit" value="Update configuration" [disabled]="!form.valid">
 </form>
index 2ab371cbbdc0c2e457d82f9f389ad94fc729565f..a1e334a746af0533079781ebd09a81cb6af7a0c6 100644 (file)
@@ -8,7 +8,7 @@ import { FormReactive, USER_VIDEO_QUOTA } from '@app/shared'
 import {
   ADMIN_EMAIL,
   CACHE_PREVIEWS_SIZE,
-  INSTANCE_NAME, INSTANCE_SHORT_DESCRIPTION,
+  INSTANCE_NAME, INSTANCE_SHORT_DESCRIPTION, SERVICES_TWITTER_USERNAME,
   SIGNUP_LIMIT,
   TRANSCODING_THREADS
 } from '@app/shared/forms/form-validators/custom-config'
@@ -49,6 +49,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
     instanceTerms: '',
     instanceDefaultClientRoute: '',
     instanceDefaultNSFWPolicy: '',
+    servicesTwitterUsername: '',
     cachePreviewsSize: '',
     signupLimit: '',
     adminEmail: '',
@@ -60,6 +61,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
   validationMessages = {
     instanceShortDescription: INSTANCE_SHORT_DESCRIPTION.MESSAGES,
     instanceName: INSTANCE_NAME.MESSAGES,
+    servicesTwitterUsername: SERVICES_TWITTER_USERNAME,
     cachePreviewsSize: CACHE_PREVIEWS_SIZE.MESSAGES,
     signupLimit: SIGNUP_LIMIT.MESSAGES,
     adminEmail: ADMIN_EMAIL.MESSAGES,
@@ -92,6 +94,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
       instanceTerms: [ '' ],
       instanceDefaultClientRoute: [ '' ],
       instanceDefaultNSFWPolicy: [ '' ],
+      servicesTwitterUsername: [ '', SERVICES_TWITTER_USERNAME.VALIDATORS ],
+      servicesTwitterWhitelisted: [ ],
       cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ],
       signupEnabled: [ ],
       signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ],
@@ -175,6 +179,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
           css: this.form.value['customizationCSS']
         }
       },
+      services: {
+        twitter: {
+          username: this.form.value['servicesTwitterUsername'],
+          whitelisted: this.form.value['servicesTwitterWhitelisted']
+        }
+      },
       cache: {
         previews: {
           size: this.form.value['cachePreviewsSize']
@@ -228,6 +238,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
       instanceTerms: this.customConfig.instance.terms,
       instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
       instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
+      servicesTwitterUsername: this.customConfig.services.twitter.username,
+      servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
       cachePreviewsSize: this.customConfig.cache.previews.size,
       signupEnabled: this.customConfig.signup.enabled,
       signupLimit: this.customConfig.signup.limit,
index c9cef2e09025045dad0a4b09bc8f7992199fb429..e3d9a4c7b44be174593d1645a0c6705795c3c62a 100644 (file)
@@ -14,6 +14,13 @@ export const INSTANCE_SHORT_DESCRIPTION = {
   }
 }
 
+export const SERVICES_TWITTER_USERNAME = {
+  VALIDATORS: [ Validators.required ],
+  MESSAGES: {
+    'required': 'Twitter username is required.'
+  }
+}
+
 export const CACHE_PREVIEWS_SIZE = {
   VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
   MESSAGES: {
index 12b317101485d2737d260b0ea81a51362b4b08fc..423a7b91589f559649ed309edbf0c04d70b1cf7b 100644 (file)
@@ -35,6 +35,7 @@ import 'core-js/es6/regexp';
 import 'core-js/es6/map';
 import 'core-js/es6/weak-map';
 import 'core-js/es6/set';
+import 'core-js/es7/object';
 
 /** IE10 and IE11 requires the following for NgClass support on SVG elements */
 // import 'classlist.js';  // Run `npm install --save classlist.js`.
@@ -44,7 +45,6 @@ import 'core-js/es6/set';
 // For Google Bot
 import 'core-js/es6/reflect';
 
-
 /** Evergreen browsers require these. **/
 // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
 import 'core-js/es7/reflect'
index 25dde72c96db5af92d0fff3e880e85fd76e5861b..2826e76f8220dfb7b88dae9e64cbac72f7512740 100644 (file)
@@ -90,3 +90,12 @@ instance:
   customizations:
     javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
     css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
+
+services:
+  # Cards configuration to format video in Twitter
+  twitter:
+    username: '@Chocobozzz' # The Twitter @username the card should be attributed to
+    # If true, a video player will be embedded in the Twitter feed on PeerTube video share
+    # If false, we use an image link card that will redirect on your PeerTube instance
+    # Change it to "true", and then test on https://cards-dev.twitter.com/validator to see if you are whitelisted
+    whitelisted: false
index 1d7d35c9c899ed97bd4cf4fd6bd56d4f8d43093d..a6f1740fedde17ee8be663acb955928f40969128 100644 (file)
@@ -106,3 +106,12 @@ instance:
   customizations:
     javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
     css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
+
+services:
+  # Cards configuration to format video in Twitter
+  twitter:
+    username: '@Chocobozzz' # The Twitter @username the card should be attributed to
+    # If true, a video player will be embedded in the Twitter feed on PeerTube video share
+    # If false, we use an image link card that will redirect on your PeerTube instance
+    # Test on https://cards-dev.twitter.com/validator to see if you are whitelisted
+    whitelisted: false
\ No newline at end of file
index e47b71f44995c0c62983b52249da11da35fe5139..12074a80e408b63e857976c09c5c0c188b4b00c3 100644 (file)
@@ -161,6 +161,12 @@ function customConfig (): CustomConfig {
         javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
       }
     },
+    services: {
+      twitter: {
+        username: CONFIG.SERVICES.TWITTER.USERNAME,
+        whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED
+      }
+    },
     cache: {
       previews: {
         size: CONFIG.CACHE.PREVIEWS.SIZE
index b5dc7b7eddf13d9ca2cc121dfa16ff065852e8f8..20f7e5c9c2a613abee62adb73310615c11f991b5 100644 (file)
@@ -77,8 +77,8 @@ function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
     'description': videoDescriptionEscaped,
     'image': previewUrl,
 
-    'twitter:card': 'summary_large_image',
-    'twitter:site': '@Chocobozzz',
+    'twitter:card': CONFIG.SERVICES.TWITTER.WHITELISTED ? 'player' : 'summary_large_image',
+    'twitter:site': CONFIG.SERVICES.TWITTER.USERNAME,
     'twitter:title': videoNameEscaped,
     'twitter:description': videoDescriptionEscaped,
     'twitter:image': previewUrl,
index 739f623c605a656e2f5e10e0e85596188589a05b..9bf53e940e7869d596eca90731dfef4057d700b2 100644 (file)
@@ -29,7 +29,8 @@ function checkMissedConfig () {
     'user.video_quota',
     'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads',
     'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
-    'instance.default_nsfw_policy'
+    'instance.default_nsfw_policy',
+    'services.twitter.username', 'services.twitter.whitelisted'
   ]
   const miss: string[] = []
 
index c52c27c78672d3549cc63748cc4e4ee10a95ee2a..c4e8522c297ad7ceca1d6711299eb4e576d65d5e 100644 (file)
@@ -174,6 +174,12 @@ const CONFIG = {
       get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
       get CSS () { return config.get<string>('instance.customizations.css') }
     }
+  },
+  SERVICES: {
+    TWITTER: {
+      get USERNAME () { return config.get<string>('services.twitter.username') },
+      get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') }
+    }
   }
 }
 
index 58b780f38f9677d4ad79fb228e264bec18c1f173..6aa31e38d649859c0fb06062efbd1586024cd3f3 100644 (file)
@@ -26,6 +26,12 @@ describe('Test config API validators', function () {
         css: 'body { background-color: red; }'
       }
     },
+    services: {
+      twitter: {
+        username: '@MySuperUsername',
+        whitelisted: true
+      }
+    },
     cache: {
       previews: {
         size: 2
index 3f1b1532cb319399b73bc3857061020284cdf8ed..f24961b858372a1b7de47981f6b99fed0255989d 100644 (file)
@@ -62,6 +62,8 @@ describe('Test config', function () {
     expect(data.instance.defaultNSFWPolicy).to.equal('display')
     expect(data.instance.customizations.css).to.be.empty
     expect(data.instance.customizations.javascript).to.be.empty
+    expect(data.services.twitter.username).to.equal('@Chocobozzz')
+    expect(data.services.twitter.whitelisted).to.be.false
     expect(data.cache.previews.size).to.equal(1)
     expect(data.signup.enabled).to.be.true
     expect(data.signup.limit).to.equal(4)
@@ -90,6 +92,12 @@ describe('Test config', function () {
           css: 'body { background-color: red; }'
         }
       },
+      services: {
+        twitter: {
+          username: '@Kuja',
+          whitelisted: true
+        }
+      },
       cache: {
         previews: {
           size: 2
@@ -130,6 +138,8 @@ describe('Test config', function () {
     expect(data.instance.defaultNSFWPolicy).to.equal('blur')
     expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
     expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
+    expect(data.services.twitter.username).to.equal('@Kuja')
+    expect(data.services.twitter.whitelisted).to.be.true
     expect(data.cache.previews.size).to.equal(2)
     expect(data.signup.enabled).to.be.false
     expect(data.signup.limit).to.equal(5)
@@ -162,6 +172,8 @@ describe('Test config', function () {
     expect(data.instance.defaultNSFWPolicy).to.equal('blur')
     expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
     expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
+    expect(data.services.twitter.username).to.equal('@Kuja')
+    expect(data.services.twitter.whitelisted).to.be.true
     expect(data.cache.previews.size).to.equal(2)
     expect(data.signup.enabled).to.be.false
     expect(data.signup.limit).to.equal(5)
@@ -205,6 +217,8 @@ describe('Test config', function () {
     expect(data.instance.defaultNSFWPolicy).to.equal('display')
     expect(data.instance.customizations.css).to.be.empty
     expect(data.instance.customizations.javascript).to.be.empty
+    expect(data.services.twitter.username).to.equal('@Chocobozzz')
+    expect(data.services.twitter.whitelisted).to.be.false
     expect(data.cache.previews.size).to.equal(1)
     expect(data.signup.enabled).to.be.true
     expect(data.signup.limit).to.equal(4)
index 2be1cf5dda44919c4dbc51829ec1ca274b84713d..2adb39c5eed8d1c47ad9c995209f5acff7941c69 100644 (file)
@@ -11,7 +11,7 @@ import {
   runServer,
   serverLogin,
   uploadVideo,
-  getVideosList
+  getVideosList, updateCustomConfig, getCustomConfig
 } from './utils'
 
 describe('Test a client controllers', function () {
@@ -73,6 +73,34 @@ describe('Test a client controllers', function () {
     expect(res.text).to.contain(expectedLink)
   })
 
+  it('Should have valid twitter card', async function () {
+    const res = await request(server.url)
+      .get('/videos/watch/' + server.video.uuid)
+      .set('Accept', 'text/html')
+      .expect(200)
+
+    expect(res.text).to.contain('<meta property="twitter:card" content="summary_large_image" />')
+    expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
+  })
+
+  it('Should have valid twitter card if Twitter is whitelisted', async function () {
+    const res1 = await getCustomConfig(server.url, server.accessToken)
+    const config = res1.body
+    config.services.twitter = {
+      username: '@Kuja',
+      whitelisted: true
+    }
+    await updateCustomConfig(server.url, server.accessToken, config)
+
+    const res = await request(server.url)
+      .get('/videos/watch/' + server.video.uuid)
+      .set('Accept', 'text/html')
+      .expect(200)
+
+    expect(res.text).to.contain('<meta property="twitter:card" content="player" />')
+    expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
+  })
+
   after(async function () {
     process.kill(-server.app.pid)
 
index 30956bd47eed643f6c4d2a58d832a0e729843dfa..a3a651cd88aa1bc952b9314718b1509b43b4aae7 100644 (file)
@@ -14,6 +14,13 @@ export interface CustomConfig {
     }
   }
 
+  services: {
+    twitter: {
+      username: string
+      whitelisted: boolean
+    }
+  }
+
   cache: {
     previews: {
       size: number