]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - support/doc/plugins/guide.md
Merge branch 'release/5.1.0' 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
d8e9a42c
C
6- [Concepts](#concepts)
7 - [Hooks](#hooks)
8 - [Static files](#static-files)
9 - [CSS](#css)
d2466f0a 10 - [Server API (only for plugins)](#server-api-only-for-plugins)
d8e9a42c
C
11 - [Settings](#settings)
12 - [Storage](#storage)
7545a094 13 - [Update video constants](#update-video-constants)
5e2b2e27 14 - [Add custom routes](#add-custom-routes)
9d4c60dc 15 - [Add custom WebSocket handlers](#add-custom-websocket-handlers)
1a2820e6 16 - [Add external auth methods](#add-external-auth-methods)
7aca6b24 17 - [Add new transcoding profiles](#add-new-transcoding-profiles)
62bc0352 18 - [Server helpers](#server-helpers)
c3441b03 19 - [Federation](#federation)
d2466f0a 20 - [Client API (themes & plugins)](#client-api-themes--plugins)
3c33d714 21 - [Get plugin static and router routes](#get-plugin-static-and-router-routes)
b3af2601
C
22 - [Notifier](#notifier)
23 - [Markdown Renderer](#markdown-renderer)
096231d0 24 - [Auth header](#auth-header)
b3af2601 25 - [Custom Modal](#custom-modal)
7545a094
C
26 - [Translate](#translate)
27 - [Get public settings](#get-public-settings)
ec99e848 28 - [Get server config](#get-server-config)
8546fe87 29 - [Add custom fields to video form](#add-custom-fields-to-video-form)
d2466f0a 30 - [Register settings script](#register-settings-script)
8afade26 31 - [Plugin selector on HTML elements](#plugin-selector-on-html-elements)
62bc0352 32 - [HTML placeholder elements](#html-placeholder-elements)
8beea2d3 33 - [Add/remove left menu links](#addremove-left-menu-links)
3c33d714 34 - [Create client page](#create-client-page)
d8e9a42c
C
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)
7545a094 42 - [Add translations](#add-translations)
8546fe87 43 - [Build your plugin](#build-your-plugin)
d8e9a42c
C
44 - [Test your plugin/theme](#test-your-plugintheme)
45 - [Publish](#publish)
d61515e1 46 - [Unpublish](#unpublish)
7545a094 47- [Plugin & Theme hooks/helpers API](#plugin--theme-hookshelpers-api)
d8e9a42c
C
48- [Tips](#tips)
49 - [Compatibility with PeerTube](#compatibility-with-peertube)
50 - [Spam/moderation plugin](#spammoderation-plugin)
112be80e 51 - [Other plugin examples](#other-plugin-examples)
d8e9a42c
C
52
53<!-- END doctoc generated TOC please keep comment here to allow auto update -->
54
662e5d4f
C
55## Concepts
56
32d7f2b7 57Themes are exactly the same as plugins, except that:
662e5d4f
C
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
64A plugin registers functions in JavaScript to execute when PeerTube (server and client) fires events. There are 3 types of hooks:
5831dbcb 65 * `filter`: used to filter functions parameters or return values.
662e5d4f
C
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
662e5d4f
C
69
70On 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
7545a094
C
80And `main.js` defines a `register` function:
81
82Example:
83
84```js
85async function register ({
86 registerHook,
5831dbcb 87
7545a094
C
88 registerSetting,
89 settingsManager,
5831dbcb 90
7545a094 91 storageManager,
5831dbcb 92
7545a094
C
93 videoCategoryManager,
94 videoLicenceManager,
5e2b2e27 95 videoLanguageManager,
5831dbcb 96
5e2b2e27 97 peertubeHelpers,
5831dbcb
C
98
99 getRouter,
100
101 registerExternalAuth,
102 unregisterExternalAuth,
103 registerIdAndPassAuth,
104 unregisterIdAndPassAuth
7545a094
C
105}) {
106 registerHook({
107 target: 'action:application.listening',
108 handler: () => displayHelloWorld()
109 })
110}
111```
112
7226e90f 113Hooks 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
116async 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
662e5d4f
C
127
128On client side, these hooks are registered by the `clientScripts` files defined in `package.json`.
129All 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
7545a094
C
148And these scripts also define a `register` function:
149
150```js
151function register ({ registerHook, peertubeHelpers }) {
152 registerHook({
153 target: 'action:application.init',
154 handler: () => onApplicationInit(peertubeHelpers)
155 })
156}
157```
158
662e5d4f
C
159### Static files
160
5831dbcb
C
161Plugins can declare static directories that PeerTube will serve (images for example)
162from `/plugins/{plugin-name}/{plugin-version}/static/`
662e5d4f
C
163or `/themes/{theme-name}/{theme-version}/static/` routes.
164
165### CSS
166
167Plugins can declare CSS files that PeerTube will automatically inject in the client.
7545a094
C
168If you need to override existing style, you can use the `#custom-css` selector:
169
170```
171body#custom-css {
172 color: red;
173}
174
175#custom-css .header {
176 background-color: red;
177}
178```
662e5d4f 179
d2466f0a 180### Server API (only for plugins)
662e5d4f
C
181
182#### Settings
183
184Plugins can register settings, that PeerTube will inject in the administration interface.
d2466f0a
C
185The 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.**
662e5d4f
C
187
188Example:
189
190```js
d2466f0a
C
191function register (...) {
192 registerSetting({
193 name: 'admin-name',
194 label: 'Admin name',
248875d2 195
d2466f0a 196 type: 'input',
248875d2
C
197 // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
198
2bee9db5
C
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
248875d2
C
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
d2466f0a
C
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 => {
2bee9db5 224 settings['admin-name']
d2466f0a
C
225 })
226}
662e5d4f
C
227```
228
d8e9a42c 229#### Storage
662e5d4f
C
230
231Plugins can store/load JSON data, that PeerTube will store in its database (so don't put files in there).
232
233Example:
234
235```js
302eba0d
C
236function register ({
237 storageManager
238}) {
d2466f0a
C
239 const value = await storageManager.getData('mykey')
240 await storageManager.storeData('mykey', { subkey: 'value' })
241}
662e5d4f
C
242```
243
096231d0 244You can also store files in the plugin data directory (`/{plugins-directory}/data/{npm-plugin-name}`) **in PeerTube >= 3.2**.
302eba0d
C
245This directory and its content won't be deleted when your plugin is uninstalled/upgraded.
246
247```js
248function 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
7545a094
C
260#### Update video constants
261
dc3d9022 262You can add/delete video categories, licences or languages using the appropriate constant managers:
7545a094
C
263
264```js
2bee9db5
C
265function register ({
266 videoLanguageManager,
267 videoCategoryManager,
268 videoLicenceManager,
269 videoPrivacyManager,
270 playlistPrivacyManager
dc3d9022 271}) {
272 videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
273 videoLanguageManager.deleteConstant('fr')
7545a094 274
dc3d9022 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
7545a094 279
dc3d9022 280 videoLicenceManager.addConstant(42, 'Best licence')
281 videoLicenceManager.deleteConstant(7) // Public domain
b3af2601 282
dc3d9022 283 videoPrivacyManager.deleteConstant(2) // Remove Unlisted video privacy
284 playlistPrivacyManager.deleteConstant(3) // Remove Private video playlist privacy
d2466f0a 285}
7545a094
C
286```
287
5e2b2e27
C
288#### Add custom routes
289
290You can create custom routes using an [express Router](https://expressjs.com/en/4x/api.html#router) for your plugin:
291
292```js
302eba0d 293function register ({
4857f887 294 getRouter
302eba0d 295}) {
d2466f0a
C
296 const router = getRouter()
297 router.get('/ping', (req, res) => res.json({ message: 'pong' }))
302eba0d
C
298
299 // Users are automatically authenticated
b31d7262
C
300 router.get('/auth', async (res, res) => {
301 const user = await peertubeHelpers.user.getAuthUser(res)
302eba0d
C
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 })
d2466f0a 314}
5e2b2e27
C
315```
316
317The `ping` route can be accessed using:
318 * `/plugins/:pluginName/:pluginVersion/router/ping`
319 * Or `/plugins/:pluginName/router/ping`
320
321
9d4c60dc
C
322#### Add custom WebSocket handlers
323
fbe3fa54
JL
324**PeerTube >= 5.0**
325
9d4c60dc
C
326You can create custom WebSocket servers (like [ws](https://github.com/websockets/ws) for example) using `registerWebSocketRoute`:
327
328```js
329function 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
355The `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
5831dbcb
C
359#### Add external auth methods
360
361If 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
d2466f0a
C
364function 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 }
5831dbcb 393 }
5831dbcb 394
d2466f0a
C
395 // Auth failed
396 return null
397 }
398 })
5831dbcb 399
d2466f0a
C
400 // Unregister this auth method
401 unregisterIdAndPassAuth('my-auth-method')
402}
5831dbcb
C
403```
404
405You 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
d2466f0a
C
408function 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
60b880ac
C
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 }
d2466f0a 458 })
5831dbcb 459 })
5831dbcb 460
d2466f0a
C
461 // Unregister this external auth method
462 unregisterExternalAuth('my-auth-method)
463}
5831dbcb
C
464```
465
7aca6b24
C
466#### Add new transcoding profiles
467
468Adding transcoding profiles allow admins to change ffmpeg encoding parameters and/or encoders.
469A transcoding profile has to be chosen by the admin of the instance using the admin configuration.
470
471```js
472async 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
a60696ab 484 // All these options are optional
7aca6b24 485 return {
a60696ab
C
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 []
5fb7cfba 492 inputOptions: [],
a60696ab 493 // Default to []
7aca6b24
C
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
531PeerTube will try different encoders depending on their priority.
532If the encoder is not available in the current transcoding profile or in ffmpeg, it tries the next one.
533Plugins can change the order of these encoders and add their custom encoders:
534
535```js
536async function register ({
537 transcodingManager
538}) {
539
540 // Adapt bitrate when using libx264 encoder
541 {
542 const builder = () => {
543 return {
5fb7cfba 544 inputOptions: [],
7aca6b24
C
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
d5fc35c2
TLC
564During live transcode input options are applied once for each target resolution.
565Plugins are responsible for detecting such situation and applying input options only once if necessary.
566
62bc0352 567#### Server helpers
d2466f0a
C
568
569PeerTube provides your plugin some helpers. For example:
570
571```js
572async 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
c5c95361 589See the [plugin API reference](https://docs.joinpeertube.org/api/plugins) to see the complete helpers list.
d2466f0a 590
c3441b03
C
591#### Federation
592
593You can use some server hooks to federate plugin data to other PeerTube instances that may have installed your plugin.
594
595For example to federate additional video metadata:
596
597```js
598async 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
d2466f0a 634### Client API (themes & plugins)
7545a094 635
3c33d714 636#### Get plugin static and router routes
7545a094
C
637
638To get your plugin static route:
639
640```js
d2466f0a
C
641function register (...) {
642 const baseStaticUrl = peertubeHelpers.getBaseStaticRoute()
643 const imageUrl = baseStaticUrl + '/images/chocobo.png'
644}
7545a094
C
645```
646
3c33d714
C
647And to get your plugin router route, use `peertubeHelpers.getBaseRouterRoute()`:
648
649```js
650function 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
74c2dece
K
665#### Notifier
666
667To notify the user with the PeerTube ToastModule:
668
669```js
d2466f0a
C
670function register (...) {
671 const { notifier } = peertubeHelpers
672 notifier.success('Success message content.')
673 notifier.error('Error message content.')
674}
74c2dece
K
675```
676
8c7725dc
K
677#### Markdown Renderer
678
679To render a formatted markdown text to HTML:
680
681```js
d2466f0a
C
682function register (...) {
683 const { markdownRenderer } = peertubeHelpers
8c7725dc 684
d2466f0a
C
685 await markdownRenderer.textMarkdownToHTML('**My Bold Text**')
686 // return <strong>My Bold Text</strong>
8c7725dc 687
d2466f0a
C
688 await markdownRenderer.enhancedMarkdownToHTML('![alt-img](http://.../my-image.jpg)')
689 // return <img alt=alt-img src=http://.../my-image.jpg />
690}
8c7725dc
K
691```
692
096231d0
C
693#### Auth header
694
695**PeerTube >= 3.2**
696
697To make your own HTTP requests using the current authenticated user, use an helper to automatically set appropriate headers:
698
699```js
700function 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
437e8e06
K
717#### Custom Modal
718
719To show a custom modal:
720
721```js
d2466f0a
C
722function 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}
437e8e06
K
735```
736
7545a094
C
737#### Translate
738
739You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file):
740
741```js
d2466f0a
C
742function register (...) {
743 peertubeHelpers.translate('User name')
744 .then(translation => console.log('Translated User name by ' + translation))
745}
7545a094
C
746```
747
748#### Get public settings
749
750To get your public plugin settings:
751
752```js
d2466f0a
C
753function 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 }
5831dbcb 760
d2466f0a
C
761 // ...
762 })
763}
5831dbcb 764```
7545a094 765
ec99e848
C
766#### Get server config
767
768```js
d2466f0a
C
769function register (...) {
770 peertubeHelpers.getServerConfig()
771 .then(config => {
772 console.log('Fetched server config.', config)
773 })
774}
ec99e848
C
775```
776
8546fe87 777#### Add custom fields to video form
e08a26e2
C
778
779To add custom fields in the video form (in *Plugin settings* tab):
780
8546fe87 781```js
e08a26e2
C
782async 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',
03a65456
C
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
e08a26e2 791 type: 'input-textarea',
03a65456 792
0f319334 793 default: '',
3c065fe3 794
0f319334
C
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'
3c065fe3
C
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' }
0f319334 808 }
e08a26e2
C
809 }
810
3c065fe3
C
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
87e0b71d 817 for (const type of [ 'upload', 'import-url', 'import-torrent', 'update', 'go-live' ]) {
3c065fe3 818 registerVideoField(commonOptions, { type, ...videoFormOptions })
e08a26e2
C
819 }
820}
821```
822
823PeerTube will send this field value in `body.pluginData['my-field-name']` and fetch it from `video.pluginData['my-field-name']`.
824
825So for example, if you want to store an additional metadata for videos, register the following hooks in **server**:
826
8546fe87 827```js
e08a26e2
C
828async 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}
2498aaea 861```
d2466f0a
C
862
863#### Register settings script
864
865To hide some fields in your settings plugin page depending on the form state:
866
867```js
868async 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```
8afade26
C
880#### Plugin selector on HTML elements
881
882PeerTube provides some selectors (using `id` HTML attribute) on important blocks so plugins can easily change their style.
883
884For example `#plugin-selector-login-form` could be used to hide the login form.
885
c5c95361 886See the complete list on https://docs.joinpeertube.org/api/plugins
d2466f0a 887
62bc0352
C
888#### HTML placeholder elements
889
890PeerTube provides some HTML id so plugins can easily insert their own element:
891
b044cb18 892```js
62bc0352
C
893async 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
c5c95361 902See the complete list on https://docs.joinpeertube.org/api/plugins
d2466f0a 903
8beea2d3
C
904#### Add/remove left menu links
905
906Left menu links can be filtered (add/remove a section or add/remove links) using the `filter:left-menu.links.create.result` client hook.
907
3c33d714
C
908#### Create client page
909
910To create a client page, register a new client route:
911
912```js
913function register ({ registerClientRoute }) {
914 registerClientRoute({
915 route: 'my-super/route',
916 onMount: ({ rootEl }) => {
917 rootEl.innerHTML = 'hello'
918 }
919 })
920}
921```
922
8beea2d3 923
662e5d4f
C
924### Publishing
925
754c73f3 926PeerTube 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.
662e5d4f 927
4c0a6954 928> The official plugin index source code is available at https://framagit.org/framasoft/peertube/plugin-index
929
662e5d4f
C
930## Write a plugin/theme
931
932Steps:
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
947If 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
953If 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
961Set 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
970Update `README.md` file:
971
972```
973$ $EDITOR README.md
974```
975
976### Update package.json
977
978Update 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)
5831dbcb 985
662e5d4f 986**Caution:** Don't update or remove other keys, or PeerTube will not be able to index/install your plugin.
5831dbcb 987If you don't need static directories, use an empty `object`:
662e5d4f
C
988
989```json
990{
991 ...,
992 "staticDirs": {},
993 ...
994}
995```
996
9fa6ca16 997And if you don't need CSS or client script files, use an empty `array`:
662e5d4f
C
998
999```json
1000{
1001 ...,
1002 "css": [],
9fa6ca16 1003 "clientScripts": [],
662e5d4f
C
1004 ...
1005}
1006```
1007
1008### Write code
1009
1010Now you can register hooks or settings, write CSS and add static directories to your plugin or your theme :)
b969539c
C
1011It'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**
662e5d4f 1014
662e5d4f 1015If you want to write modern JavaScript, please use a transpiler like [Babel](https://babeljs.io/).
8b03e2ce 1016
b969539c
C
1017**Typescript**
1018
56162c6a
JL
1019The 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`.
1020Please 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
1022If you don't want to use `peertube-plugin-quickstart-typescript`, you can also manually add a dev dependency to __Peertube__ types:
8b03e2ce 1023
8b03e2ce 1024```
b8fa3e8c 1025npm install --save-dev @peertube/peertube-types
8b03e2ce 1026```
1027
1028This package exposes *server* definition files by default:
1029```ts
b969539c 1030import { RegisterServerOptions } from '@peertube/peertube-types'
8b03e2ce 1031
1032export async function register ({ registerHook }: RegisterServerOptions) {
1033 registerHook({
1034 target: 'action:application.listening',
1035 handler: () => displayHelloWorld()
1036 })
1037}
1038```
1039
1040But it also exposes client types and various models used in __PeerTube__:
1041```ts
b969539c
C
1042import { Video } from '@peertube/peertube-types';
1043import { RegisterClientOptions } from '@peertube/peertube-types/client';
8b03e2ce 1044
b8fa3e8c 1045function register({ registerHook, peertubeHelpers }: RegisterClientOptions) {
8b03e2ce 1046 registerHook({
b8fa3e8c 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(),
b969539c
C
1061 }).then((res) => res.json())
1062 .then((data) => console.log('Hi %s.', data));
b8fa3e8c 1063 },
1064 });
8b03e2ce 1065}
b8fa3e8c 1066
1067export { register };
8b03e2ce 1068```
662e5d4f 1069
7545a094
C
1070### Add translations
1071
1072If 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": {
67baf647 1078 "fr": "./languages/fr.json",
7545a094
C
1079 "pt-BR": "./languages/pt-BR.json"
1080 },
1081 ...
1082}
1083```
1084
1085The key should be one of the locales defined in [i18n.ts](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/i18n/i18n.ts).
7545a094 1086
112be80e
C
1087Translation 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
36578353
C
1096### Build your plugin
1097
1098If you added client scripts, you'll need to build them using webpack.
1099
1100Install webpack:
1101
1102```
1103$ npm install
1104```
1105
1106Add/update your files in the `clientFiles` array of `webpack.config.js`:
1107
1108```
1109$ $EDITOR ./webpack.config.js
1110```
1111
1112Build your client files:
1113
1114```
1115$ npm run build
1116```
1117
1118You built files are in the `dist/` directory. Check `package.json` to correctly point to them.
1119
1120
662e5d4f
C
1121### Test your plugin/theme
1122
09f33366
C
1123PeerTube dev server (ran with `npm run dev` on `localhost:3000`) can't inject plugin CSS.
1124It's the reason why we don't use the dev mode but build PeerTube instead.
1125
662e5d4f 1126You'll need to have a local PeerTube instance:
5831dbcb 1127 * Follow the [dev prerequisites](https://github.com/Chocobozzz/PeerTube/blob/develop/.github/CONTRIBUTING.md#prerequisites)
662e5d4f 1128 (to clone the repository, install dependencies and prepare the database)
09f33366 1129 * Build PeerTube:
662e5d4f
C
1130
1131```
09f33366 1132$ npm run build
9fa6ca16
C
1133```
1134
1135 * Build the CLI:
5831dbcb 1136
9fa6ca16
C
1137```
1138$ npm run setup:cli
662e5d4f 1139```
5831dbcb 1140
1ab94472 1141 * Run PeerTube (you can access to your instance on `localhost:9000`):
662e5d4f
C
1142
1143```
9452d4fd 1144$ NODE_ENV=dev npm start
662e5d4f
C
1145```
1146
5831dbcb 1147 * Register the instance via the CLI:
662e5d4f
C
1148
1149```
1150$ node ./dist/server/tools/peertube.js auth add -u 'http://localhost:9000' -U 'root' --password 'test'
1151```
1152
1153Then, 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
1161Go in your plugin/theme directory, and run:
1162
1163```
1164$ npm publish
1165```
1166
1167Every time you want to publish another version of your plugin/theme, just update the `version` key from the `package.json`
1168and republish it on NPM. Remember that the PeerTube index will take into account your new plugin/theme version after ~24 hours.
1169
4c0a6954 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
d61515e1
C
1176### Unpublish
1177
1178If for a particular reason you don't want to maintain your plugin/theme anymore
1179you 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```
d8e9a42c 1184
bfa1a32b
C
1185## Plugin & Theme hooks/helpers API
1186
c5c95361 1187See the dedicated documentation: https://docs.joinpeertube.org/api/plugins
bfa1a32b
C
1188
1189
d8e9a42c
C
1190## Tips
1191
1192### Compatibility with PeerTube
1193
1194Unfortunately, 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`).
1195So please:
1196 * Don't make assumptions and check every parameter you want to use. For example:
1197
1198```js
1199registerHook({
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```
a4879b53 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)
5831dbcb 1210 * Don't use PeerTube dependencies. Use your own :)
d8e9a42c 1211
51326912 1212If your plugin is broken with a new PeerTube release, update your code and the `peertubeEngine` field of your `package.json` field.
5831dbcb 1213This way, older PeerTube versions will still use your old plugin, and new PeerTube versions will use your updated plugin.
d8e9a42c
C
1214
1215### Spam/moderation plugin
1216
1217If 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
5831dbcb 1224
112be80e
C
1225### Other plugin examples
1226
1227You can take a look to "official" PeerTube plugins if you want to take inspiration from them: https://framagit.org/framasoft/peertube/official-plugins