]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - support/doc/plugins/guide.md
Merge remote-tracking branch 'weblate/develop' into develop
[github/Chocobozzz/PeerTube.git] / support / doc / plugins / guide.md
CommitLineData
662e5d4f
C
1# Plugins & Themes
2
d8e9a42c
C
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 -->
5
6
7- [Concepts](#concepts)
8 - [Hooks](#hooks)
9 - [Static files](#static-files)
10 - [CSS](#css)
11 - [Server helpers (only for plugins)](#server-helpers-only-for-plugins)
12 - [Settings](#settings)
13 - [Storage](#storage)
7545a094 14 - [Update video constants](#update-video-constants)
5e2b2e27 15 - [Add custom routes](#add-custom-routes)
1a2820e6 16 - [Add external auth methods](#add-external-auth-methods)
7545a094 17 - [Client helpers (themes & plugins)](#client-helpers-themes--plugins)
b3af2601
C
18 - [Plugin static route](#plugin-static-route)
19 - [Notifier](#notifier)
20 - [Markdown Renderer](#markdown-renderer)
21 - [Custom Modal](#custom-modal)
7545a094
C
22 - [Translate](#translate)
23 - [Get public settings](#get-public-settings)
8546fe87 24 - [Add custom fields to video form](#add-custom-fields-to-video-form)
d8e9a42c
C
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)
7545a094 32 - [Add translations](#add-translations)
8546fe87 33 - [Build your plugin](#build-your-plugin)
d8e9a42c
C
34 - [Test your plugin/theme](#test-your-plugintheme)
35 - [Publish](#publish)
7545a094 36- [Plugin & Theme hooks/helpers API](#plugin--theme-hookshelpers-api)
d8e9a42c
C
37- [Tips](#tips)
38 - [Compatibility with PeerTube](#compatibility-with-peertube)
39 - [Spam/moderation plugin](#spammoderation-plugin)
112be80e 40 - [Other plugin examples](#other-plugin-examples)
d8e9a42c
C
41
42<!-- END doctoc generated TOC please keep comment here to allow auto update -->
43
662e5d4f
C
44## Concepts
45
32d7f2b7 46Themes are exactly the same as plugins, except that:
662e5d4f
C
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
50
51### Hooks
52
53A plugin registers functions in JavaScript to execute when PeerTube (server and client) fires events. There are 3 types of hooks:
5831dbcb 54 * `filter`: used to filter functions parameters or return values.
662e5d4f
C
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
662e5d4f
C
58
59On server side, these hooks are registered by the `library` file defined in `package.json`.
60
61```json
62{
63 ...,
64 "library": "./main.js",
65 ...,
66}
67```
68
7545a094
C
69And `main.js` defines a `register` function:
70
71Example:
72
73```js
74async function register ({
75 registerHook,
5831dbcb 76
7545a094
C
77 registerSetting,
78 settingsManager,
5831dbcb 79
7545a094 80 storageManager,
5831dbcb 81
7545a094
C
82 videoCategoryManager,
83 videoLicenceManager,
5e2b2e27 84 videoLanguageManager,
5831dbcb 85
5e2b2e27 86 peertubeHelpers,
5831dbcb
C
87
88 getRouter,
89
90 registerExternalAuth,
91 unregisterExternalAuth,
92 registerIdAndPassAuth,
93 unregisterIdAndPassAuth
7545a094
C
94}) {
95 registerHook({
96 target: 'action:application.listening',
97 handler: () => displayHelloWorld()
98 })
99}
100```
101
662e5d4f
C
102
103On client side, these hooks are registered by the `clientScripts` files defined in `package.json`.
104All client scripts have scopes so PeerTube client only loads scripts it needs:
105
106```json
107{
108 ...,
109 "clientScripts": [
110 {
111 "script": "client/common-client-plugin.js",
112 "scopes": [ "common" ]
113 },
114 {
115 "script": "client/video-watch-client-plugin.js",
116 "scopes": [ "video-watch" ]
117 }
118 ],
119 ...
120}
121```
122
7545a094
C
123And these scripts also define a `register` function:
124
125```js
126function register ({ registerHook, peertubeHelpers }) {
127 registerHook({
128 target: 'action:application.init',
129 handler: () => onApplicationInit(peertubeHelpers)
130 })
131}
132```
133
662e5d4f
C
134### Static files
135
5831dbcb
C
136Plugins can declare static directories that PeerTube will serve (images for example)
137from `/plugins/{plugin-name}/{plugin-version}/static/`
662e5d4f
C
138or `/themes/{theme-name}/{theme-version}/static/` routes.
139
140### CSS
141
142Plugins can declare CSS files that PeerTube will automatically inject in the client.
7545a094
C
143If you need to override existing style, you can use the `#custom-css` selector:
144
145```
146body#custom-css {
147 color: red;
148}
149
150#custom-css .header {
151 background-color: red;
152}
153```
662e5d4f 154
9fa6ca16 155### Server helpers (only for plugins)
662e5d4f
C
156
157#### Settings
158
159Plugins can register settings, that PeerTube will inject in the administration interface.
160
161Example:
162
163```js
164registerSetting({
165 name: 'admin-name',
166 label: 'Admin name',
167 type: 'input',
8c7725dc 168 // type: input | input-checkbox | input-textarea | markdown-text | markdown-enhanced
662e5d4f
C
169 default: 'my super name'
170})
171
172const adminName = await settingsManager.getSetting('admin-name')
2db9c70f
C
173
174const result = await settingsManager.getSettings([ 'admin-name', 'admin-password' ])
175result['admin-name]
5831dbcb
C
176
177settingsManager.onSettingsChange(settings => {
178 settings['admin-name])
179})
662e5d4f
C
180```
181
d8e9a42c 182#### Storage
662e5d4f
C
183
184Plugins can store/load JSON data, that PeerTube will store in its database (so don't put files in there).
185
186Example:
187
188```js
189const value = await storageManager.getData('mykey')
9fa6ca16 190await storageManager.storeData('mykey', { subkey: 'value' })
662e5d4f
C
191```
192
7545a094
C
193#### Update video constants
194
195You can add/delete video categories, licences or languages using the appropriate managers:
196
197```js
198videoLanguageManager.addLanguage('al_bhed', 'Al Bhed')
199videoLanguageManager.deleteLanguage('fr')
200
201videoCategoryManager.addCategory(42, 'Best category')
202videoCategoryManager.deleteCategory(1) // Music
203
204videoLicenceManager.addLicence(42, 'Best licence')
205videoLicenceManager.deleteLicence(7) // Public domain
b3af2601
C
206
207videoPrivacyManager.deletePrivacy(2) // Remove Unlisted video privacy
208playlistPrivacyManager.deletePlaylistPrivacy(3) // Remove Private video playlist privacy
7545a094
C
209```
210
5e2b2e27
C
211#### Add custom routes
212
213You can create custom routes using an [express Router](https://expressjs.com/en/4x/api.html#router) for your plugin:
214
215```js
216const router = getRouter()
217router.get('/ping', (req, res) => res.json({ message: 'pong' }))
218```
219
220The `ping` route can be accessed using:
221 * `/plugins/:pluginName/:pluginVersion/router/ping`
222 * Or `/plugins/:pluginName/router/ping`
223
224
5831dbcb
C
225#### Add external auth methods
226
227If 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):
228
229```js
230registerIdAndPassAuth({
231 authName: 'my-auth-method',
232
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
235 getWeight: () => 60,
236
237 // Optional function called by PeerTube when the user clicked on the logout button
238 onLogout: user => {
239 console.log('User %s logged out.', user.username')
240 },
241
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 }
246 },
247
248 // Used by PeerTube when the user tries to authenticate
249 login: ({ id, password }) => {
250 if (id === 'user' && password === 'super password') {
251 return {
252 username: 'user'
253 email: 'user@example.com'
254 role: 2
255 displayName: 'User display name'
256 }
257 }
258
259 // Auth failed
260 return null
261 }
262})
263
264// Unregister this auth method
265unregisterIdAndPassAuth('my-auth-method')
266```
267
268You 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):
269
270```js
271// result contains the userAuthenticated auth method you can call to authenticate a user
272const result = registerExternalAuth({
273 authName: 'my-auth-method',
274
275 // Will be displayed in a button next to the login form
276 authDisplayName: () => 'Auth method'
277
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')
281 },
282
283 // Same than registerIdAndPassAuth option
284 // onLogout: ...
285
286 // Same than registerIdAndPassAuth option
287 // hookTokenValidity: ...
288})
289
290router.use('/external-auth-callback', (req, res) => {
291 // Forward the request to PeerTube
292 result.userAuthenticated({
293 req,
294 res,
295 username: 'user'
296 email: 'user@example.com'
297 role: 2
298 displayName: 'User display name'
299 })
300})
301
302// Unregister this external auth method
303unregisterExternalAuth('my-auth-method)
304```
305
7545a094
C
306### Client helpers (themes & plugins)
307
74c2dece 308#### Plugin static route
7545a094
C
309
310To get your plugin static route:
311
312```js
313const baseStaticUrl = peertubeHelpers.getBaseStaticRoute()
314const imageUrl = baseStaticUrl + '/images/chocobo.png'
315```
316
74c2dece
K
317#### Notifier
318
319To notify the user with the PeerTube ToastModule:
320
321```js
322const { notifier } = peertubeHelpers
323notifier.success('Success message content.')
324notifier.error('Error message content.')
325```
326
8c7725dc
K
327#### Markdown Renderer
328
329To render a formatted markdown text to HTML:
330
331```js
332const { markdownRenderer } = peertubeHelpers
333
334await markdownRenderer.textMarkdownToHTML('**My Bold Text**')
335// return <strong>My Bold Text</strong>
336
337await markdownRenderer.enhancedMarkdownToHTML('![alt-img](http://.../my-image.jpg)')
338// return <img alt=alt-img src=http://.../my-image.jpg />
339```
340
437e8e06
K
341#### Custom Modal
342
343To show a custom modal:
344
345```js
346 peertubeHelpers.showModal({
347 title: 'My custom modal title',
348 content: '<p>My custom modal content</p>',
349 // Optionals parameters :
350 // show close icon
351 close: true,
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: () => {} },
356 })
357```
358
7545a094
C
359#### Translate
360
361You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file):
362
363```js
364peertubeHelpers.translate('User name')
365 .then(translation => console.log('Translated User name by ' + translation))
366```
367
368#### Get public settings
369
370To get your public plugin settings:
371
372```js
373peertubeHelpers.getSettings()
374 .then(s => {
375 if (!s || !s['site-id'] || !s['url']) {
376 console.error('Matomo settings are not set.')
377 return
378 }
5831dbcb 379
7545a094
C
380 // ...
381 })
5831dbcb 382```
7545a094 383
8546fe87 384#### Add custom fields to video form
e08a26e2
C
385
386To add custom fields in the video form (in *Plugin settings* tab):
387
8546fe87 388```js
e08a26e2
C
389async 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',
396 default: ''
397 }
398
399 for (const type of [ 'upload', 'import-url', 'import-torrent', 'update' ]) {
400 registerVideoField(commonOptions, { type })
401 }
402}
403```
404
405PeerTube will send this field value in `body.pluginData['my-field-name']` and fetch it from `video.pluginData['my-field-name']`.
406
407So for example, if you want to store an additional metadata for videos, register the following hooks in **server**:
408
8546fe87 409```js
e08a26e2
C
410async function register ({
411 registerHook,
412 storageManager
413}) {
414 const fieldName = 'my-field-name'
415
416 // Store data associated to this video
417 registerHook({
418 target: 'action:api.video.updated',
419 handler: ({ video, body }) => {
420 if (!body.pluginData) return
421
422 const value = body.pluginData[fieldName]
423 if (!value) return
424
425 storageManager.storeData(fieldName + '-' + video.id, value)
426 }
427 })
428
429 // Add your custom value to the video, so the client autofill your field using the previously stored value
430 registerHook({
431 target: 'filter:api.video.get.result',
432 handler: async (video) => {
433 if (!video) return video
434 if (!video.pluginData) video.pluginData = {}
435
436 const result = await storageManager.getData(fieldName + '-' + video.id)
437 video.pluginData[fieldName] = result
438
439 return video
440 }
441 })
442}
443
444```
445
7545a094 446
662e5d4f
C
447### Publishing
448
449PeerTube plugins and themes should be published on [NPM](https://www.npmjs.com/) so that PeerTube indexes
450take 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).
451
452## Write a plugin/theme
453
454Steps:
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
461 * Update `README.md`
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
466
467### Clone the quickstart repository
468
469If you develop a plugin, clone the `peertube-plugin-quickstart` repository:
470
471```
472$ git clone https://framagit.org/framasoft/peertube/peertube-plugin-quickstart.git peertube-plugin-mysupername
473```
474
475If you develop a theme, clone the `peertube-theme-quickstart` repository:
476
477```
478$ git clone https://framagit.org/framasoft/peertube/peertube-theme-quickstart.git peertube-theme-mysupername
479```
480
481### Configure your repository
482
483Set your repository URL:
484
485```
486$ cd peertube-plugin-mysupername # or cd peertube-theme-mysupername
487$ git remote set-url origin https://your-git-repo
488```
489
490### Update README
491
492Update `README.md` file:
493
494```
495$ $EDITOR README.md
496```
497
498### Update package.json
499
500Update the `package.json` fields:
501 * `name` (should start with `peertube-plugin-` or `peertube-theme-`)
502 * `description`
503 * `homepage`
504 * `author`
505 * `bugs`
506 * `engine.peertube` (the PeerTube version compatibility, must be `>=x.y.z` and nothing else)
5831dbcb 507
662e5d4f 508**Caution:** Don't update or remove other keys, or PeerTube will not be able to index/install your plugin.
5831dbcb 509If you don't need static directories, use an empty `object`:
662e5d4f
C
510
511```json
512{
513 ...,
514 "staticDirs": {},
515 ...
516}
517```
518
9fa6ca16 519And if you don't need CSS or client script files, use an empty `array`:
662e5d4f
C
520
521```json
522{
523 ...,
524 "css": [],
9fa6ca16 525 "clientScripts": [],
662e5d4f
C
526 ...
527}
528```
529
530### Write code
531
532Now you can register hooks or settings, write CSS and add static directories to your plugin or your theme :)
533
5831dbcb 534**Caution:** It's up to you to check the code you write will be compatible with the PeerTube NodeJS version,
662e5d4f
C
535and will be supported by web browsers.
536If you want to write modern JavaScript, please use a transpiler like [Babel](https://babeljs.io/).
537
7545a094
C
538### Add translations
539
540If you want to translate strings of your plugin (like labels of your registered settings), create a file and add it to `package.json`:
541
542```json
543{
544 ...,
545 "translations": {
546 "fr-FR": "./languages/fr.json",
547 "pt-BR": "./languages/pt-BR.json"
548 },
549 ...
550}
551```
552
553The key should be one of the locales defined in [i18n.ts](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/i18n/i18n.ts).
554You **must** use the complete locales (`fr-FR` instead of `fr`).
555
112be80e
C
556Translation files are just objects, with the english sentence as the key and the translation as the value.
557`fr.json` could contain for example:
558
559```json
560{
561 "Hello world": "Hello le monde"
562}
563```
564
36578353
C
565### Build your plugin
566
567If you added client scripts, you'll need to build them using webpack.
568
569Install webpack:
570
571```
572$ npm install
573```
574
575Add/update your files in the `clientFiles` array of `webpack.config.js`:
576
577```
578$ $EDITOR ./webpack.config.js
579```
580
581Build your client files:
582
583```
584$ npm run build
585```
586
587You built files are in the `dist/` directory. Check `package.json` to correctly point to them.
588
589
662e5d4f
C
590### Test your plugin/theme
591
592You'll need to have a local PeerTube instance:
5831dbcb 593 * Follow the [dev prerequisites](https://github.com/Chocobozzz/PeerTube/blob/develop/.github/CONTRIBUTING.md#prerequisites)
662e5d4f 594 (to clone the repository, install dependencies and prepare the database)
5831dbcb 595 * Build PeerTube (`--light` to only build the english language):
662e5d4f
C
596
597```
598$ npm run build -- --light
9fa6ca16
C
599```
600
601 * Build the CLI:
5831dbcb 602
9fa6ca16
C
603```
604$ npm run setup:cli
662e5d4f 605```
5831dbcb
C
606
607 * Run PeerTube (you can access to your instance on http://localhost:9000):
662e5d4f
C
608
609```
610$ NODE_ENV=test npm start
611```
612
5831dbcb 613 * Register the instance via the CLI:
662e5d4f
C
614
615```
616$ node ./dist/server/tools/peertube.js auth add -u 'http://localhost:9000' -U 'root' --password 'test'
617```
618
619Then, you can install or reinstall your local plugin/theme by running:
620
621```
622$ node ./dist/server/tools/peertube.js plugins install --path /your/absolute/plugin-or-theme/path
623```
624
625### Publish
626
627Go in your plugin/theme directory, and run:
628
629```
630$ npm publish
631```
632
633Every time you want to publish another version of your plugin/theme, just update the `version` key from the `package.json`
634and republish it on NPM. Remember that the PeerTube index will take into account your new plugin/theme version after ~24 hours.
635
d8e9a42c 636
bfa1a32b
C
637## Plugin & Theme hooks/helpers API
638
195474f9 639See the dedicated documentation: https://docs.joinpeertube.org/#/api-plugins
bfa1a32b
C
640
641
d8e9a42c
C
642## Tips
643
644### Compatibility with PeerTube
645
646Unfortunately, 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`).
647So please:
648 * Don't make assumptions and check every parameter you want to use. For example:
649
650```js
651registerHook({
652 target: 'filter:api.video.get.result',
653 handler: video => {
654 // We check the parameter exists and the name field exists too, to avoid exceptions
655 if (video && video.name) video.name += ' <3'
656
657 return video
658 }
659})
660```
51326912 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)
5831dbcb 662 * Don't use PeerTube dependencies. Use your own :)
d8e9a42c 663
51326912 664If your plugin is broken with a new PeerTube release, update your code and the `peertubeEngine` field of your `package.json` field.
5831dbcb 665This way, older PeerTube versions will still use your old plugin, and new PeerTube versions will use your updated plugin.
d8e9a42c
C
666
667### Spam/moderation plugin
668
669If 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
5831dbcb 676
112be80e
C
677### Other plugin examples
678
679You can take a look to "official" PeerTube plugins if you want to take inspiration from them: https://framagit.org/framasoft/peertube/official-plugins