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 -->
6 - [Concepts](#concepts)
8 - [Static files](#static-files)
10 - [Server API (only for plugins)](#server-api-only-for-plugins)
11 - [Settings](#settings)
13 - [Update video constants](#update-video-constants)
14 - [Add custom routes](#add-custom-routes)
15 - [Add custom WebSocket handlers](#add-custom-websocket-handlers)
16 - [Add external auth methods](#add-external-auth-methods)
17 - [Add new transcoding profiles](#add-new-transcoding-profiles)
18 - [Server helpers](#server-helpers)
19 - [Federation](#federation)
20 - [Client API (themes & plugins)](#client-api-themes--plugins)
21 - [Get plugin static and router routes](#get-plugin-static-and-router-routes)
22 - [Notifier](#notifier)
23 - [Markdown Renderer](#markdown-renderer)
24 - [Auth header](#auth-header)
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 - [Plugin selector on HTML elements](#plugin-selector-on-html-elements)
32 - [HTML placeholder elements](#html-placeholder-elements)
33 - [Add/remove left menu links](#addremove-left-menu-links)
34 - [Create client page](#create-client-page)
35 - [Publishing](#publishing)
36 - [Write a plugin/theme](#write-a-plugintheme)
37 - [Clone the quickstart repository](#clone-the-quickstart-repository)
38 - [Configure your repository](#configure-your-repository)
39 - [Update README](#update-readme)
40 - [Update package.json](#update-packagejson)
41 - [Write code](#write-code)
42 - [Add translations](#add-translations)
43 - [Build your plugin](#build-your-plugin)
44 - [Test your plugin/theme](#test-your-plugintheme)
46 - [Unpublish](#unpublish)
47 - [Plugin & Theme hooks/helpers API](#plugin--theme-hookshelpers-api)
49 - [Compatibility with PeerTube](#compatibility-with-peertube)
50 - [Spam/moderation plugin](#spammoderation-plugin)
51 - [Other plugin examples](#other-plugin-examples)
53 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
57 Themes are exactly the same as plugins, except that:
58 * Their name starts with `peertube-theme-` instead of `peertube-plugin-`
59 * They cannot declare server code (so they cannot register server hooks or settings)
60 * CSS files are loaded by client only if the theme is chosen by the administrator or the user
64 A plugin registers functions in JavaScript to execute when PeerTube (server and client) fires events. There are 3 types of hooks:
65 * `filter`: used to filter functions parameters or return values.
66 For example to replace words in video comments, or change the videos list behaviour
67 * `action`: used to do something after a certain trigger. For example to send a hook every time a video is published
68 * `static`: same than `action` but PeerTube waits their execution
70 On server side, these hooks are registered by the `library` file defined in `package.json`.
75 "library": "./main.js",
80 And `main.js` defines a `register` function:
85 async function register ({
101 registerExternalAuth,
102 unregisterExternalAuth,
103 registerIdAndPassAuth,
104 unregisterIdAndPassAuth
107 target: 'action:application.listening',
108 handler: () => displayHelloWorld()
113 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):
116 async function register ({
118 peertubeHelpers: { logger }
121 target: 'action:api.video.updated',
122 handler: ({ req, res }) => logger.debug('original request parameters', { params: req.params })
128 On client side, these hooks are registered by the `clientScripts` files defined in `package.json`.
129 All client scripts have scopes so PeerTube client only loads scripts it needs:
136 "script": "client/common-client-plugin.js",
137 "scopes": [ "common" ]
140 "script": "client/video-watch-client-plugin.js",
141 "scopes": [ "video-watch" ]
148 And these scripts also define a `register` function:
151 function register ({ registerHook, peertubeHelpers }) {
153 target: 'action:application.init',
154 handler: () => onApplicationInit(peertubeHelpers)
161 Plugins can declare static directories that PeerTube will serve (images for example)
162 from `/plugins/{plugin-name}/{plugin-version}/static/`
163 or `/themes/{theme-name}/{theme-version}/static/` routes.
167 Plugins can declare CSS files that PeerTube will automatically inject in the client.
168 If you need to override existing style, you can use the `#custom-css` selector:
175 #custom-css .header {
176 background-color: red;
180 ### Server API (only for plugins)
184 Plugins can register settings, that PeerTube will inject in the administration interface.
185 The following fields will be automatically translated using the plugin translation files: `label`, `html`, `descriptionHTML`, `options.label`.
186 **These fields are injected in the plugin settings page as HTML, so pay attention to your translation files.**
191 function register (...) {
197 // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
199 // If type: 'select', give the select available options
201 { label: 'Label 1', value: 'value1' },
202 { label: 'Label 2', value: 'value2' }
205 // If type: 'html', set the HTML that will be injected in the page
206 html: '<strong class="...">Hello</strong><br /><br />'
209 descriptionHTML: 'The purpose of this field is...',
211 default: 'my super name',
213 // If the setting is not private, anyone can view its value (client code included)
214 // If the setting is private, only server-side hooks can access it
218 const adminName = await settingsManager.getSetting('admin-name')
220 const result = await settingsManager.getSettings([ 'admin-name', 'admin-password' ])
223 settingsManager.onSettingsChange(settings => {
224 settings['admin-name']
231 Plugins can store/load JSON data, that PeerTube will store in its database (so don't put files in there).
239 const value = await storageManager.getData('mykey')
240 await storageManager.storeData('mykey', { subkey: 'value' })
244 You can also store files in the plugin data directory (`/{plugins-directory}/data/{npm-plugin-name}`) **in PeerTube >= 3.2**.
245 This directory and its content won't be deleted when your plugin is uninstalled/upgraded.
252 const basePath = peertubeHelpers.plugin.getDataDirectoryPath()
254 fs.writeFile(path.join(basePath, 'filename.txt'), 'content of my file', function (err) {
260 #### Update video constants
262 You can add/delete video categories, licences or languages using the appropriate constant managers:
266 videoLanguageManager,
267 videoCategoryManager,
270 playlistPrivacyManager
272 videoLanguageManager.addConstant('al_bhed', 'Al Bhed')
273 videoLanguageManager.deleteConstant('fr')
275 videoCategoryManager.addConstant(42, 'Best category')
276 videoCategoryManager.deleteConstant(1) // Music
277 videoCategoryManager.resetConstants() // Reset to initial categories
278 videoCategoryManager.getConstants() // Retrieve all category constants
280 videoLicenceManager.addConstant(42, 'Best licence')
281 videoLicenceManager.deleteConstant(7) // Public domain
283 videoPrivacyManager.deleteConstant(2) // Remove Unlisted video privacy
284 playlistPrivacyManager.deleteConstant(3) // Remove Private video playlist privacy
288 #### Add custom routes
290 You can create custom routes using an [express Router](https://expressjs.com/en/4x/api.html#router) for your plugin:
296 const router = getRouter()
297 router.get('/ping', (req, res) => res.json({ message: 'pong' }))
299 // Users are automatically authenticated
300 router.get('/auth', async (res, res) => {
301 const user = await peertubeHelpers.user.getAuthUser(res)
303 const isAdmin = user.role === 0
304 const isModerator = user.role === 1
305 const isUser = user.role === 2
308 username: user.username,
317 The `ping` route can be accessed using:
318 * `/plugins/:pluginName/:pluginVersion/router/ping`
319 * Or `/plugins/:pluginName/router/ping`
322 #### Add custom WebSocket handlers
326 You can create custom WebSocket servers (like [ws](https://github.com/websockets/ws) for example) using `registerWebSocketRoute`:
330 registerWebSocketRoute,
333 const wss = new WebSocketServer({ noServer: true })
335 wss.on('connection', function connection(ws) {
336 peertubeHelpers.logger.info('WebSocket connected!')
339 ws.send('WebSocket message sent by server');
343 registerWebSocketRoute({
344 route: '/my-websocket-route',
346 handler: (request, socket, head) => {
347 wss.handleUpgrade(request, socket, head, ws => {
348 wss.emit('connection', ws, request)
355 The `my-websocket-route` route can be accessed using:
356 * `/plugins/:pluginName/:pluginVersion/ws/my-websocket-route`
357 * Or `/plugins/:pluginName/ws/my-websocket-route`
359 #### Add external auth methods
361 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):
364 function register (...) {
366 registerIdAndPassAuth({
367 authName: 'my-auth-method',
369 // PeerTube will try all id and pass plugins in the weight DESC order
370 // Exposing this value in the plugin settings could be interesting
373 // Optional function called by PeerTube when the user clicked on the logout button
375 console.log('User %s logged out.', user.username')
378 // Optional function called by PeerTube when the access token or refresh token are generated/refreshed
379 hookTokenValidity: ({ token, type }) => {
380 if (type === 'access') return { valid: true }
381 if (type === 'refresh') return { valid: false }
384 // Used by PeerTube when the user tries to authenticate
385 login: ({ id, password }) => {
386 if (id === 'user' && password === 'super password') {
389 email: 'user@example.com'
391 displayName: 'User display name'
400 // Unregister this auth method
401 unregisterIdAndPassAuth('my-auth-method')
405 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):
408 function register (...) {
410 // result contains the userAuthenticated auth method you can call to authenticate a user
411 const result = registerExternalAuth({
412 authName: 'my-auth-method',
414 // Will be displayed in a button next to the login form
415 authDisplayName: () => 'Auth method'
417 // If the user click on the auth button, PeerTube will forward the request in this function
418 onAuthRequest: (req, res) => {
419 res.redirect('https://external-auth.example.com/auth')
422 // Same than registerIdAndPassAuth option
425 // Same than registerIdAndPassAuth option
426 // hookTokenValidity: ...
429 router.use('/external-auth-callback', (req, res) => {
430 // Forward the request to PeerTube
431 result.userAuthenticated({
435 email: 'user@example.com'
437 displayName: 'User display name',
439 // Custom admin flags (bypass video auto moderation etc.)
440 // https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/users/user-flag.model.ts
445 videoQuota: 1024 * 1024 * 1024, // 1GB
447 videoQuotaDaily: -1, // Unlimited
449 // Update the user profile if it already exists
450 // Default behaviour is no update
451 // Introduced in PeerTube >= 5.1
452 userUpdater: ({ fieldName, currentValue, newValue }) => {
453 // Always use new value except for videoQuotaDaily field
454 if (fieldName === 'videoQuotaDaily') return currentValue
461 // Unregister this external auth method
462 unregisterExternalAuth('my-auth-method)
466 #### Add new transcoding profiles
468 Adding transcoding profiles allow admins to change ffmpeg encoding parameters and/or encoders.
469 A transcoding profile has to be chosen by the admin of the instance using the admin configuration.
472 async function register ({
476 // Adapt bitrate when using libx264 encoder
478 const builder = (options) => {
479 const { input, resolution, fps, streamNum } = options
481 const streamString = streamNum ? ':' + streamNum : ''
483 // You can also return a promise
484 // All these options are optional
487 // Used to define an alternative scale filter, needed by some encoders
488 // Default to 'scale'
495 // Use a custom bitrate
496 '-b' + streamString + ' 10K'
501 const encoder = 'libx264'
502 const profileName = 'low-quality'
504 // Support this profile for VOD transcoding
505 transcodingManager.addVODProfile(encoder, profileName, builder)
507 // And/Or support this profile for live transcoding
508 transcodingManager.addLiveProfile(encoder, profileName, builder)
512 const builder = (options) => {
513 const { streamNum } = options
515 const streamString = streamNum ? ':' + streamNum : ''
517 // Always copy stream when PeerTube use libfdk_aac or aac encoders
523 const profileName = 'copy-audio'
525 for (const encoder of [ 'libfdk_aac', 'aac' ]) {
526 transcodingManager.addVODProfile(encoder, profileName, builder)
531 PeerTube will try different encoders depending on their priority.
532 If the encoder is not available in the current transcoding profile or in ffmpeg, it tries the next one.
533 Plugins can change the order of these encoders and add their custom encoders:
536 async function register ({
540 // Adapt bitrate when using libx264 encoder
542 const builder = () => {
549 // Support libopus and libvpx-vp9 encoders (these codecs could be incompatible with the player)
550 transcodingManager.addVODProfile('libopus', 'test-vod-profile', builder)
552 // Default priorities are ~100
553 // Lowest priority = 1
554 transcodingManager.addVODEncoderPriority('audio', 'libopus', 1000)
556 transcodingManager.addVODProfile('libvpx-vp9', 'test-vod-profile', builder)
557 transcodingManager.addVODEncoderPriority('video', 'libvpx-vp9', 1000)
559 transcodingManager.addLiveProfile('libopus', 'test-live-profile', builder)
560 transcodingManager.addLiveEncoderPriority('audio', 'libopus', 1000)
564 During live transcode input options are applied once for each target resolution.
565 Plugins are responsible for detecting such situation and applying input options only once if necessary.
569 PeerTube provides your plugin some helpers. For example:
572 async function register ({
577 const serverActor = await peertubeHelpers.server.getServerActor()
579 await peertubeHelpers.moderation.blockServer({ byAccountId: serverActor.Account.id, hostToBlock: '...' })
584 const video = await peertubeHelpers.videos.loadByUrl('...')
589 See the [plugin API reference](https://docs.joinpeertube.org/api/plugins) to see the complete helpers list.
593 You can use some server hooks to federate plugin data to other PeerTube instances that may have installed your plugin.
595 For example to federate additional video metadata:
598 async function register ({ registerHook }) {
600 // Send plugin metadata to remote instances
601 // We also update the JSON LD context because we added a new field
604 target: 'filter:activity-pub.video.json-ld.build.result',
605 handler: async (jsonld, { video }) => {
606 return Object.assign(jsonld, { recordedAt: 'https://example.com/event' })
611 target: 'filter:activity-pub.activity.context.build.result',
613 return jsonld.concat([ { recordedAt: 'https://schema.org/recordedAt' } ])
618 // Save remote video metadata
620 for (const h of [ 'action:activity-pub.remote-video.created', 'action:activity-pub.remote-video.updated' ]) {
623 handler: ({ video, videoAPObject }) => {
624 if (videoAPObject.recordedAt) {
625 // Save information about the video
634 ### Client API (themes & plugins)
636 #### Get plugin static and router routes
638 To get your plugin static route:
641 function register (...) {
642 const baseStaticUrl = peertubeHelpers.getBaseStaticRoute()
643 const imageUrl = baseStaticUrl + '/images/chocobo.png'
647 And to get your plugin router route, use `peertubeHelpers.getBaseRouterRoute()`:
650 function register (...) {
652 target: 'action:video-watch.video.loaded',
653 handler: ({ video }) => {
654 fetch(peertubeHelpers.getBaseRouterRoute() + '/my/plugin/api', {
656 headers: peertubeHelpers.getAuthHeader()
657 }).then(res => res.json())
658 .then(data => console.log('Hi %s.', data))
667 To notify the user with the PeerTube ToastModule:
670 function register (...) {
671 const { notifier } = peertubeHelpers
672 notifier.success('Success message content.')
673 notifier.error('Error message content.')
677 #### Markdown Renderer
679 To render a formatted markdown text to HTML:
682 function register (...) {
683 const { markdownRenderer } = peertubeHelpers
685 await markdownRenderer.textMarkdownToHTML('**My Bold Text**')
686 // return <strong>My Bold Text</strong>
688 await markdownRenderer.enhancedMarkdownToHTML('![alt-img](http://.../my-image.jpg)')
689 // return <img alt=alt-img src=http://.../my-image.jpg />
697 To make your own HTTP requests using the current authenticated user, use an helper to automatically set appropriate headers:
700 function register (...) {
702 target: 'action:auth-user.information-loaded',
703 handler: ({ user }) => {
705 // Useless because we have the same info in the ({ user }) parameter
706 // It's just an example
707 fetch('/api/v1/users/me', {
709 headers: peertubeHelpers.getAuthHeader()
710 }).then(res => res.json())
711 .then(data => console.log('Hi %s.', data.username))
719 To show a custom modal:
722 function register (...) {
723 peertubeHelpers.showModal({
724 title: 'My custom modal title',
725 content: '<p>My custom modal content</p>',
726 // Optionals parameters :
729 // show cancel button and call action() after hiding modal
730 cancel: { value: 'cancel', action: () => {} },
731 // show confirm button and call action() after hiding modal
732 confirm: { value: 'confirm', action: () => {} },
739 You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file):
742 function register (...) {
743 peertubeHelpers.translate('User name')
744 .then(translation => console.log('Translated User name by ' + translation))
748 #### Get public settings
750 To get your public plugin settings:
753 function register (...) {
754 peertubeHelpers.getSettings()
756 if (!s || !s['site-id'] || !s['url']) {
757 console.error('Matomo settings are not set.')
766 #### Get server config
769 function register (...) {
770 peertubeHelpers.getServerConfig()
772 console.log('Fetched server config.', config)
777 #### Add custom fields to video form
779 To add custom fields in the video form (in *Plugin settings* tab):
782 async function register ({ registerVideoField, peertubeHelpers }) {
783 const descriptionHTML = await peertubeHelpers.translate(descriptionSource)
784 const commonOptions = {
785 name: 'my-field-name,
786 label: 'My added field',
787 descriptionHTML: 'Optional description',
789 // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
790 // /!\ 'input-checkbox' could send "false" and "true" strings instead of boolean
791 type: 'input-textarea',
795 // Optional, to hide a field depending on the current form state
796 // liveVideo is in the options object when the user is creating/updating a live
797 // videoToUpdate is in the options object when the user is updating a video
798 hidden: ({ formValues, videoToUpdate, liveVideo }) => {
799 return formValues.pluginData['other-field'] === 'toto'
802 // Optional, to display an error depending on the form state
803 error: ({ formValues, value }) => {
804 if (formValues['privacy'] !== 1 && formValues['privacy'] !== 2) return { error: false }
805 if (value === true) return { error: false }
807 return { error: true, text: 'Should be enabled' }
811 const videoFormOptions = {
812 // Optional, to choose to put your setting in a specific tab in video form
813 // type: 'main' | 'plugin-settings'
817 for (const type of [ 'upload', 'import-url', 'import-torrent', 'update', 'go-live' ]) {
818 registerVideoField(commonOptions, { type, ...videoFormOptions })
823 PeerTube will send this field value in `body.pluginData['my-field-name']` and fetch it from `video.pluginData['my-field-name']`.
825 So for example, if you want to store an additional metadata for videos, register the following hooks in **server**:
828 async function register ({
832 const fieldName = 'my-field-name'
834 // Store data associated to this video
836 target: 'action:api.video.updated',
837 handler: ({ video, body }) => {
838 if (!body.pluginData) return
840 const value = body.pluginData[fieldName]
843 storageManager.storeData(fieldName + '-' + video.id, value)
847 // Add your custom value to the video, so the client autofill your field using the previously stored value
849 target: 'filter:api.video.get.result',
850 handler: async (video) => {
851 if (!video) return video
852 if (!video.pluginData) video.pluginData = {}
854 const result = await storageManager.getData(fieldName + '-' + video.id)
855 video.pluginData[fieldName] = result
863 #### Register settings script
865 To hide some fields in your settings plugin page depending on the form state:
868 async function register ({ registerSettingsScript }) {
869 registerSettingsScript({
870 isSettingHidden: options => {
871 if (options.setting.name === 'my-setting' && options.formValues['field45'] === '2') {
880 #### Plugin selector on HTML elements
882 PeerTube provides some selectors (using `id` HTML attribute) on important blocks so plugins can easily change their style.
884 For example `#plugin-selector-login-form` could be used to hide the login form.
886 See the complete list on https://docs.joinpeertube.org/api/plugins
888 #### HTML placeholder elements
890 PeerTube provides some HTML id so plugins can easily insert their own element:
893 async function register (...) {
894 const elem = document.createElement('div')
895 elem.className = 'hello-world-h4'
896 elem.innerHTML = '<h4>Hello everybody! This is an element next to the player</h4>'
898 document.getElementById('plugin-placeholder-player-next').appendChild(elem)
902 See the complete list on https://docs.joinpeertube.org/api/plugins
904 #### Add/remove left menu links
906 Left menu links can be filtered (add/remove a section or add/remove links) using the `filter:left-menu.links.create.result` client hook.
908 #### Create client page
910 To create a client page, register a new client route:
913 function register ({ registerClientRoute }) {
914 registerClientRoute({
915 route: 'my-super/route',
916 onMount: ({ rootEl }) => {
917 rootEl.innerHTML = 'hello'
926 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.
928 > The official plugin index source code is available at https://framagit.org/framasoft/peertube/plugin-index
930 ## Write a plugin/theme
933 * Find a name for your plugin or your theme (must not have spaces, it can only contain lowercase letters and `-`)
934 * Add the appropriate prefix:
935 * If you develop a plugin, add `peertube-plugin-` prefix to your plugin name (for example: `peertube-plugin-mysupername`)
936 * If you develop a theme, add `peertube-theme-` prefix to your theme name (for example: `peertube-theme-mysupertheme`)
937 * Clone the quickstart repository
938 * Configure your repository
940 * Update `package.json`
941 * Register hooks, add CSS and static files
942 * Test your plugin/theme with a local PeerTube installation
943 * Publish your plugin/theme on NPM
945 ### Clone the quickstart repository
947 If you develop a plugin, clone the `peertube-plugin-quickstart` repository:
950 git clone https://framagit.org/framasoft/peertube/peertube-plugin-quickstart.git peertube-plugin-mysupername
953 If you develop a theme, clone the `peertube-theme-quickstart` repository:
956 git clone https://framagit.org/framasoft/peertube/peertube-theme-quickstart.git peertube-theme-mysupername
959 ### Configure your repository
961 Set your repository URL:
964 cd peertube-plugin-mysupername # or cd peertube-theme-mysupername
965 git remote set-url origin https://your-git-repo
970 Update `README.md` file:
976 ### Update package.json
978 Update the `package.json` fields:
979 * `name` (should start with `peertube-plugin-` or `peertube-theme-`)
984 * `engine.peertube` (the PeerTube version compatibility, must be `>=x.y.z` and nothing else)
986 **Caution:** Don't update or remove other keys, or PeerTube will not be able to index/install your plugin.
987 If you don't need static directories, use an empty `object`:
997 And if you don't need CSS or client script files, use an empty `array`:
1003 "clientScripts": [],
1010 Now you can register hooks or settings, write CSS and add static directories to your plugin or your theme :)
1011 It's up to you to check the code you write will be compatible with the PeerTube NodeJS version, and will be supported by web browsers.
1015 If you want to write modern JavaScript, please use a transpiler like [Babel](https://babeljs.io/).
1019 The easiest way to use __Typescript__ for both front-end and backend code is to clone [peertube-plugin-quickstart-typescript](https://github.com/JohnXLivingston/peertube-plugin-quickstart-typescript/) (also available on [framagit](https://framagit.org/Livingston/peertube-plugin-quickstart-typescript/)) instead of `peertube-plugin-quickstart`.
1020 Please read carefully the [README file](https://github.com/JohnXLivingston/peertube-plugin-quickstart-typescript/blob/main/README.md), as there are some other differences with `peertube-plugin-quickstart` (using SCSS instead of CSS, linting rules, ...).
1022 If you don't want to use `peertube-plugin-quickstart-typescript`, you can also manually add a dev dependency to __Peertube__ types:
1025 npm install --save-dev @peertube/peertube-types
1028 This package exposes *server* definition files by default:
1030 import { RegisterServerOptions } from '@peertube/peertube-types'
1032 export async function register ({ registerHook }: RegisterServerOptions) {
1034 target: 'action:application.listening',
1035 handler: () => displayHelloWorld()
1040 But it also exposes client types and various models used in __PeerTube__:
1042 import { Video } from '@peertube/peertube-types';
1043 import { RegisterClientOptions } from '@peertube/peertube-types/client';
1045 function register({ registerHook, peertubeHelpers }: RegisterClientOptions) {
1047 target: 'action:admin-plugin-settings.init',
1048 handler: ({ npmName }: { npmName: string }) => {
1049 if ('peertube-plugin-transcription' !== npmName) {
1056 target: 'action:video-watch.video.loaded',
1057 handler: ({ video }: { video: Video }) => {
1058 fetch(`${peertubeHelpers.getBaseRouterRoute()}/videos/${video.uuid}/captions`, {
1060 headers: peertubeHelpers.getAuthHeader(),
1061 }).then((res) => res.json())
1062 .then((data) => console.log('Hi %s.', data));
1067 export { register };
1070 ### Add translations
1072 If you want to translate strings of your plugin (like labels of your registered settings), create a file and add it to `package.json`:
1078 "fr": "./languages/fr.json",
1079 "pt-BR": "./languages/pt-BR.json"
1085 The key should be one of the locales defined in [i18n.ts](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/i18n/i18n.ts).
1087 Translation files are just objects, with the english sentence as the key and the translation as the value.
1088 `fr.json` could contain for example:
1092 "Hello world": "Hello le monde"
1096 ### Build your plugin
1098 If you added client scripts, you'll need to build them using webpack.
1106 Add/update your files in the `clientFiles` array of `webpack.config.js`:
1109 $EDITOR ./webpack.config.js
1112 Build your client files:
1118 You built files are in the `dist/` directory. Check `package.json` to correctly point to them.
1121 ### Test your plugin/theme
1123 PeerTube dev server (ran with `npm run dev` on `localhost:3000`) can't inject plugin CSS.
1124 It's the reason why we don't use the dev mode but build PeerTube instead.
1126 You'll need to have a local PeerTube instance:
1127 * Follow the [dev prerequisites](https://github.com/Chocobozzz/PeerTube/blob/develop/.github/CONTRIBUTING.md#prerequisites)
1128 (to clone the repository, install dependencies and prepare the database)
1141 * Run PeerTube (you can access to your instance on `localhost:9000`):
1144 NODE_ENV=dev npm start
1147 * Register the instance via the CLI:
1150 node ./dist/server/tools/peertube.js auth add -u 'http://localhost:9000' -U 'root' --password 'test'
1153 Then, you can install or reinstall your local plugin/theme by running:
1156 node ./dist/server/tools/peertube.js plugins install --path /your/absolute/plugin-or-theme/path
1161 Go in your plugin/theme directory, and run:
1167 Every time you want to publish another version of your plugin/theme, just update the `version` key from the `package.json`
1168 and republish it on NPM. Remember that the PeerTube index will take into account your new plugin/theme version after ~24 hours.
1170 > If you need to force your plugin update on a specific __PeerTube__ instance, you may update the latest available version manually:
1172 > UPDATE "plugin" SET "latestVersion" = 'X.X.X' WHERE "plugin"."name" = 'plugin-shortname';
1174 > You'll then be able to click the __Update plugin__ button on the plugin list.
1178 If for a particular reason you don't want to maintain your plugin/theme anymore
1179 you can deprecate it. The plugin index will automatically remove it preventing users to find/install it from the PeerTube admin interface:
1182 npm deprecate peertube-plugin-xxx@"> 0.0.0" "explain here why you deprecate your plugin/theme"
1185 ## Plugin & Theme hooks/helpers API
1187 See the dedicated documentation: https://docs.joinpeertube.org/api/plugins
1192 ### Compatibility with PeerTube
1194 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`).
1196 * Don't make assumptions and check every parameter you want to use. For example:
1200 target: 'filter:api.video.get.result',
1202 // We check the parameter exists and the name field exists too, to avoid exceptions
1203 if (video && video.name) video.name += ' <3'
1209 * Don't try to require parent PeerTube modules, only use `peertubeHelpers`. If you need another helper or a specific hook, please [create an issue](https://github.com/Chocobozzz/PeerTube/issues/new/choose)
1210 * Don't use PeerTube dependencies. Use your own :)
1212 If your plugin is broken with a new PeerTube release, update your code and the `peertubeEngine` field of your `package.json` field.
1213 This way, older PeerTube versions will still use your old plugin, and new PeerTube versions will use your updated plugin.
1215 ### Spam/moderation plugin
1217 If you want to create an antispam/moderation plugin, you could use the following hooks:
1218 * `filter:api.video.upload.accept.result`: to accept or not local uploads
1219 * `filter:api.video-thread.create.accept.result`: to accept or not local thread
1220 * `filter:api.video-comment-reply.create.accept.result`: to accept or not local replies
1221 * `filter:api.video-threads.list.result`: to change/hide the text of threads
1222 * `filter:api.video-thread-comments.list.result`: to change/hide the text of replies
1223 * `filter:video.auto-blacklist.result`: to automatically blacklist local or remote videos
1225 ### Other plugin examples
1227 You can take a look to "official" PeerTube plugins if you want to take inspiration from them: https://framagit.org/framasoft/peertube/official-plugins