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