]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - support/doc/plugins/guide.md
External auth can update user on login
[github/Chocobozzz/PeerTube.git] / support / doc / plugins / guide.md
index e30d95fc94434b249977760bdfea5dd200e7049e..9ddab3ece7c89da363e03ea2efeb273de3610735 100644 (file)
@@ -3,7 +3,6 @@
 <!-- START doctoc generated TOC please keep comment here to allow auto update -->
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 
 <!-- START doctoc generated TOC please keep comment here to allow auto update -->
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 
-
 - [Concepts](#concepts)
   - [Hooks](#hooks)
   - [Static files](#static-files)
 - [Concepts](#concepts)
   - [Hooks](#hooks)
   - [Static files](#static-files)
     - [Storage](#storage)
     - [Update video constants](#update-video-constants)
     - [Add custom routes](#add-custom-routes)
     - [Storage](#storage)
     - [Update video constants](#update-video-constants)
     - [Add custom routes](#add-custom-routes)
+    - [Add custom WebSocket handlers](#add-custom-websocket-handlers)
     - [Add external auth methods](#add-external-auth-methods)
     - [Add new transcoding profiles](#add-new-transcoding-profiles)
     - [Add external auth methods](#add-external-auth-methods)
     - [Add new transcoding profiles](#add-new-transcoding-profiles)
-  - [Helpers](#helpers)
+    - [Server helpers](#server-helpers)
   - [Client API (themes & plugins)](#client-api-themes--plugins)
   - [Client API (themes & plugins)](#client-api-themes--plugins)
-    - [Plugin static route](#plugin-static-route)
+    - [Get plugin static and router routes](#get-plugin-static-and-router-routes)
     - [Notifier](#notifier)
     - [Markdown Renderer](#markdown-renderer)
     - [Notifier](#notifier)
     - [Markdown Renderer](#markdown-renderer)
+    - [Auth header](#auth-header)
     - [Custom Modal](#custom-modal)
     - [Translate](#translate)
     - [Get public settings](#get-public-settings)
     - [Get server config](#get-server-config)
     - [Add custom fields to video form](#add-custom-fields-to-video-form)
     - [Register settings script](#register-settings-script)
     - [Custom Modal](#custom-modal)
     - [Translate](#translate)
     - [Get public settings](#get-public-settings)
     - [Get server config](#get-server-config)
     - [Add custom fields to video form](#add-custom-fields-to-video-form)
     - [Register settings script](#register-settings-script)
+    - [Plugin selector on HTML elements](#plugin-selector-on-html-elements)
+    - [HTML placeholder elements](#html-placeholder-elements)
+    - [Add/remove left menu links](#addremove-left-menu-links)
+    - [Create client page](#create-client-page)
   - [Publishing](#publishing)
 - [Write a plugin/theme](#write-a-plugintheme)
   - [Clone the quickstart repository](#clone-the-quickstart-repository)
   - [Publishing](#publishing)
 - [Write a plugin/theme](#write-a-plugintheme)
   - [Clone the quickstart repository](#clone-the-quickstart-repository)
@@ -37,6 +42,7 @@
   - [Build your plugin](#build-your-plugin)
   - [Test your plugin/theme](#test-your-plugintheme)
   - [Publish](#publish)
   - [Build your plugin](#build-your-plugin)
   - [Test your plugin/theme](#test-your-plugintheme)
   - [Publish](#publish)
+  - [Unpublish](#unpublish)
 - [Plugin & Theme hooks/helpers API](#plugin--theme-hookshelpers-api)
 - [Tips](#tips)
   - [Compatibility with PeerTube](#compatibility-with-peertube)
 - [Plugin & Theme hooks/helpers API](#plugin--theme-hookshelpers-api)
 - [Tips](#tips)
   - [Compatibility with PeerTube](#compatibility-with-peertube)
@@ -103,6 +109,20 @@ async function register ({
 }
 ```
 
 }
 ```
 
+Hooks prefixed by `action:api` also give access the original **express** [Request](http://expressjs.com/en/api.html#req) and [Response](http://expressjs.com/en/api.html#res):
+
+```js
+async function register ({
+  registerHook,
+  peertubeHelpers: { logger }
+}) {
+  registerHook({
+    target: 'action:api.video.updated',
+    handler: ({ req, res }) => logger.debug('original request parameters', { params: req.params })
+  })
+}
+```
+
 
 On client side, these hooks are registered by the `clientScripts` files defined in `package.json`.
 All client scripts have scopes so PeerTube client only loads scripts it needs:
 
 On client side, these hooks are registered by the `clientScripts` files defined in `package.json`.
 All client scripts have scopes so PeerTube client only loads scripts it needs:
@@ -171,9 +191,27 @@ function register (...) {
   registerSetting({
     name: 'admin-name',
     label: 'Admin name',
   registerSetting({
     name: 'admin-name',
     label: 'Admin name',
+
     type: 'input',
     type: 'input',
-    // type: input | input-checkbox | input-password | input-textarea | markdown-text | markdown-enhanced | 'select' | 'html'
-    default: 'my super name'
+    // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
+
+    // If type: 'select', give the select available options
+    options: [
+      { label: 'Label 1', value: 'value1' },
+      { label: 'Label 2', value: 'value2' }
+    ],
+
+    // If type: 'html', set the HTML that will be injected in the page
+    html: '<strong class="...">Hello</strong><br /><br />'
+
+    // Optional
+    descriptionHTML: 'The purpose of this field is...',
+
+    default: 'my super name',
+
+    // If the setting is not private, anyone can view its value (client code included)
+    // If the setting is private, only server-side hooks can access it
+    private: false
   })
 
   const adminName = await settingsManager.getSetting('admin-name')
   })
 
   const adminName = await settingsManager.getSetting('admin-name')
@@ -182,7 +220,7 @@ function register (...) {
   result['admin-name]
 
   settingsManager.onSettingsChange(settings => {
   result['admin-name]
 
   settingsManager.onSettingsChange(settings => {
-    settings['admin-name])
+    settings['admin-name']
   })
 }
 ```
   })
 }
 ```
@@ -194,29 +232,55 @@ Plugins can store/load JSON data, that PeerTube will store in its database (so d
 Example:
 
 ```js
 Example:
 
 ```js
-function register (...) {
+function register ({
+  storageManager
+}) {
   const value = await storageManager.getData('mykey')
   await storageManager.storeData('mykey', { subkey: 'value' })
 }
 ```
 
   const value = await storageManager.getData('mykey')
   await storageManager.storeData('mykey', { subkey: 'value' })
 }
 ```
 
+You can also store files in the plugin data directory (`/{plugins-directory}/data/{npm-plugin-name}`) **in PeerTube >= 3.2**.
+This directory and its content won't be deleted when your plugin is uninstalled/upgraded.
+
+```js
+function register ({
+  storageManager,
+  peertubeHelpers
+}) {
+  const basePath = peertubeHelpers.plugin.getDataDirectoryPath()
+
+  fs.writeFile(path.join(basePath, 'filename.txt'), 'content of my file', function (err) {
+    ...
+  })
+}
+```
+
 #### Update video constants
 
 #### Update video constants
 
-You can add/delete video categories, licences or languages using the appropriate managers:
+You can add/delete video categories, licences or languages using the appropriate constant managers:
 
 ```js
 
 ```js
-function register (...) {
-  videoLanguageManager.addLanguage('al_bhed', 'Al Bhed')
-  videoLanguageManager.deleteLanguage('fr')
+function register ({
+  videoLanguageManager,
+  videoCategoryManager,
+  videoLicenceManager,
+  videoPrivacyManager,
+  playlistPrivacyManager
+}) {
+  videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
+  videoLanguageManager.deleteConstant('fr')
 
 
-  videoCategoryManager.addCategory(42, 'Best category')
-  videoCategoryManager.deleteCategory(1) // Music
+  videoCategoryManager.addConstant(42, 'Best category')
+  videoCategoryManager.deleteConstant(1) // Music
+  videoCategoryManager.resetConstants() // Reset to initial categories
+  videoCategoryManager.getConstants() // Retrieve all category constants
 
 
-  videoLicenceManager.addLicence(42, 'Best licence')
-  videoLicenceManager.deleteLicence(7) // Public domain
+  videoLicenceManager.addConstant(42, 'Best licence')
+  videoLicenceManager.deleteConstant(7) // Public domain
 
 
-  videoPrivacyManager.deletePrivacy(2) // Remove Unlisted video privacy
-  playlistPrivacyManager.deletePlaylistPrivacy(3) // Remove Private video playlist privacy
+  videoPrivacyManager.deleteConstant(2) // Remove Unlisted video privacy
+  playlistPrivacyManager.deleteConstant(3) // Remove Private video playlist privacy
 }
 ```
 
 }
 ```
 
@@ -225,9 +289,27 @@ function register (...) {
 You can create custom routes using an [express Router](https://expressjs.com/en/4x/api.html#router) for your plugin:
 
 ```js
 You can create custom routes using an [express Router](https://expressjs.com/en/4x/api.html#router) for your plugin:
 
 ```js
-function register (...) {
+function register ({
+  router
+}) {
   const router = getRouter()
   router.get('/ping', (req, res) => res.json({ message: 'pong' }))
   const router = getRouter()
   router.get('/ping', (req, res) => res.json({ message: 'pong' }))
+
+  // Users are automatically authenticated
+  router.get('/auth', async (res, res) => {
+    const user = await peertubeHelpers.user.getAuthUser(res)
+
+    const isAdmin = user.role === 0
+    const isModerator = user.role === 1
+    const isUser = user.role === 2
+
+    res.json({
+      username: user.username,
+      isAdmin,
+      isModerator,
+      isUser
+    })
+  })
 }
 ```
 
 }
 ```
 
@@ -236,6 +318,43 @@ The `ping` route can be accessed using:
  * Or `/plugins/:pluginName/router/ping`
 
 
  * Or `/plugins/:pluginName/router/ping`
 
 
+#### Add custom WebSocket handlers
+
+**PeerTube >= 5.0**
+
+You can create custom WebSocket servers (like [ws](https://github.com/websockets/ws) for example) using `registerWebSocketRoute`:
+
+```js
+function register ({
+  registerWebSocketRoute,
+  peertubeHelpers
+}) {
+  const wss = new WebSocketServer({ noServer: true })
+
+  wss.on('connection', function connection(ws) {
+    peertubeHelpers.logger.info('WebSocket connected!')
+
+    setInterval(() => {
+      ws.send('WebSocket message sent by server');
+    }, 1000)
+  })
+
+  registerWebSocketRoute({
+    route: '/my-websocket-route',
+
+    handler: (request, socket, head) => {
+      wss.handleUpgrade(request, socket, head, ws => {
+        wss.emit('connection', ws, request)
+      })
+    }
+  })
+}
+```
+
+The `my-websocket-route` route can be accessed using:
+ * `/plugins/:pluginName/:pluginVersion/ws/my-websocket-route`
+ * Or `/plugins/:pluginName/ws/my-websocket-route`
+
 #### Add external auth methods
 
 If you want to add a classic username/email and password auth method (like [LDAP](https://framagit.org/framasoft/peertube/official-plugins/-/tree/master/peertube-plugin-auth-ldap) for example):
 #### Add external auth methods
 
 If you want to add a classic username/email and password auth method (like [LDAP](https://framagit.org/framasoft/peertube/official-plugins/-/tree/master/peertube-plugin-auth-ldap) for example):
@@ -314,7 +433,27 @@ function register (...) {
       username: 'user'
       email: 'user@example.com'
       role: 2
       username: 'user'
       email: 'user@example.com'
       role: 2
-      displayName: 'User display name'
+      displayName: 'User display name',
+
+      // Custom admin flags (bypass video auto moderation etc.)
+      // https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/users/user-flag.model.ts
+      // PeerTube >= 5.1
+      adminFlags: 0,
+      // Quota in bytes
+      // PeerTube >= 5.1
+      videoQuota: 1024 * 1024 * 1024, // 1GB
+      // PeerTube >= 5.1
+      videoQuotaDaily: -1, // Unlimited
+
+      // Update the user profile if it already exists
+      // Default behaviour is no update
+      // Introduced in PeerTube >= 5.1
+      userUpdater: ({ fieldName, currentValue, newValue }) => {
+        // Always use new value except for videoQuotaDaily field
+        if (fieldName === 'videoQuotaDaily') return currentValue
+
+        return newValue
+      }
     })
   })
 
     })
   })
 
@@ -424,7 +563,7 @@ async function register ({
 During live transcode input options are applied once for each target resolution.
 Plugins are responsible for detecting such situation and applying input options only once if necessary.
 
 During live transcode input options are applied once for each target resolution.
 Plugins are responsible for detecting such situation and applying input options only once if necessary.
 
-### Helpers
+#### Server helpers
 
 PeerTube provides your plugin some helpers. For example:
 
 
 PeerTube provides your plugin some helpers. For example:
 
@@ -450,7 +589,7 @@ See the [plugin API reference](https://docs.joinpeertube.org/api-plugins) to see
 
 ### Client API (themes & plugins)
 
 
 ### Client API (themes & plugins)
 
-#### Plugin static route
+#### Get plugin static and router routes
 
 To get your plugin static route:
 
 
 To get your plugin static route:
 
@@ -461,6 +600,24 @@ function register (...) {
 }
 ```
 
 }
 ```
 
+And to get your plugin router route, use `peertubeHelpers.getBaseRouterRoute()`:
+
+```js
+function register (...) {
+  registerHook({
+    target: 'action:video-watch.video.loaded',
+    handler: ({ video }) => {
+      fetch(peertubeHelpers.getBaseRouterRoute() + '/my/plugin/api', {
+        method: 'GET',
+        headers: peertubeHelpers.getAuthHeader()
+      }).then(res => res.json())
+        .then(data => console.log('Hi %s.', data))
+    }
+  })
+}
+```
+
+
 #### Notifier
 
 To notify the user with the PeerTube ToastModule:
 #### Notifier
 
 To notify the user with the PeerTube ToastModule:
@@ -489,6 +646,30 @@ function register (...) {
 }
 ```
 
 }
 ```
 
+#### Auth header
+
+**PeerTube >= 3.2**
+
+To make your own HTTP requests using the current authenticated user, use an helper to automatically set appropriate headers:
+
+```js
+function register (...) {
+  registerHook({
+    target: 'action:auth-user.information-loaded',
+    handler: ({ user }) => {
+
+      // Useless because we have the same info in the ({ user }) parameter
+      // It's just an example
+      fetch('/api/v1/users/me', {
+        method: 'GET',
+        headers: peertubeHelpers.getAuthHeader()
+      }).then(res => res.json())
+        .then(data => console.log('Hi %s.', data.username))
+    }
+  })
+}
+```
+
 #### Custom Modal
 
 To show a custom modal:
 #### Custom Modal
 
 To show a custom modal:
@@ -560,12 +741,37 @@ async function register ({ registerVideoField, peertubeHelpers }) {
     name: 'my-field-name,
     label: 'My added field',
     descriptionHTML: 'Optional description',
     name: 'my-field-name,
     label: 'My added field',
     descriptionHTML: 'Optional description',
+
+    // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
+    // /!\ 'input-checkbox' could send "false" and "true" strings instead of boolean
     type: 'input-textarea',
     type: 'input-textarea',
-    default: ''
+
+    default: '',
+
+    // Optional, to hide a field depending on the current form state
+    // liveVideo is in the options object when the user is creating/updating a live
+    // videoToUpdate is in the options object when the user is updating a video
+    hidden: ({ formValues, videoToUpdate, liveVideo }) => {
+      return formValues.pluginData['other-field'] === 'toto'
+    },
+
+    // Optional, to display an error depending on the form state
+    error: ({ formValues, value }) => {
+      if (formValues['privacy'] !== 1 && formValues['privacy'] !== 2) return { error: false }
+      if (value === true) return { error: false }
+
+      return { error: true, text: 'Should be enabled' }
+    }
+  }
+
+  const videoFormOptions = {
+    // Optional, to choose to put your setting in a specific tab in video form
+    // type: 'main' | 'plugin-settings'
+    tab: 'main'
   }
 
   }
 
-  for (const type of [ 'upload', 'import-url', 'import-torrent', 'update' ]) {
-    registerVideoField(commonOptions, { type })
+  for (const type of [ 'upload', 'import-url', 'import-torrent', 'update', 'go-live' ]) {
+    registerVideoField(commonOptions, { type, ...videoFormOptions  })
   }
 }
 ```
   }
 }
 ```
@@ -627,12 +833,55 @@ async function register ({ registerSettingsScript }) {
   })
 }
 ```
   })
 }
 ```
+#### Plugin selector on HTML elements
+
+PeerTube provides some selectors (using `id` HTML attribute) on important blocks so plugins can easily change their style.
+
+For example `#plugin-selector-login-form` could be used to hide the login form.
+
+See the complete list on https://docs.joinpeertube.org/api-plugins
+
+#### HTML placeholder elements
+
+PeerTube provides some HTML id so plugins can easily insert their own element:
+
+```js
+async function register (...) {
+  const elem = document.createElement('div')
+  elem.className = 'hello-world-h4'
+  elem.innerHTML = '<h4>Hello everybody! This is an element next to the player</h4>'
+
+  document.getElementById('plugin-placeholder-player-next').appendChild(elem)
+}
+```
+
+See the complete list on https://docs.joinpeertube.org/api-plugins
+
+#### Add/remove left menu links
+
+Left menu links can be filtered (add/remove a section or add/remove links) using the `filter:left-menu.links.create.result` client hook.
+
+#### Create client page
+
+To create a client page, register a new client route:
+
+```js
+function register ({ registerClientRoute }) {
+  registerClientRoute({
+    route: 'my-super/route',
+    onMount: ({ rootEl }) => {
+      rootEl.innerHTML = 'hello'
+    }
+  })
+}
+```
 
 
 ### Publishing
 
 
 
 ### Publishing
 
-PeerTube plugins and themes should be published on [NPM](https://www.npmjs.com/) so that PeerTube indexes
-take into account your plugin (after ~ 1 day). An official PeerTube index is available on https://packages.joinpeertube.org/ (it's just a REST API, so don't expect a beautiful website).
+PeerTube plugins and themes should be published on [NPM](https://www.npmjs.com/) so that PeerTube indexes take into account your plugin (after ~ 1 day). An official plugin index is available on [packages.joinpeertube.org](https://packages.joinpeertube.org/api/v1/plugins), with no interface to present packages.
+
+> The official plugin index source code is available at https://framagit.org/framasoft/peertube/plugin-index
 
 ## Write a plugin/theme
 
 
 ## Write a plugin/theme
 
@@ -715,11 +964,65 @@ And if you don't need CSS or client script files, use an empty `array`:
 ### Write code
 
 Now you can register hooks or settings, write CSS and add static directories to your plugin or your theme :)
 ### Write code
 
 Now you can register hooks or settings, write CSS and add static directories to your plugin or your theme :)
+It's up to you to check the code you write will be compatible with the PeerTube NodeJS version, and will be supported by web browsers.
+
+**JavaScript**
 
 
-**Caution:** It's up to you to check the code you write will be compatible with the PeerTube NodeJS version,
-and will be supported by web browsers.
 If you want to write modern JavaScript, please use a transpiler like [Babel](https://babeljs.io/).
 
 If you want to write modern JavaScript, please use a transpiler like [Babel](https://babeljs.io/).
 
+**Typescript**
+
+The easiest way to use __Typescript__ for both front-end and backend code is to clone [peertube-plugin-quickstart-typescript](https://github.com/JohnXLivingston/peertube-plugin-quickstart-typescript/) (also available on [framagit](https://framagit.org/Livingston/peertube-plugin-quickstart-typescript/)) instead of `peertube-plugin-quickstart`.
+Please read carefully the [README file](https://github.com/JohnXLivingston/peertube-plugin-quickstart-typescript/blob/main/README.md), as there are some other differences with `peertube-plugin-quickstart` (using SCSS instead of CSS, linting rules, ...).
+
+If you don't want to use `peertube-plugin-quickstart-typescript`, you can also manually add a dev dependency to __Peertube__ types:
+
+```
+npm install --save-dev @peertube/peertube-types
+```
+
+This package exposes *server* definition files by default:
+```ts
+import { RegisterServerOptions } from '@peertube/peertube-types'
+
+export async function register ({ registerHook }: RegisterServerOptions) {
+  registerHook({
+    target: 'action:application.listening',
+    handler: () => displayHelloWorld()
+  })
+}
+```
+
+But it also exposes client types and various models used in __PeerTube__:
+```ts
+import { Video } from '@peertube/peertube-types';
+import { RegisterClientOptions } from '@peertube/peertube-types/client';
+
+function register({ registerHook, peertubeHelpers }: RegisterClientOptions) {
+  registerHook({
+    target: 'action:admin-plugin-settings.init',
+    handler: ({ npmName }: { npmName: string }) => {
+      if ('peertube-plugin-transcription' !== npmName) {
+        return;
+      }
+    },
+  });
+
+  registerHook({
+    target: 'action:video-watch.video.loaded',
+    handler: ({ video }: { video: Video }) => {
+      fetch(`${peertubeHelpers.getBaseRouterRoute()}/videos/${video.uuid}/captions`, {
+        method: 'PUT',
+        headers: peertubeHelpers.getAuthHeader(),
+      }).then((res) => res.json())
+        .then((data) => console.log('Hi %s.', data));
+    },
+  });
+}
+
+export { register };
+```
+
 ### Add translations
 
 If you want to translate strings of your plugin (like labels of your registered settings), create a file and add it to `package.json`:
 ### Add translations
 
 If you want to translate strings of your plugin (like labels of your registered settings), create a file and add it to `package.json`:
@@ -728,7 +1031,7 @@ If you want to translate strings of your plugin (like labels of your registered
 {
   ...,
   "translations": {
 {
   ...,
   "translations": {
-    "fr-FR": "./languages/fr.json",
+    "fr": "./languages/fr.json",
     "pt-BR": "./languages/pt-BR.json"
   },
   ...
     "pt-BR": "./languages/pt-BR.json"
   },
   ...
@@ -736,7 +1039,6 @@ If you want to translate strings of your plugin (like labels of your registered
 ```
 
 The key should be one of the locales defined in [i18n.ts](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/i18n/i18n.ts).
 ```
 
 The key should be one of the locales defined in [i18n.ts](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/i18n/i18n.ts).
-You **must** use the complete locales (`fr-FR` instead of `fr`).
 
 Translation files are just objects, with the english sentence as the key and the translation as the value.
 `fr.json` could contain for example:
 
 Translation files are just objects, with the english sentence as the key and the translation as the value.
 `fr.json` could contain for example:
@@ -774,13 +1076,16 @@ You built files are in the `dist/` directory. Check `package.json` to correctly
 
 ### Test your plugin/theme
 
 
 ### Test your plugin/theme
 
+PeerTube dev server (ran with `npm run dev` on `localhost:3000`) can't inject plugin CSS.
+It's the reason why we don't use the dev mode but build PeerTube instead.
+
 You'll need to have a local PeerTube instance:
  * Follow the [dev prerequisites](https://github.com/Chocobozzz/PeerTube/blob/develop/.github/CONTRIBUTING.md#prerequisites)
  (to clone the repository, install dependencies and prepare the database)
 You'll need to have a local PeerTube instance:
  * Follow the [dev prerequisites](https://github.com/Chocobozzz/PeerTube/blob/develop/.github/CONTRIBUTING.md#prerequisites)
  (to clone the repository, install dependencies and prepare the database)
- * Build PeerTube (`--light` to only build the english language):
+ * Build PeerTube:
 
 ```
 
 ```
-$ npm run build -- --light
+$ npm run build
 ```
 
  * Build the CLI:
 ```
 
  * Build the CLI:
@@ -792,7 +1097,7 @@ $ npm run setup:cli
  * Run PeerTube (you can access to your instance on http://localhost:9000):
 
 ```
  * Run PeerTube (you can access to your instance on http://localhost:9000):
 
 ```
-$ NODE_ENV=test npm start
+$ NODE_ENV=dev npm start
 ```
 
  * Register the instance via the CLI:
 ```
 
  * Register the instance via the CLI:
@@ -818,6 +1123,20 @@ $ npm publish
 Every time you want to publish another version of your plugin/theme, just update the `version` key from the `package.json`
 and republish it on NPM. Remember that the PeerTube index will take into account your new plugin/theme version after ~24 hours.
 
 Every time you want to publish another version of your plugin/theme, just update the `version` key from the `package.json`
 and republish it on NPM. Remember that the PeerTube index will take into account your new plugin/theme version after ~24 hours.
 
+> If you need to force your plugin update on a specific __PeerTube__ instance, you may update the latest available version manually:
+> ```sql
+> UPDATE "plugin" SET "latestVersion" = 'X.X.X' WHERE "plugin"."name" = 'plugin-shortname';
+> ```
+> You'll then be able to click the __Update plugin__ button on the plugin list.
+
+### Unpublish
+
+If for a particular reason you don't want to maintain your plugin/theme anymore
+you can deprecate it. The plugin index will automatically remove it preventing users to find/install it from the PeerTube admin interface:
+
+```bash
+$ npm deprecate peertube-plugin-xxx@"> 0.0.0" "explain here why you deprecate your plugin/theme"
+```
 
 ## Plugin & Theme hooks/helpers API
 
 
 ## Plugin & Theme hooks/helpers API