]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - support/doc/plugins/guide.md
0d35820e583a947ae086c70752e92f0179f37e3b
[github/Chocobozzz/PeerTube.git] / support / doc / plugins / guide.md
1 # Plugins & Themes
2
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)
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)
35 - [Publish](#publish)
36 - [Plugin & Theme hooks/helpers API](#plugin--theme-hookshelpers-api)
37 - [Tips](#tips)
38 - [Compatibility with PeerTube](#compatibility-with-peertube)
39 - [Spam/moderation plugin](#spammoderation-plugin)
40 - [Other plugin examples](#other-plugin-examples)
41
42 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
43
44 ## Concepts
45
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
50
51 ### Hooks
52
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
58
59 On 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
69 And `main.js` defines a `register` function:
70
71 Example:
72
73 ```js
74 async function register ({
75 registerHook,
76
77 registerSetting,
78 settingsManager,
79
80 storageManager,
81
82 videoCategoryManager,
83 videoLicenceManager,
84 videoLanguageManager,
85
86 peertubeHelpers,
87
88 getRouter,
89
90 registerExternalAuth,
91 unregisterExternalAuth,
92 registerIdAndPassAuth,
93 unregisterIdAndPassAuth
94 }) {
95 registerHook({
96 target: 'action:application.listening',
97 handler: () => displayHelloWorld()
98 })
99 }
100 ```
101
102
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:
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
123 And these scripts also define a `register` function:
124
125 ```js
126 function register ({ registerHook, peertubeHelpers }) {
127 registerHook({
128 target: 'action:application.init',
129 handler: () => onApplicationInit(peertubeHelpers)
130 })
131 }
132 ```
133
134 ### Static files
135
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.
139
140 ### CSS
141
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:
144
145 ```
146 body#custom-css {
147 color: red;
148 }
149
150 #custom-css .header {
151 background-color: red;
152 }
153 ```
154
155 ### Server helpers (only for plugins)
156
157 #### Settings
158
159 Plugins can register settings, that PeerTube will inject in the administration interface.
160
161 Example:
162
163 ```js
164 registerSetting({
165 name: 'admin-name',
166 label: 'Admin name',
167 type: 'input',
168 // type: input | input-checkbox | input-password | input-textarea | markdown-text | markdown-enhanced
169 default: 'my super name'
170 })
171
172 const adminName = await settingsManager.getSetting('admin-name')
173
174 const result = await settingsManager.getSettings([ 'admin-name', 'admin-password' ])
175 result['admin-name]
176
177 settingsManager.onSettingsChange(settings => {
178 settings['admin-name])
179 })
180 ```
181
182 #### Storage
183
184 Plugins can store/load JSON data, that PeerTube will store in its database (so don't put files in there).
185
186 Example:
187
188 ```js
189 const value = await storageManager.getData('mykey')
190 await storageManager.storeData('mykey', { subkey: 'value' })
191 ```
192
193 #### Update video constants
194
195 You can add/delete video categories, licences or languages using the appropriate managers:
196
197 ```js
198 videoLanguageManager.addLanguage('al_bhed', 'Al Bhed')
199 videoLanguageManager.deleteLanguage('fr')
200
201 videoCategoryManager.addCategory(42, 'Best category')
202 videoCategoryManager.deleteCategory(1) // Music
203
204 videoLicenceManager.addLicence(42, 'Best licence')
205 videoLicenceManager.deleteLicence(7) // Public domain
206
207 videoPrivacyManager.deletePrivacy(2) // Remove Unlisted video privacy
208 playlistPrivacyManager.deletePlaylistPrivacy(3) // Remove Private video playlist privacy
209 ```
210
211 #### Add custom routes
212
213 You can create custom routes using an [express Router](https://expressjs.com/en/4x/api.html#router) for your plugin:
214
215 ```js
216 const router = getRouter()
217 router.get('/ping', (req, res) => res.json({ message: 'pong' }))
218 ```
219
220 The `ping` route can be accessed using:
221 * `/plugins/:pluginName/:pluginVersion/router/ping`
222 * Or `/plugins/:pluginName/router/ping`
223
224
225 #### Add external auth methods
226
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):
228
229 ```js
230 registerIdAndPassAuth({
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
265 unregisterIdAndPassAuth('my-auth-method')
266 ```
267
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):
269
270 ```js
271 // result contains the userAuthenticated auth method you can call to authenticate a user
272 const 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
290 router.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
303 unregisterExternalAuth('my-auth-method)
304 ```
305
306 ### Client helpers (themes & plugins)
307
308 #### Plugin static route
309
310 To get your plugin static route:
311
312 ```js
313 const baseStaticUrl = peertubeHelpers.getBaseStaticRoute()
314 const imageUrl = baseStaticUrl + '/images/chocobo.png'
315 ```
316
317 #### Notifier
318
319 To notify the user with the PeerTube ToastModule:
320
321 ```js
322 const { notifier } = peertubeHelpers
323 notifier.success('Success message content.')
324 notifier.error('Error message content.')
325 ```
326
327 #### Markdown Renderer
328
329 To render a formatted markdown text to HTML:
330
331 ```js
332 const { markdownRenderer } = peertubeHelpers
333
334 await markdownRenderer.textMarkdownToHTML('**My Bold Text**')
335 // return <strong>My Bold Text</strong>
336
337 await markdownRenderer.enhancedMarkdownToHTML('![alt-img](http://.../my-image.jpg)')
338 // return <img alt=alt-img src=http://.../my-image.jpg />
339 ```
340
341 #### Custom Modal
342
343 To 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
359 #### Translate
360
361 You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file):
362
363 ```js
364 peertubeHelpers.translate('User name')
365 .then(translation => console.log('Translated User name by ' + translation))
366 ```
367
368 #### Get public settings
369
370 To get your public plugin settings:
371
372 ```js
373 peertubeHelpers.getSettings()
374 .then(s => {
375 if (!s || !s['site-id'] || !s['url']) {
376 console.error('Matomo settings are not set.')
377 return
378 }
379
380 // ...
381 })
382 ```
383
384 #### Add custom fields to video form
385
386 To add custom fields in the video form (in *Plugin settings* tab):
387
388 ```js
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',
396 default: ''
397 }
398
399 for (const type of [ 'upload', 'import-url', 'import-torrent', 'update' ]) {
400 registerVideoField(commonOptions, { type })
401 }
402 }
403 ```
404
405 PeerTube will send this field value in `body.pluginData['my-field-name']` and fetch it from `video.pluginData['my-field-name']`.
406
407 So for example, if you want to store an additional metadata for videos, register the following hooks in **server**:
408
409 ```js
410 async 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
446
447 ### Publishing
448
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).
451
452 ## Write a plugin/theme
453
454 Steps:
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
469 If 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
475 If 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
483 Set 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
492 Update `README.md` file:
493
494 ```
495 $ $EDITOR README.md
496 ```
497
498 ### Update package.json
499
500 Update 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)
507
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`:
510
511 ```json
512 {
513 ...,
514 "staticDirs": {},
515 ...
516 }
517 ```
518
519 And if you don't need CSS or client script files, use an empty `array`:
520
521 ```json
522 {
523 ...,
524 "css": [],
525 "clientScripts": [],
526 ...
527 }
528 ```
529
530 ### Write code
531
532 Now you can register hooks or settings, write CSS and add static directories to your plugin or your theme :)
533
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/).
537
538 ### Add translations
539
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`:
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
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`).
555
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:
558
559 ```json
560 {
561 "Hello world": "Hello le monde"
562 }
563 ```
564
565 ### Build your plugin
566
567 If you added client scripts, you'll need to build them using webpack.
568
569 Install webpack:
570
571 ```
572 $ npm install
573 ```
574
575 Add/update your files in the `clientFiles` array of `webpack.config.js`:
576
577 ```
578 $ $EDITOR ./webpack.config.js
579 ```
580
581 Build your client files:
582
583 ```
584 $ npm run build
585 ```
586
587 You built files are in the `dist/` directory. Check `package.json` to correctly point to them.
588
589
590 ### Test your plugin/theme
591
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):
596
597 ```
598 $ npm run build -- --light
599 ```
600
601 * Build the CLI:
602
603 ```
604 $ npm run setup:cli
605 ```
606
607 * Run PeerTube (you can access to your instance on http://localhost:9000):
608
609 ```
610 $ NODE_ENV=test npm start
611 ```
612
613 * Register the instance via the CLI:
614
615 ```
616 $ node ./dist/server/tools/peertube.js auth add -u 'http://localhost:9000' -U 'root' --password 'test'
617 ```
618
619 Then, 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
627 Go in your plugin/theme directory, and run:
628
629 ```
630 $ npm publish
631 ```
632
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.
635
636
637 ## Plugin & Theme hooks/helpers API
638
639 See the dedicated documentation: https://docs.joinpeertube.org/api-plugins
640
641
642 ## Tips
643
644 ### Compatibility with PeerTube
645
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`).
647 So please:
648 * Don't make assumptions and check every parameter you want to use. For example:
649
650 ```js
651 registerHook({
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 ```
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 :)
663
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.
666
667 ### Spam/moderation plugin
668
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
676
677 ### Other plugin examples
678
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