]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - support/doc/plugins/guide.md
Remove $ for bash commands
[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 - [Concepts](#concepts)
7 - [Hooks](#hooks)
8 - [Static files](#static-files)
9 - [CSS](#css)
10 - [Server API (only for plugins)](#server-api-only-for-plugins)
11 - [Settings](#settings)
12 - [Storage](#storage)
13 - [Update video constants](#update-video-constants)
14 - [Add custom routes](#add-custom-routes)
15 - [Add custom WebSocket handlers](#add-custom-websocket-handlers)
16 - [Add external auth methods](#add-external-auth-methods)
17 - [Add new transcoding profiles](#add-new-transcoding-profiles)
18 - [Server helpers](#server-helpers)
19 - [Federation](#federation)
20 - [Client API (themes & plugins)](#client-api-themes--plugins)
21 - [Get plugin static and router routes](#get-plugin-static-and-router-routes)
22 - [Notifier](#notifier)
23 - [Markdown Renderer](#markdown-renderer)
24 - [Auth header](#auth-header)
25 - [Custom Modal](#custom-modal)
26 - [Translate](#translate)
27 - [Get public settings](#get-public-settings)
28 - [Get server config](#get-server-config)
29 - [Add custom fields to video form](#add-custom-fields-to-video-form)
30 - [Register settings script](#register-settings-script)
31 - [Plugin selector on HTML elements](#plugin-selector-on-html-elements)
32 - [HTML placeholder elements](#html-placeholder-elements)
33 - [Add/remove left menu links](#addremove-left-menu-links)
34 - [Create client page](#create-client-page)
35 - [Publishing](#publishing)
36 - [Write a plugin/theme](#write-a-plugintheme)
37 - [Clone the quickstart repository](#clone-the-quickstart-repository)
38 - [Configure your repository](#configure-your-repository)
39 - [Update README](#update-readme)
40 - [Update package.json](#update-packagejson)
41 - [Write code](#write-code)
42 - [Add translations](#add-translations)
43 - [Build your plugin](#build-your-plugin)
44 - [Test your plugin/theme](#test-your-plugintheme)
45 - [Publish](#publish)
46 - [Unpublish](#unpublish)
47 - [Plugin & Theme hooks/helpers API](#plugin--theme-hookshelpers-api)
48 - [Tips](#tips)
49 - [Compatibility with PeerTube](#compatibility-with-peertube)
50 - [Spam/moderation plugin](#spammoderation-plugin)
51 - [Other plugin examples](#other-plugin-examples)
52
53 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
54
55 ## Concepts
56
57 Themes are exactly the same as plugins, except that:
58 * Their name starts with `peertube-theme-` instead of `peertube-plugin-`
59 * They cannot declare server code (so they cannot register server hooks or settings)
60 * CSS files are loaded by client only if the theme is chosen by the administrator or the user
61
62 ### Hooks
63
64 A plugin registers functions in JavaScript to execute when PeerTube (server and client) fires events. There are 3 types of hooks:
65 * `filter`: used to filter functions parameters or return values.
66 For example to replace words in video comments, or change the videos list behaviour
67 * `action`: used to do something after a certain trigger. For example to send a hook every time a video is published
68 * `static`: same than `action` but PeerTube waits their execution
69
70 On server side, these hooks are registered by the `library` file defined in `package.json`.
71
72 ```json
73 {
74 ...,
75 "library": "./main.js",
76 ...,
77 }
78 ```
79
80 And `main.js` defines a `register` function:
81
82 Example:
83
84 ```js
85 async function register ({
86 registerHook,
87
88 registerSetting,
89 settingsManager,
90
91 storageManager,
92
93 videoCategoryManager,
94 videoLicenceManager,
95 videoLanguageManager,
96
97 peertubeHelpers,
98
99 getRouter,
100
101 registerExternalAuth,
102 unregisterExternalAuth,
103 registerIdAndPassAuth,
104 unregisterIdAndPassAuth
105 }) {
106 registerHook({
107 target: 'action:application.listening',
108 handler: () => displayHelloWorld()
109 })
110 }
111 ```
112
113 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):
114
115 ```js
116 async function register ({
117 registerHook,
118 peertubeHelpers: { logger }
119 }) {
120 registerHook({
121 target: 'action:api.video.updated',
122 handler: ({ req, res }) => logger.debug('original request parameters', { params: req.params })
123 })
124 }
125 ```
126
127
128 On client side, these hooks are registered by the `clientScripts` files defined in `package.json`.
129 All client scripts have scopes so PeerTube client only loads scripts it needs:
130
131 ```json
132 {
133 ...,
134 "clientScripts": [
135 {
136 "script": "client/common-client-plugin.js",
137 "scopes": [ "common" ]
138 },
139 {
140 "script": "client/video-watch-client-plugin.js",
141 "scopes": [ "video-watch" ]
142 }
143 ],
144 ...
145 }
146 ```
147
148 And these scripts also define a `register` function:
149
150 ```js
151 function register ({ registerHook, peertubeHelpers }) {
152 registerHook({
153 target: 'action:application.init',
154 handler: () => onApplicationInit(peertubeHelpers)
155 })
156 }
157 ```
158
159 ### Static files
160
161 Plugins can declare static directories that PeerTube will serve (images for example)
162 from `/plugins/{plugin-name}/{plugin-version}/static/`
163 or `/themes/{theme-name}/{theme-version}/static/` routes.
164
165 ### CSS
166
167 Plugins can declare CSS files that PeerTube will automatically inject in the client.
168 If you need to override existing style, you can use the `#custom-css` selector:
169
170 ```
171 body#custom-css {
172 color: red;
173 }
174
175 #custom-css .header {
176 background-color: red;
177 }
178 ```
179
180 ### Server API (only for plugins)
181
182 #### Settings
183
184 Plugins can register settings, that PeerTube will inject in the administration interface.
185 The following fields will be automatically translated using the plugin translation files: `label`, `html`, `descriptionHTML`, `options.label`.
186 **These fields are injected in the plugin settings page as HTML, so pay attention to your translation files.**
187
188 Example:
189
190 ```js
191 function register (...) {
192 registerSetting({
193 name: 'admin-name',
194 label: 'Admin name',
195
196 type: 'input',
197 // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
198
199 // If type: 'select', give the select available options
200 options: [
201 { label: 'Label 1', value: 'value1' },
202 { label: 'Label 2', value: 'value2' }
203 ],
204
205 // If type: 'html', set the HTML that will be injected in the page
206 html: '<strong class="...">Hello</strong><br /><br />'
207
208 // Optional
209 descriptionHTML: 'The purpose of this field is...',
210
211 default: 'my super name',
212
213 // If the setting is not private, anyone can view its value (client code included)
214 // If the setting is private, only server-side hooks can access it
215 private: false
216 })
217
218 const adminName = await settingsManager.getSetting('admin-name')
219
220 const result = await settingsManager.getSettings([ 'admin-name', 'admin-password' ])
221 result['admin-name]
222
223 settingsManager.onSettingsChange(settings => {
224 settings['admin-name']
225 })
226 }
227 ```
228
229 #### Storage
230
231 Plugins can store/load JSON data, that PeerTube will store in its database (so don't put files in there).
232
233 Example:
234
235 ```js
236 function register ({
237 storageManager
238 }) {
239 const value = await storageManager.getData('mykey')
240 await storageManager.storeData('mykey', { subkey: 'value' })
241 }
242 ```
243
244 You can also store files in the plugin data directory (`/{plugins-directory}/data/{npm-plugin-name}`) **in PeerTube >= 3.2**.
245 This directory and its content won't be deleted when your plugin is uninstalled/upgraded.
246
247 ```js
248 function register ({
249 storageManager,
250 peertubeHelpers
251 }) {
252 const basePath = peertubeHelpers.plugin.getDataDirectoryPath()
253
254 fs.writeFile(path.join(basePath, 'filename.txt'), 'content of my file', function (err) {
255 ...
256 })
257 }
258 ```
259
260 #### Update video constants
261
262 You can add/delete video categories, licences or languages using the appropriate constant managers:
263
264 ```js
265 function register ({
266 videoLanguageManager,
267 videoCategoryManager,
268 videoLicenceManager,
269 videoPrivacyManager,
270 playlistPrivacyManager
271 }) {
272 videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
273 videoLanguageManager.deleteConstant('fr')
274
275 videoCategoryManager.addConstant(42, 'Best category')
276 videoCategoryManager.deleteConstant(1) // Music
277 videoCategoryManager.resetConstants() // Reset to initial categories
278 videoCategoryManager.getConstants() // Retrieve all category constants
279
280 videoLicenceManager.addConstant(42, 'Best licence')
281 videoLicenceManager.deleteConstant(7) // Public domain
282
283 videoPrivacyManager.deleteConstant(2) // Remove Unlisted video privacy
284 playlistPrivacyManager.deleteConstant(3) // Remove Private video playlist privacy
285 }
286 ```
287
288 #### Add custom routes
289
290 You can create custom routes using an [express Router](https://expressjs.com/en/4x/api.html#router) for your plugin:
291
292 ```js
293 function register ({
294 getRouter
295 }) {
296 const router = getRouter()
297 router.get('/ping', (req, res) => res.json({ message: 'pong' }))
298
299 // Users are automatically authenticated
300 router.get('/auth', async (res, res) => {
301 const user = await peertubeHelpers.user.getAuthUser(res)
302
303 const isAdmin = user.role === 0
304 const isModerator = user.role === 1
305 const isUser = user.role === 2
306
307 res.json({
308 username: user.username,
309 isAdmin,
310 isModerator,
311 isUser
312 })
313 })
314 }
315 ```
316
317 The `ping` route can be accessed using:
318 * `/plugins/:pluginName/:pluginVersion/router/ping`
319 * Or `/plugins/:pluginName/router/ping`
320
321
322 #### Add custom WebSocket handlers
323
324 **PeerTube >= 5.0**
325
326 You can create custom WebSocket servers (like [ws](https://github.com/websockets/ws) for example) using `registerWebSocketRoute`:
327
328 ```js
329 function register ({
330 registerWebSocketRoute,
331 peertubeHelpers
332 }) {
333 const wss = new WebSocketServer({ noServer: true })
334
335 wss.on('connection', function connection(ws) {
336 peertubeHelpers.logger.info('WebSocket connected!')
337
338 setInterval(() => {
339 ws.send('WebSocket message sent by server');
340 }, 1000)
341 })
342
343 registerWebSocketRoute({
344 route: '/my-websocket-route',
345
346 handler: (request, socket, head) => {
347 wss.handleUpgrade(request, socket, head, ws => {
348 wss.emit('connection', ws, request)
349 })
350 }
351 })
352 }
353 ```
354
355 The `my-websocket-route` route can be accessed using:
356 * `/plugins/:pluginName/:pluginVersion/ws/my-websocket-route`
357 * Or `/plugins/:pluginName/ws/my-websocket-route`
358
359 #### Add external auth methods
360
361 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):
362
363 ```js
364 function register (...) {
365
366 registerIdAndPassAuth({
367 authName: 'my-auth-method',
368
369 // PeerTube will try all id and pass plugins in the weight DESC order
370 // Exposing this value in the plugin settings could be interesting
371 getWeight: () => 60,
372
373 // Optional function called by PeerTube when the user clicked on the logout button
374 onLogout: user => {
375 console.log('User %s logged out.', user.username')
376 },
377
378 // Optional function called by PeerTube when the access token or refresh token are generated/refreshed
379 hookTokenValidity: ({ token, type }) => {
380 if (type === 'access') return { valid: true }
381 if (type === 'refresh') return { valid: false }
382 },
383
384 // Used by PeerTube when the user tries to authenticate
385 login: ({ id, password }) => {
386 if (id === 'user' && password === 'super password') {
387 return {
388 username: 'user'
389 email: 'user@example.com'
390 role: 2
391 displayName: 'User display name'
392 }
393 }
394
395 // Auth failed
396 return null
397 }
398 })
399
400 // Unregister this auth method
401 unregisterIdAndPassAuth('my-auth-method')
402 }
403 ```
404
405 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):
406
407 ```js
408 function register (...) {
409
410 // result contains the userAuthenticated auth method you can call to authenticate a user
411 const result = registerExternalAuth({
412 authName: 'my-auth-method',
413
414 // Will be displayed in a button next to the login form
415 authDisplayName: () => 'Auth method'
416
417 // If the user click on the auth button, PeerTube will forward the request in this function
418 onAuthRequest: (req, res) => {
419 res.redirect('https://external-auth.example.com/auth')
420 },
421
422 // Same than registerIdAndPassAuth option
423 // onLogout: ...
424
425 // Same than registerIdAndPassAuth option
426 // hookTokenValidity: ...
427 })
428
429 router.use('/external-auth-callback', (req, res) => {
430 // Forward the request to PeerTube
431 result.userAuthenticated({
432 req,
433 res,
434 username: 'user'
435 email: 'user@example.com'
436 role: 2
437 displayName: 'User display name',
438
439 // Custom admin flags (bypass video auto moderation etc.)
440 // https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/users/user-flag.model.ts
441 // PeerTube >= 5.1
442 adminFlags: 0,
443 // Quota in bytes
444 // PeerTube >= 5.1
445 videoQuota: 1024 * 1024 * 1024, // 1GB
446 // PeerTube >= 5.1
447 videoQuotaDaily: -1, // Unlimited
448
449 // Update the user profile if it already exists
450 // Default behaviour is no update
451 // Introduced in PeerTube >= 5.1
452 userUpdater: ({ fieldName, currentValue, newValue }) => {
453 // Always use new value except for videoQuotaDaily field
454 if (fieldName === 'videoQuotaDaily') return currentValue
455
456 return newValue
457 }
458 })
459 })
460
461 // Unregister this external auth method
462 unregisterExternalAuth('my-auth-method)
463 }
464 ```
465
466 #### Add new transcoding profiles
467
468 Adding transcoding profiles allow admins to change ffmpeg encoding parameters and/or encoders.
469 A transcoding profile has to be chosen by the admin of the instance using the admin configuration.
470
471 ```js
472 async function register ({
473 transcodingManager
474 }) {
475
476 // Adapt bitrate when using libx264 encoder
477 {
478 const builder = (options) => {
479 const { input, resolution, fps, streamNum } = options
480
481 const streamString = streamNum ? ':' + streamNum : ''
482
483 // You can also return a promise
484 // All these options are optional
485 return {
486 scaleFilter: {
487 // Used to define an alternative scale filter, needed by some encoders
488 // Default to 'scale'
489 name: 'scale_vaapi'
490 },
491 // Default to []
492 inputOptions: [],
493 // Default to []
494 outputOptions: [
495 // Use a custom bitrate
496 '-b' + streamString + ' 10K'
497 ]
498 }
499 }
500
501 const encoder = 'libx264'
502 const profileName = 'low-quality'
503
504 // Support this profile for VOD transcoding
505 transcodingManager.addVODProfile(encoder, profileName, builder)
506
507 // And/Or support this profile for live transcoding
508 transcodingManager.addLiveProfile(encoder, profileName, builder)
509 }
510
511 {
512 const builder = (options) => {
513 const { streamNum } = options
514
515 const streamString = streamNum ? ':' + streamNum : ''
516
517 // Always copy stream when PeerTube use libfdk_aac or aac encoders
518 return {
519 copy: true
520 }
521 }
522
523 const profileName = 'copy-audio'
524
525 for (const encoder of [ 'libfdk_aac', 'aac' ]) {
526 transcodingManager.addVODProfile(encoder, profileName, builder)
527 }
528 }
529 ```
530
531 PeerTube will try different encoders depending on their priority.
532 If the encoder is not available in the current transcoding profile or in ffmpeg, it tries the next one.
533 Plugins can change the order of these encoders and add their custom encoders:
534
535 ```js
536 async function register ({
537 transcodingManager
538 }) {
539
540 // Adapt bitrate when using libx264 encoder
541 {
542 const builder = () => {
543 return {
544 inputOptions: [],
545 outputOptions: []
546 }
547 }
548
549 // Support libopus and libvpx-vp9 encoders (these codecs could be incompatible with the player)
550 transcodingManager.addVODProfile('libopus', 'test-vod-profile', builder)
551
552 // Default priorities are ~100
553 // Lowest priority = 1
554 transcodingManager.addVODEncoderPriority('audio', 'libopus', 1000)
555
556 transcodingManager.addVODProfile('libvpx-vp9', 'test-vod-profile', builder)
557 transcodingManager.addVODEncoderPriority('video', 'libvpx-vp9', 1000)
558
559 transcodingManager.addLiveProfile('libopus', 'test-live-profile', builder)
560 transcodingManager.addLiveEncoderPriority('audio', 'libopus', 1000)
561 }
562 ```
563
564 During live transcode input options are applied once for each target resolution.
565 Plugins are responsible for detecting such situation and applying input options only once if necessary.
566
567 #### Server helpers
568
569 PeerTube provides your plugin some helpers. For example:
570
571 ```js
572 async function register ({
573 peertubeHelpers
574 }) {
575 // Block a server
576 {
577 const serverActor = await peertubeHelpers.server.getServerActor()
578
579 await peertubeHelpers.moderation.blockServer({ byAccountId: serverActor.Account.id, hostToBlock: '...' })
580 }
581
582 // Load a video
583 {
584 const video = await peertubeHelpers.videos.loadByUrl('...')
585 }
586 }
587 ```
588
589 See the [plugin API reference](https://docs.joinpeertube.org/api/plugins) to see the complete helpers list.
590
591 #### Federation
592
593 You can use some server hooks to federate plugin data to other PeerTube instances that may have installed your plugin.
594
595 For example to federate additional video metadata:
596
597 ```js
598 async function register ({ registerHook }) {
599
600 // Send plugin metadata to remote instances
601 // We also update the JSON LD context because we added a new field
602 {
603 registerHook({
604 target: 'filter:activity-pub.video.json-ld.build.result',
605 handler: async (jsonld, { video }) => {
606 return Object.assign(jsonld, { recordedAt: 'https://example.com/event' })
607 }
608 })
609
610 registerHook({
611 target: 'filter:activity-pub.activity.context.build.result',
612 handler: jsonld => {
613 return jsonld.concat([ { recordedAt: 'https://schema.org/recordedAt' } ])
614 }
615 })
616 }
617
618 // Save remote video metadata
619 {
620 for (const h of [ 'action:activity-pub.remote-video.created', 'action:activity-pub.remote-video.updated' ]) {
621 registerHook({
622 target: h,
623 handler: ({ video, videoAPObject }) => {
624 if (videoAPObject.recordedAt) {
625 // Save information about the video
626 }
627 }
628 })
629 }
630 }
631 ```
632
633
634 ### Client API (themes & plugins)
635
636 #### Get plugin static and router routes
637
638 To get your plugin static route:
639
640 ```js
641 function register (...) {
642 const baseStaticUrl = peertubeHelpers.getBaseStaticRoute()
643 const imageUrl = baseStaticUrl + '/images/chocobo.png'
644 }
645 ```
646
647 And to get your plugin router route, use `peertubeHelpers.getBaseRouterRoute()`:
648
649 ```js
650 function register (...) {
651 registerHook({
652 target: 'action:video-watch.video.loaded',
653 handler: ({ video }) => {
654 fetch(peertubeHelpers.getBaseRouterRoute() + '/my/plugin/api', {
655 method: 'GET',
656 headers: peertubeHelpers.getAuthHeader()
657 }).then(res => res.json())
658 .then(data => console.log('Hi %s.', data))
659 }
660 })
661 }
662 ```
663
664
665 #### Notifier
666
667 To notify the user with the PeerTube ToastModule:
668
669 ```js
670 function register (...) {
671 const { notifier } = peertubeHelpers
672 notifier.success('Success message content.')
673 notifier.error('Error message content.')
674 }
675 ```
676
677 #### Markdown Renderer
678
679 To render a formatted markdown text to HTML:
680
681 ```js
682 function register (...) {
683 const { markdownRenderer } = peertubeHelpers
684
685 await markdownRenderer.textMarkdownToHTML('**My Bold Text**')
686 // return <strong>My Bold Text</strong>
687
688 await markdownRenderer.enhancedMarkdownToHTML('![alt-img](http://.../my-image.jpg)')
689 // return <img alt=alt-img src=http://.../my-image.jpg />
690 }
691 ```
692
693 #### Auth header
694
695 **PeerTube >= 3.2**
696
697 To make your own HTTP requests using the current authenticated user, use an helper to automatically set appropriate headers:
698
699 ```js
700 function register (...) {
701 registerHook({
702 target: 'action:auth-user.information-loaded',
703 handler: ({ user }) => {
704
705 // Useless because we have the same info in the ({ user }) parameter
706 // It's just an example
707 fetch('/api/v1/users/me', {
708 method: 'GET',
709 headers: peertubeHelpers.getAuthHeader()
710 }).then(res => res.json())
711 .then(data => console.log('Hi %s.', data.username))
712 }
713 })
714 }
715 ```
716
717 #### Custom Modal
718
719 To show a custom modal:
720
721 ```js
722 function register (...) {
723 peertubeHelpers.showModal({
724 title: 'My custom modal title',
725 content: '<p>My custom modal content</p>',
726 // Optionals parameters :
727 // show close icon
728 close: true,
729 // show cancel button and call action() after hiding modal
730 cancel: { value: 'cancel', action: () => {} },
731 // show confirm button and call action() after hiding modal
732 confirm: { value: 'confirm', action: () => {} },
733 })
734 }
735 ```
736
737 #### Translate
738
739 You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file):
740
741 ```js
742 function register (...) {
743 peertubeHelpers.translate('User name')
744 .then(translation => console.log('Translated User name by ' + translation))
745 }
746 ```
747
748 #### Get public settings
749
750 To get your public plugin settings:
751
752 ```js
753 function register (...) {
754 peertubeHelpers.getSettings()
755 .then(s => {
756 if (!s || !s['site-id'] || !s['url']) {
757 console.error('Matomo settings are not set.')
758 return
759 }
760
761 // ...
762 })
763 }
764 ```
765
766 #### Get server config
767
768 ```js
769 function register (...) {
770 peertubeHelpers.getServerConfig()
771 .then(config => {
772 console.log('Fetched server config.', config)
773 })
774 }
775 ```
776
777 #### Add custom fields to video form
778
779 To add custom fields in the video form (in *Plugin settings* tab):
780
781 ```js
782 async function register ({ registerVideoField, peertubeHelpers }) {
783 const descriptionHTML = await peertubeHelpers.translate(descriptionSource)
784 const commonOptions = {
785 name: 'my-field-name,
786 label: 'My added field',
787 descriptionHTML: 'Optional description',
788
789 // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
790 // /!\ 'input-checkbox' could send "false" and "true" strings instead of boolean
791 type: 'input-textarea',
792
793 default: '',
794
795 // Optional, to hide a field depending on the current form state
796 // liveVideo is in the options object when the user is creating/updating a live
797 // videoToUpdate is in the options object when the user is updating a video
798 hidden: ({ formValues, videoToUpdate, liveVideo }) => {
799 return formValues.pluginData['other-field'] === 'toto'
800 },
801
802 // Optional, to display an error depending on the form state
803 error: ({ formValues, value }) => {
804 if (formValues['privacy'] !== 1 && formValues['privacy'] !== 2) return { error: false }
805 if (value === true) return { error: false }
806
807 return { error: true, text: 'Should be enabled' }
808 }
809 }
810
811 const videoFormOptions = {
812 // Optional, to choose to put your setting in a specific tab in video form
813 // type: 'main' | 'plugin-settings'
814 tab: 'main'
815 }
816
817 for (const type of [ 'upload', 'import-url', 'import-torrent', 'update', 'go-live' ]) {
818 registerVideoField(commonOptions, { type, ...videoFormOptions })
819 }
820 }
821 ```
822
823 PeerTube will send this field value in `body.pluginData['my-field-name']` and fetch it from `video.pluginData['my-field-name']`.
824
825 So for example, if you want to store an additional metadata for videos, register the following hooks in **server**:
826
827 ```js
828 async function register ({
829 registerHook,
830 storageManager
831 }) {
832 const fieldName = 'my-field-name'
833
834 // Store data associated to this video
835 registerHook({
836 target: 'action:api.video.updated',
837 handler: ({ video, body }) => {
838 if (!body.pluginData) return
839
840 const value = body.pluginData[fieldName]
841 if (!value) return
842
843 storageManager.storeData(fieldName + '-' + video.id, value)
844 }
845 })
846
847 // Add your custom value to the video, so the client autofill your field using the previously stored value
848 registerHook({
849 target: 'filter:api.video.get.result',
850 handler: async (video) => {
851 if (!video) return video
852 if (!video.pluginData) video.pluginData = {}
853
854 const result = await storageManager.getData(fieldName + '-' + video.id)
855 video.pluginData[fieldName] = result
856
857 return video
858 }
859 })
860 }
861 ```
862
863 #### Register settings script
864
865 To hide some fields in your settings plugin page depending on the form state:
866
867 ```js
868 async function register ({ registerSettingsScript }) {
869 registerSettingsScript({
870 isSettingHidden: options => {
871 if (options.setting.name === 'my-setting' && options.formValues['field45'] === '2') {
872 return true
873 }
874
875 return false
876 }
877 })
878 }
879 ```
880 #### Plugin selector on HTML elements
881
882 PeerTube provides some selectors (using `id` HTML attribute) on important blocks so plugins can easily change their style.
883
884 For example `#plugin-selector-login-form` could be used to hide the login form.
885
886 See the complete list on https://docs.joinpeertube.org/api/plugins
887
888 #### HTML placeholder elements
889
890 PeerTube provides some HTML id so plugins can easily insert their own element:
891
892 ```js
893 async function register (...) {
894 const elem = document.createElement('div')
895 elem.className = 'hello-world-h4'
896 elem.innerHTML = '<h4>Hello everybody! This is an element next to the player</h4>'
897
898 document.getElementById('plugin-placeholder-player-next').appendChild(elem)
899 }
900 ```
901
902 See the complete list on https://docs.joinpeertube.org/api/plugins
903
904 #### Add/remove left menu links
905
906 Left menu links can be filtered (add/remove a section or add/remove links) using the `filter:left-menu.links.create.result` client hook.
907
908 #### Create client page
909
910 To create a client page, register a new client route:
911
912 ```js
913 function register ({ registerClientRoute }) {
914 registerClientRoute({
915 route: 'my-super/route',
916 onMount: ({ rootEl }) => {
917 rootEl.innerHTML = 'hello'
918 }
919 })
920 }
921 ```
922
923
924 ### Publishing
925
926 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.
927
928 > The official plugin index source code is available at https://framagit.org/framasoft/peertube/plugin-index
929
930 ## Write a plugin/theme
931
932 Steps:
933 * Find a name for your plugin or your theme (must not have spaces, it can only contain lowercase letters and `-`)
934 * Add the appropriate prefix:
935 * If you develop a plugin, add `peertube-plugin-` prefix to your plugin name (for example: `peertube-plugin-mysupername`)
936 * If you develop a theme, add `peertube-theme-` prefix to your theme name (for example: `peertube-theme-mysupertheme`)
937 * Clone the quickstart repository
938 * Configure your repository
939 * Update `README.md`
940 * Update `package.json`
941 * Register hooks, add CSS and static files
942 * Test your plugin/theme with a local PeerTube installation
943 * Publish your plugin/theme on NPM
944
945 ### Clone the quickstart repository
946
947 If you develop a plugin, clone the `peertube-plugin-quickstart` repository:
948
949 ```
950 git clone https://framagit.org/framasoft/peertube/peertube-plugin-quickstart.git peertube-plugin-mysupername
951 ```
952
953 If you develop a theme, clone the `peertube-theme-quickstart` repository:
954
955 ```
956 git clone https://framagit.org/framasoft/peertube/peertube-theme-quickstart.git peertube-theme-mysupername
957 ```
958
959 ### Configure your repository
960
961 Set your repository URL:
962
963 ```
964 cd peertube-plugin-mysupername # or cd peertube-theme-mysupername
965 git remote set-url origin https://your-git-repo
966 ```
967
968 ### Update README
969
970 Update `README.md` file:
971
972 ```
973 $EDITOR README.md
974 ```
975
976 ### Update package.json
977
978 Update the `package.json` fields:
979 * `name` (should start with `peertube-plugin-` or `peertube-theme-`)
980 * `description`
981 * `homepage`
982 * `author`
983 * `bugs`
984 * `engine.peertube` (the PeerTube version compatibility, must be `>=x.y.z` and nothing else)
985
986 **Caution:** Don't update or remove other keys, or PeerTube will not be able to index/install your plugin.
987 If you don't need static directories, use an empty `object`:
988
989 ```json
990 {
991 ...,
992 "staticDirs": {},
993 ...
994 }
995 ```
996
997 And if you don't need CSS or client script files, use an empty `array`:
998
999 ```json
1000 {
1001 ...,
1002 "css": [],
1003 "clientScripts": [],
1004 ...
1005 }
1006 ```
1007
1008 ### Write code
1009
1010 Now you can register hooks or settings, write CSS and add static directories to your plugin or your theme :)
1011 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.
1012
1013 **JavaScript**
1014
1015 If you want to write modern JavaScript, please use a transpiler like [Babel](https://babeljs.io/).
1016
1017 **Typescript**
1018
1019 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`.
1020 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, ...).
1021
1022 If you don't want to use `peertube-plugin-quickstart-typescript`, you can also manually add a dev dependency to __Peertube__ types:
1023
1024 ```
1025 npm install --save-dev @peertube/peertube-types
1026 ```
1027
1028 This package exposes *server* definition files by default:
1029 ```ts
1030 import { RegisterServerOptions } from '@peertube/peertube-types'
1031
1032 export async function register ({ registerHook }: RegisterServerOptions) {
1033 registerHook({
1034 target: 'action:application.listening',
1035 handler: () => displayHelloWorld()
1036 })
1037 }
1038 ```
1039
1040 But it also exposes client types and various models used in __PeerTube__:
1041 ```ts
1042 import { Video } from '@peertube/peertube-types';
1043 import { RegisterClientOptions } from '@peertube/peertube-types/client';
1044
1045 function register({ registerHook, peertubeHelpers }: RegisterClientOptions) {
1046 registerHook({
1047 target: 'action:admin-plugin-settings.init',
1048 handler: ({ npmName }: { npmName: string }) => {
1049 if ('peertube-plugin-transcription' !== npmName) {
1050 return;
1051 }
1052 },
1053 });
1054
1055 registerHook({
1056 target: 'action:video-watch.video.loaded',
1057 handler: ({ video }: { video: Video }) => {
1058 fetch(`${peertubeHelpers.getBaseRouterRoute()}/videos/${video.uuid}/captions`, {
1059 method: 'PUT',
1060 headers: peertubeHelpers.getAuthHeader(),
1061 }).then((res) => res.json())
1062 .then((data) => console.log('Hi %s.', data));
1063 },
1064 });
1065 }
1066
1067 export { register };
1068 ```
1069
1070 ### Add translations
1071
1072 If you want to translate strings of your plugin (like labels of your registered settings), create a file and add it to `package.json`:
1073
1074 ```json
1075 {
1076 ...,
1077 "translations": {
1078 "fr": "./languages/fr.json",
1079 "pt-BR": "./languages/pt-BR.json"
1080 },
1081 ...
1082 }
1083 ```
1084
1085 The key should be one of the locales defined in [i18n.ts](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/i18n/i18n.ts).
1086
1087 Translation files are just objects, with the english sentence as the key and the translation as the value.
1088 `fr.json` could contain for example:
1089
1090 ```json
1091 {
1092 "Hello world": "Hello le monde"
1093 }
1094 ```
1095
1096 ### Build your plugin
1097
1098 If you added client scripts, you'll need to build them using webpack.
1099
1100 Install webpack:
1101
1102 ```
1103 npm install
1104 ```
1105
1106 Add/update your files in the `clientFiles` array of `webpack.config.js`:
1107
1108 ```
1109 $EDITOR ./webpack.config.js
1110 ```
1111
1112 Build your client files:
1113
1114 ```
1115 npm run build
1116 ```
1117
1118 You built files are in the `dist/` directory. Check `package.json` to correctly point to them.
1119
1120
1121 ### Test your plugin/theme
1122
1123 PeerTube dev server (ran with `npm run dev` on `localhost:3000`) can't inject plugin CSS.
1124 It's the reason why we don't use the dev mode but build PeerTube instead.
1125
1126 You'll need to have a local PeerTube instance:
1127 * Follow the [dev prerequisites](https://github.com/Chocobozzz/PeerTube/blob/develop/.github/CONTRIBUTING.md#prerequisites)
1128 (to clone the repository, install dependencies and prepare the database)
1129 * Build PeerTube:
1130
1131 ```
1132 npm run build
1133 ```
1134
1135 * Build the CLI:
1136
1137 ```
1138 npm run setup:cli
1139 ```
1140
1141 * Run PeerTube (you can access to your instance on `localhost:9000`):
1142
1143 ```
1144 NODE_ENV=dev npm start
1145 ```
1146
1147 * Register the instance via the CLI:
1148
1149 ```
1150 node ./dist/server/tools/peertube.js auth add -u 'http://localhost:9000' -U 'root' --password 'test'
1151 ```
1152
1153 Then, you can install or reinstall your local plugin/theme by running:
1154
1155 ```
1156 node ./dist/server/tools/peertube.js plugins install --path /your/absolute/plugin-or-theme/path
1157 ```
1158
1159 ### Publish
1160
1161 Go in your plugin/theme directory, and run:
1162
1163 ```
1164 npm publish
1165 ```
1166
1167 Every time you want to publish another version of your plugin/theme, just update the `version` key from the `package.json`
1168 and republish it on NPM. Remember that the PeerTube index will take into account your new plugin/theme version after ~24 hours.
1169
1170 > If you need to force your plugin update on a specific __PeerTube__ instance, you may update the latest available version manually:
1171 > ```sql
1172 > UPDATE "plugin" SET "latestVersion" = 'X.X.X' WHERE "plugin"."name" = 'plugin-shortname';
1173 > ```
1174 > You'll then be able to click the __Update plugin__ button on the plugin list.
1175
1176 ### Unpublish
1177
1178 If for a particular reason you don't want to maintain your plugin/theme anymore
1179 you can deprecate it. The plugin index will automatically remove it preventing users to find/install it from the PeerTube admin interface:
1180
1181 ```bash
1182 npm deprecate peertube-plugin-xxx@"> 0.0.0" "explain here why you deprecate your plugin/theme"
1183 ```
1184
1185 ## Plugin & Theme hooks/helpers API
1186
1187 See the dedicated documentation: https://docs.joinpeertube.org/api/plugins
1188
1189
1190 ## Tips
1191
1192 ### Compatibility with PeerTube
1193
1194 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`).
1195 So please:
1196 * Don't make assumptions and check every parameter you want to use. For example:
1197
1198 ```js
1199 registerHook({
1200 target: 'filter:api.video.get.result',
1201 handler: video => {
1202 // We check the parameter exists and the name field exists too, to avoid exceptions
1203 if (video && video.name) video.name += ' <3'
1204
1205 return video
1206 }
1207 })
1208 ```
1209 * 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)
1210 * Don't use PeerTube dependencies. Use your own :)
1211
1212 If your plugin is broken with a new PeerTube release, update your code and the `peertubeEngine` field of your `package.json` field.
1213 This way, older PeerTube versions will still use your old plugin, and new PeerTube versions will use your updated plugin.
1214
1215 ### Spam/moderation plugin
1216
1217 If you want to create an antispam/moderation plugin, you could use the following hooks:
1218 * `filter:api.video.upload.accept.result`: to accept or not local uploads
1219 * `filter:api.video-thread.create.accept.result`: to accept or not local thread
1220 * `filter:api.video-comment-reply.create.accept.result`: to accept or not local replies
1221 * `filter:api.video-threads.list.result`: to change/hide the text of threads
1222 * `filter:api.video-thread-comments.list.result`: to change/hide the text of replies
1223 * `filter:video.auto-blacklist.result`: to automatically blacklist local or remote videos
1224
1225 ### Other plugin examples
1226
1227 You can take a look to "official" PeerTube plugins if you want to take inspiration from them: https://framagit.org/framasoft/peertube/official-plugins