3 <!-- START doctoc generated TOC please keep comment here to allow auto update -->
4 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
7 - [Concepts](#concepts)
9 - [Static files](#static-files)
11 - [Server helpers (only for plugins)](#server-helpers-only-for-plugins)
12 - [Settings](#settings)
14 - [Update video constants](#update-video-constants)
15 - [Add custom routes](#add-custom-routes)
16 - [Add external auth methods](#add-external-auth-methods)
17 - [Client helpers (themes & plugins)](#client-helpers-themes--plugins)
18 - [Plugin static route](#plugin-static-route)
19 - [Notifier](#notifier)
20 - [Markdown Renderer](#markdown-renderer)
21 - [Custom Modal](#custom-modal)
22 - [Translate](#translate)
23 - [Get public settings](#get-public-settings)
24 - [Add custom fields to video form](#add-custom-fields-to-video-form)
25 - [Publishing](#publishing)
26 - [Write a plugin/theme](#write-a-plugintheme)
27 - [Clone the quickstart repository](#clone-the-quickstart-repository)
28 - [Configure your repository](#configure-your-repository)
29 - [Update README](#update-readme)
30 - [Update package.json](#update-packagejson)
31 - [Write code](#write-code)
32 - [Add translations](#add-translations)
33 - [Build your plugin](#build-your-plugin)
34 - [Test your plugin/theme](#test-your-plugintheme)
36 - [Plugin & Theme hooks/helpers API](#plugin--theme-hookshelpers-api)
38 - [Compatibility with PeerTube](#compatibility-with-peertube)
39 - [Spam/moderation plugin](#spammoderation-plugin)
40 - [Other plugin examples](#other-plugin-examples)
42 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
46 Themes are exactly the same as plugins, except that:
47 * Their name starts with `peertube-theme-` instead of `peertube-plugin-`
48 * They cannot declare server code (so they cannot register server hooks or settings)
49 * CSS files are loaded by client only if the theme is chosen by the administrator or the user
53 A plugin registers functions in JavaScript to execute when PeerTube (server and client) fires events. There are 3 types of hooks:
54 * `filter`: used to filter functions parameters or return values.
55 For example to replace words in video comments, or change the videos list behaviour
56 * `action`: used to do something after a certain trigger. For example to send a hook every time a video is published
57 * `static`: same than `action` but PeerTube waits their execution
59 On server side, these hooks are registered by the `library` file defined in `package.json`.
64 "library": "./main.js",
69 And `main.js` defines a `register` function:
74 async function register ({
91 unregisterExternalAuth,
92 registerIdAndPassAuth,
93 unregisterIdAndPassAuth
96 target: 'action:application.listening',
97 handler: () => displayHelloWorld()
103 On client side, these hooks are registered by the `clientScripts` files defined in `package.json`.
104 All client scripts have scopes so PeerTube client only loads scripts it needs:
111 "script": "client/common-client-plugin.js",
112 "scopes": [ "common" ]
115 "script": "client/video-watch-client-plugin.js",
116 "scopes": [ "video-watch" ]
123 And these scripts also define a `register` function:
126 function register ({ registerHook, peertubeHelpers }) {
128 target: 'action:application.init',
129 handler: () => onApplicationInit(peertubeHelpers)
136 Plugins can declare static directories that PeerTube will serve (images for example)
137 from `/plugins/{plugin-name}/{plugin-version}/static/`
138 or `/themes/{theme-name}/{theme-version}/static/` routes.
142 Plugins can declare CSS files that PeerTube will automatically inject in the client.
143 If you need to override existing style, you can use the `#custom-css` selector:
150 #custom-css .header {
151 background-color: red;
155 ### Server helpers (only for plugins)
159 Plugins can register settings, that PeerTube will inject in the administration interface.
168 // type: input | input-checkbox | input-password | input-textarea | markdown-text | markdown-enhanced
169 default: 'my super name'
172 const adminName = await settingsManager.getSetting('admin-name')
174 const result = await settingsManager.getSettings([ 'admin-name', 'admin-password' ])
177 settingsManager.onSettingsChange(settings => {
178 settings['admin-name])
184 Plugins can store/load JSON data, that PeerTube will store in its database (so don't put files in there).
189 const value = await storageManager.getData('mykey')
190 await storageManager.storeData('mykey', { subkey: 'value' })
193 #### Update video constants
195 You can add/delete video categories, licences or languages using the appropriate managers:
198 videoLanguageManager.addLanguage('al_bhed', 'Al Bhed')
199 videoLanguageManager.deleteLanguage('fr')
201 videoCategoryManager.addCategory(42, 'Best category')
202 videoCategoryManager.deleteCategory(1) // Music
204 videoLicenceManager.addLicence(42, 'Best licence')
205 videoLicenceManager.deleteLicence(7) // Public domain
207 videoPrivacyManager.deletePrivacy(2) // Remove Unlisted video privacy
208 playlistPrivacyManager.deletePlaylistPrivacy(3) // Remove Private video playlist privacy
211 #### Add custom routes
213 You can create custom routes using an [express Router](https://expressjs.com/en/4x/api.html#router) for your plugin:
216 const router = getRouter()
217 router.get('/ping', (req, res) => res.json({ message: 'pong' }))
220 The `ping` route can be accessed using:
221 * `/plugins/:pluginName/:pluginVersion/router/ping`
222 * Or `/plugins/:pluginName/router/ping`
225 #### Add external auth methods
227 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):
230 registerIdAndPassAuth({
231 authName: 'my-auth-method',
233 // PeerTube will try all id and pass plugins in the weight DESC order
234 // Exposing this value in the plugin settings could be interesting
237 // Optional function called by PeerTube when the user clicked on the logout button
239 console.log('User %s logged out.', user.username')
242 // Optional function called by PeerTube when the access token or refresh token are generated/refreshed
243 hookTokenValidity: ({ token, type }) => {
244 if (type === 'access') return { valid: true }
245 if (type === 'refresh') return { valid: false }
248 // Used by PeerTube when the user tries to authenticate
249 login: ({ id, password }) => {
250 if (id === 'user' && password === 'super password') {
253 email: 'user@example.com'
255 displayName: 'User display name'
264 // Unregister this auth method
265 unregisterIdAndPassAuth('my-auth-method')
268 You can also add an external auth method (like [OpenID](https://framagit.org/framasoft/peertube/official-plugins/-/tree/master/peertube-plugin-auth-openid-connect), [SAML2](https://framagit.org/framasoft/peertube/official-plugins/-/tree/master/peertube-plugin-auth-saml2) etc):
271 // result contains the userAuthenticated auth method you can call to authenticate a user
272 const result = registerExternalAuth({
273 authName: 'my-auth-method',
275 // Will be displayed in a button next to the login form
276 authDisplayName: () => 'Auth method'
278 // If the user click on the auth button, PeerTube will forward the request in this function
279 onAuthRequest: (req, res) => {
280 res.redirect('https://external-auth.example.com/auth')
283 // Same than registerIdAndPassAuth option
286 // Same than registerIdAndPassAuth option
287 // hookTokenValidity: ...
290 router.use('/external-auth-callback', (req, res) => {
291 // Forward the request to PeerTube
292 result.userAuthenticated({
296 email: 'user@example.com'
298 displayName: 'User display name'
302 // Unregister this external auth method
303 unregisterExternalAuth('my-auth-method)
306 ### Client helpers (themes & plugins)
308 #### Plugin static route
310 To get your plugin static route:
313 const baseStaticUrl = peertubeHelpers.getBaseStaticRoute()
314 const imageUrl = baseStaticUrl + '/images/chocobo.png'
319 To notify the user with the PeerTube ToastModule:
322 const { notifier } = peertubeHelpers
323 notifier.success('Success message content.')
324 notifier.error('Error message content.')
327 #### Markdown Renderer
329 To render a formatted markdown text to HTML:
332 const { markdownRenderer } = peertubeHelpers
334 await markdownRenderer.textMarkdownToHTML('**My Bold Text**')
335 // return <strong>My Bold Text</strong>
337 await markdownRenderer.enhancedMarkdownToHTML('![alt-img](http://.../my-image.jpg)')
338 // return <img alt=alt-img src=http://.../my-image.jpg />
343 To show a custom modal:
346 peertubeHelpers.showModal({
347 title: 'My custom modal title',
348 content: '<p>My custom modal content</p>',
349 // Optionals parameters :
352 // show cancel button and call action() after hiding modal
353 cancel: { value: 'cancel', action: () => {} },
354 // show confirm button and call action() after hiding modal
355 confirm: { value: 'confirm', action: () => {} },
361 You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file):
364 peertubeHelpers.translate('User name')
365 .then(translation => console.log('Translated User name by ' + translation))
368 #### Get public settings
370 To get your public plugin settings:
373 peertubeHelpers.getSettings()
375 if (!s || !s['site-id'] || !s['url']) {
376 console.error('Matomo settings are not set.')
384 #### Add custom fields to video form
386 To add custom fields in the video form (in *Plugin settings* tab):
389 async function register ({ registerVideoField, peertubeHelpers }) {
390 const descriptionHTML = await peertubeHelpers.translate(descriptionSource)
391 const commonOptions = {
392 name: 'my-field-name,
393 label: 'My added field',
394 descriptionHTML: 'Optional description',
395 type: 'input-textarea',
399 for (const type of [ 'upload', 'import-url', 'import-torrent', 'update' ]) {
400 registerVideoField(commonOptions, { type })
405 PeerTube will send this field value in `body.pluginData['my-field-name']` and fetch it from `video.pluginData['my-field-name']`.
407 So for example, if you want to store an additional metadata for videos, register the following hooks in **server**:
410 async function register ({
414 const fieldName = 'my-field-name'
416 // Store data associated to this video
418 target: 'action:api.video.updated',
419 handler: ({ video, body }) => {
420 if (!body.pluginData) return
422 const value = body.pluginData[fieldName]
425 storageManager.storeData(fieldName + '-' + video.id, value)
429 // Add your custom value to the video, so the client autofill your field using the previously stored value
431 target: 'filter:api.video.get.result',
432 handler: async (video) => {
433 if (!video) return video
434 if (!video.pluginData) video.pluginData = {}
436 const result = await storageManager.getData(fieldName + '-' + video.id)
437 video.pluginData[fieldName] = result
449 PeerTube plugins and themes should be published on [NPM](https://www.npmjs.com/) so that PeerTube indexes
450 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).
452 ## Write a plugin/theme
455 * Find a name for your plugin or your theme (must not have spaces, it can only contain lowercase letters and `-`)
456 * Add the appropriate prefix:
457 * If you develop a plugin, add `peertube-plugin-` prefix to your plugin name (for example: `peertube-plugin-mysupername`)
458 * If you develop a theme, add `peertube-theme-` prefix to your theme name (for example: `peertube-theme-mysupertheme`)
459 * Clone the quickstart repository
460 * Configure your repository
462 * Update `package.json`
463 * Register hooks, add CSS and static files
464 * Test your plugin/theme with a local PeerTube installation
465 * Publish your plugin/theme on NPM
467 ### Clone the quickstart repository
469 If you develop a plugin, clone the `peertube-plugin-quickstart` repository:
472 $ git clone https://framagit.org/framasoft/peertube/peertube-plugin-quickstart.git peertube-plugin-mysupername
475 If you develop a theme, clone the `peertube-theme-quickstart` repository:
478 $ git clone https://framagit.org/framasoft/peertube/peertube-theme-quickstart.git peertube-theme-mysupername
481 ### Configure your repository
483 Set your repository URL:
486 $ cd peertube-plugin-mysupername # or cd peertube-theme-mysupername
487 $ git remote set-url origin https://your-git-repo
492 Update `README.md` file:
498 ### Update package.json
500 Update the `package.json` fields:
501 * `name` (should start with `peertube-plugin-` or `peertube-theme-`)
506 * `engine.peertube` (the PeerTube version compatibility, must be `>=x.y.z` and nothing else)
508 **Caution:** Don't update or remove other keys, or PeerTube will not be able to index/install your plugin.
509 If you don't need static directories, use an empty `object`:
519 And if you don't need CSS or client script files, use an empty `array`:
532 Now you can register hooks or settings, write CSS and add static directories to your plugin or your theme :)
534 **Caution:** It's up to you to check the code you write will be compatible with the PeerTube NodeJS version,
535 and will be supported by web browsers.
536 If you want to write modern JavaScript, please use a transpiler like [Babel](https://babeljs.io/).
540 If you want to translate strings of your plugin (like labels of your registered settings), create a file and add it to `package.json`:
546 "fr-FR": "./languages/fr.json",
547 "pt-BR": "./languages/pt-BR.json"
553 The key should be one of the locales defined in [i18n.ts](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/i18n/i18n.ts).
554 You **must** use the complete locales (`fr-FR` instead of `fr`).
556 Translation files are just objects, with the english sentence as the key and the translation as the value.
557 `fr.json` could contain for example:
561 "Hello world": "Hello le monde"
565 ### Build your plugin
567 If you added client scripts, you'll need to build them using webpack.
575 Add/update your files in the `clientFiles` array of `webpack.config.js`:
578 $ $EDITOR ./webpack.config.js
581 Build your client files:
587 You built files are in the `dist/` directory. Check `package.json` to correctly point to them.
590 ### Test your plugin/theme
592 You'll need to have a local PeerTube instance:
593 * Follow the [dev prerequisites](https://github.com/Chocobozzz/PeerTube/blob/develop/.github/CONTRIBUTING.md#prerequisites)
594 (to clone the repository, install dependencies and prepare the database)
595 * Build PeerTube (`--light` to only build the english language):
598 $ npm run build -- --light
607 * Run PeerTube (you can access to your instance on http://localhost:9000):
610 $ NODE_ENV=test npm start
613 * Register the instance via the CLI:
616 $ node ./dist/server/tools/peertube.js auth add -u 'http://localhost:9000' -U 'root' --password 'test'
619 Then, you can install or reinstall your local plugin/theme by running:
622 $ node ./dist/server/tools/peertube.js plugins install --path /your/absolute/plugin-or-theme/path
627 Go in your plugin/theme directory, and run:
633 Every time you want to publish another version of your plugin/theme, just update the `version` key from the `package.json`
634 and republish it on NPM. Remember that the PeerTube index will take into account your new plugin/theme version after ~24 hours.
637 ## Plugin & Theme hooks/helpers API
639 See the dedicated documentation: https://docs.joinpeertube.org/api-plugins
644 ### Compatibility with PeerTube
646 Unfortunately, we don't have enough resources to provide hook compatibility between minor releases of PeerTube (for example between `1.2.x` and `1.3.x`).
648 * Don't make assumptions and check every parameter you want to use. For example:
652 target: 'filter:api.video.get.result',
654 // We check the parameter exists and the name field exists too, to avoid exceptions
655 if (video && video.name) video.name += ' <3'
661 * Don't try to require parent PeerTube modules, only use `peertubeHelpers`. If you need another helper or a specific hook, please [create an issue](https://github.com/Chocobozzz/PeerTube/issues/new/choose)
662 * Don't use PeerTube dependencies. Use your own :)
664 If your plugin is broken with a new PeerTube release, update your code and the `peertubeEngine` field of your `package.json` field.
665 This way, older PeerTube versions will still use your old plugin, and new PeerTube versions will use your updated plugin.
667 ### Spam/moderation plugin
669 If you want to create an antispam/moderation plugin, you could use the following hooks:
670 * `filter:api.video.upload.accept.result`: to accept or not local uploads
671 * `filter:api.video-thread.create.accept.result`: to accept or not local thread
672 * `filter:api.video-comment-reply.create.accept.result`: to accept or not local replies
673 * `filter:api.video-threads.list.result`: to change/hide the text of threads
674 * `filter:api.video-thread-comments.list.result`: to change/hide the text of replies
675 * `filter:video.auto-blacklist.result`: to automatically blacklist local or remote videos
677 ### Other plugin examples
679 You can take a look to "official" PeerTube plugins if you want to take inspiration from them: https://framagit.org/framasoft/peertube/official-plugins