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