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