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