Writing a Plugin
Introduction
Section titled “Introduction”Converse has a plugin architecture based on pluggable.js and is itself composed of plugins.
Plugins live in the src/plugins/ directory (for UI plugins) and
src/headless/plugins/ directory (for headless/core plugins).
The plugin architecture lets you add new features or modify existing functionality in a modular, self-contained way, without having to change other files.
To understand how the plugin architecture works more deeply, read the pluggable.js documentation and to understand its inner workings, refer to the annotated source code.
Why write a plugin?
Section titled “Why write a plugin?”Writing a plugin is the recommended way to customize or add new features to Converse.
The main benefit of plugins is isolation of concerns. From this benefit flows various secondary advantages:
- Smaller production builds — exclude unused plugins to reduce bundle size
- Easier upgrades — avoid touching Converse’s internals, making updates smoother
- Proprietary modifications — the Mozilla Public License version 2 doesn’t require you to open source your plugin files
Each plugin lives in its own file, and Converse’s plugin architecture, pluggable.js, lets you “hook in” to the core code and other plugins.
Plugins enable you to extend and override existing objects, functions, models, and views. You can also create entirely new models and views.
Registering a plugin
Section titled “Registering a plugin”Plugins need to be registered (and whitelisted) before they can be loaded and initialized.
You register a Converse plugin by calling converse.plugins.add.
The plugin itself is a JavaScript object which usually has at least an
initialize method. This method gets called at the end of
converse.initialize(), the top-level method that websites call
to configure and initialize Converse.
Here’s a basic example:
converse.plugins.add('myplugin', {
initialize () { // This method gets called once converse.initialize has been called // and the plugin itself has been loaded.
// Inside this method, you have access to the closured // _converse object as an attribute on "this". // E.g. this._converse },});Whitelisting plugins
Section titled “Whitelisting plugins”As of Converse 3.0.0 and higher, plugins need to be whitelisted before they can be used. This is a security measure because plugins have access to a powerful API. For example, they can read all messages and send messages on the user’s behalf.
To avoid malicious plugins being registered (e.g., by malware-infected advertising networks), we require whitelisting.
To whitelist a plugin, specify whitelisted_plugins when
you call converse.initialize:
converse.initialize({ whitelisted_plugins: ['myplugin'], // ... other settings});If you’re adding a “core” plugin (one that will be included in the default, open-source
version of Converse), you’ll instead whitelist it by adding its name to the core_plugins array in
src/headless/core.js
or the WHITELISTED_PLUGINS array in src/converse.js.
Where you add it depends on whether your plugin is part of the headless build (meaning it doesn’t contain any view/UI code) or not.
Security and access to the inner workings
Section titled “Security and access to the inner workings”The globally available converse object, which exposes API methods like
initialize and plugins.add, is a wrapper that encloses and protects
a sensitive inner object named _converse (note the underscore prefix).
This inner _converse object contains all the models and views,
as well as various other attributes and functions.
Within a plugin, you have access to this internal
“closured”
_converse object, which is normally not exposed in the global variable scope.
The inner _converse object is kept private to safely hide and
encapsulate sensitive information and methods that shouldn’t be exposed
to any third-party scripts running on the same page.
Accessing third-party libraries
Section titled “Accessing third-party libraries”Inside your plugin, you can access third-party libraries (such as dayjs) via the converse.env object:
const { Promise, Strophe, dayjs, sizzle, $build, $iq, $msg, $pres } = converse.env;These dependencies are closured so they don’t pollute the global namespace, that’s why you need to access them this way inside your plugin.
Plugin dependencies
Section titled “Plugin dependencies”When you register event or promise handlers in your plugin for events/promises that fire in other plugins, you want those plugins to have been loaded before your plugin gets loaded.
To handle this, use the dependencies array attribute.
This lets you specify which plugins need to be loaded before yours:
converse.plugins.add('myplugin', {
dependencies: ['converse-muc', 'converse-chatview'],
initialize () { // Now you can safely use features from converse-muc and converse-chatview },});In some cases, you might want to depend on another plugin if it’s available, but not require it.
If the strict_plugin_dependencies setting is set to false (which is
the default), then no error will be raised if a dependency plugin is not found.
Extending Converse’s configuration settings
Section titled “Extending Converse’s configuration settings”Converse comes with various configuration settings that can be used to modify its functionality and behavior.
All configuration settings have default values which can be overridden when
converse.initialize gets called.
Plugins often need their own additional configuration settings. You can add
these with the api.settings.extend method:
converse.plugins.add('myplugin', {
initialize () { const { api } = this._converse;
api.settings.extend({ 'my_custom_setting': 'default_value', 'another_setting': true, });
// Later, read the setting: const value = api.settings.get('my_custom_setting'); },});Exposing promises
Section titled “Exposing promises”Converse has a waitUntil API method that allows you to wait for various promises
to resolve before executing code.
You can add new promises for your plugin by calling api.promises.add:
api.promises.add('myPluginReady');Your plugin is then responsible for resolving these promises. You do this by calling
api.trigger, which both resolves the promise and emits an event with the same name:
api.trigger('myPluginReady');Dealing with asynchronicity
Section titled “Dealing with asynchronicity”Due to the asynchronous nature of XMPP, many subroutines in Converse execute at different times and not necessarily in the same order.
When you want to execute code in a plugin, you often need to make sure that the supporting data structures your code relies on have been created and populated.
There are two ways to wait for the right time before executing your code:
- Listen for events
- Wait for promises to resolve
Example: Waiting for connection
Section titled “Example: Waiting for connection”If you want to query the message archive between you and a friend, you might call:
api.archive.query({ 'with': 'friend@example.org' });However, calling this immediately in your plugin’s initialize method won’t work
because the user isn’t logged in yet.
Instead, listen for the connected event first:
converse.plugins.add('myplugin', {
initialize () { const { api } = this._converse;
api.listen.on('connected', () => { api.archive.query({ 'with': 'friend@example.org' }); }); }});Example: Waiting for multiple conditions
Section titled “Example: Waiting for multiple conditions”Sometimes you need to wait for multiple things to be ready. Use Promise.all with api.waitUntil:
converse.plugins.add('myplugin', {
initialize () { const { api } = this._converse;
Promise.all([ api.waitUntil('chatBoxesFetched'), api.waitUntil('roomsPanelRendered') ]).then(() => { // Both conditions are now met — safe to proceed this.initMyFeature(); }); }});Example: Responding to events
Section titled “Example: Responding to events”You can also listen for specific events to trigger your code:
api.listen.on('chatBoxOpened', (view) => { // A chat box was just opened — add your custom button or behavior console.log('Chat opened with:', view.model.get('jid'));});Finding the right promises and events to listen to can be challenging. If you need new events or promises, please open an issue or make a pull request on GitHub.
Converse has the concept of hooks — special events that let you modify behavior at runtime.
A hook differs from a regular event in two important ways:
- Converse waits for all handlers of a hook to finish before continuing
- Handlers can modify the payload and return updated data
These properties make it possible for plugins to intercept and update data without resorting to overrides.
How hooks work
Section titled “How hooks work”A hook is triggered like this:
async function hookTriggerExample () { const payload = { foo: 'bar' }; const updated_payload = await api.hook('hookName', this, payload); return updated_payload;}In a plugin, you register a listener for this hook:
api.listen.on('hookName', (context, payload) => { // Add to the payload and return it return { ...payload, 'baz': 'buzz' };});The context parameter is usually the this of the function that triggered the hook
(or sometimes a Model instance).
The payload parameter is the data passed when the hook was triggered.
After all handlers run, the updated_payload returned from the hook trigger looks like:
{ foo: 'bar', baz: 'buzz' }Your plugin added data to the payload without any coupling between the code!
Real-world example
Section titled “Real-world example”A good example is the getMessageActionButtons hook, which lets you add, modify, or remove the action buttons on chat messages.
The Actions
community plugin uses this hook to add extra actions like like or dislike to messages.
Overriding templates
Section titled “Overriding templates”Converse uses Lit templates, which are imported as separate files.
You can configure your module bundler (e.g., Webpack or Rspack) to load a different file when a template is imported, allowing you to substitute your own custom templates.
With Webpack/Rspack, specify an alias for the template you want to override:
// In your webpack/rspack configresolve: { extensions: ['.js'], modules: [ path.join(__dirname, 'node_modules'), path.join(__dirname, 'node_modules/converse.js/src') ], alias: { 'plugins/profile/templates/profile.js$': path.resolve(__dirname, 'templates/custom-profile.js') }}This overrides the template at plugins/profile/templates/profile.js
with your own template at templates/custom-profile.js.
Overriding custom elements
Section titled “Overriding custom elements”Converse defines many custom elements derived from the CustomElement class
and declared using api.elements.define.
You can redefine any custom element by calling api.elements.define again in your plugin’s initialize method.
To extend an existing element, get its class from api.elements.registry:
converse.plugins.add('myplugin', {
initialize () { const { api } = this._converse; const { html } = converse.env;
const Message = api.elements.registry['converse-chat-message'];
class MyMessage extends Message { render () { return html`<div class="my-wrapper">MyPlugin was here! ${super.render()}</div>`; } }
api.elements.define('converse-chat-message', MyMessage); },});A complete example plugin
Section titled “A complete example plugin”Here’s a documented example of a plugin that demonstrates several concepts:
import { converse } from '@converse/headless';
// Access third-party libraries from converse.envconst { Strophe, dayjs, html } = converse.env;
converse.plugins.add('myplugin', {
// Plugins that must be loaded before this one dependencies: [],
initialize () { // Access the internal _converse object and api const { _converse, api } = this;
api.log.info('The "myplugin" plugin is being initialized');
// Add custom configuration settings api.settings.extend({ 'my_plugin_greeting': 'Hello from MyPlugin!' });
// Read a setting const greeting = api.settings.get('my_plugin_greeting'); console.log(greeting);
// Listen for events api.listen.on('connected', () => { api.log.info('User connected — MyPlugin is ready!'); });
// Use a hook to add a custom toolbar button api.listen.on('getToolbarButtons', (toolbar_el, buttons) => { buttons.push(html` <button class="my-button" @click=${() => alert('Hello from MyPlugin!')}> <converse-icon class="fa fa-star" size="1em"></converse-icon> </button> `); return buttons; }); }});To use this plugin, make sure to whitelist it when initializing Converse:
converse.initialize({ whitelisted_plugins: ['myplugin'], // ... other settings});That’s it! You now have the foundation to build your own Converse plugins. If you have questions or need help, feel free to join the chat at discuss@conference.conversejs.org.
Thank you to our sponsors
If you'd like to sponsor this project, please visit Github, Patreon, Liberapay or contact us.