123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- .. raw:: html
- <div id="banner"><a href="https://github.com/jcbrand/converse.js/blob/master/docs/source/theming.rst">Edit me on GitHub</a></div>
- .. _`writing-a-plugin`:
- Writing a plugin
- ================
- Introduction
- ------------
- Converse exposes a plugin architecture through which developers can modify
- and extend its functionality.
- Using plugins is good engineering practice, and using them is the *only* recommended
- way of changing Converse or adding new features to it.
- In particular, plugins have the following advantages:
- The main benefit of plugins is their *isolation of concerns* (and features).
- From this benefit flows various 2nd degree advantages, such as the ability to
- make smaller production builds (by excluding unused plugins) and an easier
- upgrade path by avoiding touching Converse's internals.
- Each plugin comes in its own file, and Converse's plugin architecture,
- called `pluggable.js <https://github.com/jcbrand/pluggable.js/>`_, provides you
- with the ability to "hook in" to the core code and other plugins.
- Converse itself is composed out of plugins and uses pluggable.js. Take a look at the
- `src <https://github.com/jcbrand/converse.js/tree/master/src>`_ directory. All
- the files that follow the pattern `converse-*.js` are plugins.
- Plugins (by way of Pluggable.js) enable developers to extend and override existing objects,
- functions and the `Backbone <http://backbonejs.org/>`_ models and views that make up
- Converse.
- Besides that, in plugins you can also write new Backbone (or other) models and views,
- in order to add a new functionality.
- To more deeply understand how this plugin architecture works, please read the
- `pluggable.js documentation <https://jcbrand.github.io/pluggable.js/>`_
- and to understand its inner workings, please refer to the `annotated source code
- <https://jcbrand.github.io/pluggable.js/docs/pluggable.html>`_.
- .. note:: **Trying out a plugin in JSFiddle**
- Because Converse consists only of JavaScript, HTML and CSS (with no backend
- code required like PHP, Python or Ruby) it runs fine in JSFiddle.
- Here's a Fiddle with a Converse plugin that calls ``alert`` once it gets
- initialized and also when a chat message gets rendered: https://jsfiddle.net/4drfaok0/15/
- .. note:: **Generating a plugin with Yeoman**
- The rest of this document explains how to write a plugin for Converse and
- ends with a documented example of a plugin.
- There is a `Yeoman <http://yeoman.io/>`_ code generator, called
- `generator-conversejs <https://github.com/jcbrand/generator-conversejs>`_, which
- you can use to generate plugin scaffolding/boilerplate that serves as a
- starting point and basis for writing your plugin.
- Please refer to the `generator-conversejs <https://github.com/jcbrand/generator-conversejs>`_
- README for information on how to use it.
- 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, which gets called at the end of the
- ``converse.initialize`` method which is the top-level method that gets called
- by the website to configure and initialize Converse itself.
- Here's an example code snippet:
- .. code-block:: javascript
- converse.plugins.add('myplugin', {
- initialize: function () {
- // 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
- },
- });
- .. note:: It's important that `converse.plugins.add` is called **before**
- `converse.initialize` is called. Otherwise the plugin will never get
- registered and never get called.
- Whitelisting of plugins
- -----------------------
- As of Converse 3.0.0 and higher, plugins need to be whitelisted before they
- can be used. This is 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 (i.e. by malware infected
- advertising networks) we now require whitelisting.
- To whitelist a plugin simply means to specify :ref:`whitelisted_plugins` when
- you call ``converse.initialize``.
- If you're adding a "core" plugin, which means a plugin that will be
- included in the default, open-source version of Converse, then you'll
- instead whitelist the plugin by adding its name to the `core_plugins` array in
- `./src/headless/converse-core.js <https://github.com/jcbrand/converse.js/blob/master/src/headless/converse-core.js>`_.
- or the `WHITELISTED_PLUGINS` array in `./src/converse.js <https://github.com/jcbrand/converse.js/blob/master/src/converse.js>`_.
- Where you add it depends on whether your plugin is part of the headless build
- (which means it doesn't contain any view code) or not.
- Security and access to the inner workings
- -----------------------------------------
- The globally available ``converse`` object, which exposes the API methods, such
- as ``initialize`` and ``plugins.add``, is a wrapper that encloses and protects
- a sensitive inner object, named ``_converse`` (not the underscore prefix).
- This inner ``_converse`` object contains all the Backbone models and views,
- as well as various other attributes and functions.
- Within a plugin, you will have access to this internal
- `"closured" <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures>`_
- ``_converse`` object, which is normally not exposed in the global variable scope.
- The inner ``_converse`` object is made private in order to safely hide and
- encapsulate sensitive information and methods which should not be exposed
- to any 3rd-party scripts that might be running in the same page.
- Accessing 3rd party libraries
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Immediately inside the module shown above you can access 3rd party libraries (such
- dayjs and lodash) via the ``converse.env`` map.
- The code for it could look something like this:
- .. code-block:: javascript
- // Commonly used utilities and variables can be found under the "env"
- // namespace of the "converse" global.
- const { Backbone, Promise, Strophe, dayjs, sizzle, _, $build, $iq, $msg, $pres } = converse.env;
- These dependencies are closured so that they don't pollute the global
- namespace, that's why you need to access them in such a way inside the module.
- Overrides
- ---------
- Plugins can override core code or code from other plugins. You can specify
- overrides in the object passed to ``converse.plugins.add``.
- In an override you can still call the overridden function, by calling
- ``this.__super__.methodName.apply(this, arguments);`` where ``methodName`` is
- the name of the function or method you're overriding.
- The following code snippet provides an example of two different overrides:
- .. code-block:: javascript
- overrides: {
- /* The *_converse* object has a method "onConnected".
- * You can override that method as follows:
- */
- onConnected: function () {
- // Overrides the onConnected method in Converse
- // Top-level functions in "overrides" are bound to the
- // inner "_converse" object.
- const _converse = this;
- // Your custom code can come here ...
- // You can access the original function being overridden
- // via the __super__ attribute.
- // Make sure to pass on the arguments supplied to this
- // function and also to apply the proper "this" object.
- _converse.__super__.onConnected.apply(this, arguments);
- // Your custom code can come here ...
- },
- /* On the XMPPStatus Backbone model is a method sendPresence.
- * We can override is as follows:
- */
- XMPPStatus: {
- sendPresence: function (type, status_message, jid) {
- // The "_converse" object is available via the __super__
- // attribute.
- const _converse = this.__super__._converse;
- // Custom code can come here ...
- // You can call the original overridden method, by
- // accessing it via the __super__ attribute.
- // When calling it, you need to apply the proper
- // context as reference by the "this" variable.
- this.__super__.sendPresence.apply(this, arguments);
- }
- }
- }
- Use the ``overrides`` feature with caution. It basically resorts to
- monkey patching which pollutes the call stack and can make your code fragile
- and prone to bugs when Converse gets updated. Too much use of ``overrides``
- is therefore a "code smell" which should ideally be avoided.
- A better approach is to listen to the events emitted by Converse, and to add
- your code in event handlers. This is however not always possible, in which case
- the overrides are a powerful tool.
- Also, while it's possible to add new methods to classes via the ``overrides``
- feature, it's better and more explicit to use composition with
- ``Object.assign``.
- For example:
- .. code-block:: javascript
- function doSomething () {
- // Your code comes here
- }
- Object.assign(_converse.ChatBoxView.prototype, { doSomething });
- Overriding a template
- ~~~~~~~~~~~~~~~~~~~~~
- Converse uses various templates, loaded with lodash, to generate its HTML.
- It's not possible to override a template with the plugin's ``overrides``
- feature, instead you should configure a new path to your own template via your
- module bundler.
- For example, with Webpack (which Converse uses internall), you can specify an
- ``alias`` for the template you want to override. This alias then points to your
- own custom template.
- For example, in your webpack config file, you could add the following to the
- ``config`` object that gets exported:
- .. code-block:: javascript
- module: {
- {
- test: /templates\/.*\.(html|svg)$/,
- use: [{
- loader: 'lodash-template-webpack-loader',
- options: {
- escape: /\{\{\{([\s\S]+?)\}\}\}/g,
- evaluate: /\{\[([\s\S]+?)\]\}/g,
- interpolate: /\{\{([\s\S]+?)\}\}/g,
- // By default, template places the values from your data in the
- // local scope via the with statement. However, you can specify
- // a single variable name with the variable setting. This can
- // significantly improve the speed at which a template is able
- // to render.
- variable: 'o',
- prependFilenameComment: __dirname
- }
- }]
- }
- },
- resolve: {
- extensions: ['.js'],
- modules: [
- path.join(__dirname, 'node_modules'),
- path.join(__dirname, 'node_modules/converse.js/src')
- ],
- alias: {
- 'templates/profile_view.html$': path.resolve(__dirname, 'templates/profile_view.html')
- }
- }
- You'll need to install ``lodash-template-webpack-loader``.
- Currently Converse uses a fork of `lodash-template-webpack-loader <https://github.com/jcbrand/lodash-template-webpack-loader>`_.
- To install it, you can add ``"lodash-template-webpack-loader": "jcbrand/lodash-template-webpack-loader"``
- to your package.json's ``devDependencies``.
- .. _`dependencies`:
- Plugin dependencies
- -------------------
- When using ``overrides``, the code that you want to override (which is either
- in ``converse-core`` or in other plugins), needs to be parsed already by the
- time your ``overrides`` are being parsed.
- Additionally, when you register event or promise handlers in your plugin for
- events/promises that fire in other plugins, then you want those plugins to have
- been loaded before your plugin gets loaded.
- To resolve this problem we have the ``dependencies`` Array attribute.
- With this you can specify those dependencies which need to be loaded before
- your plugin is loaded.
- In some cases, you might want to depend on another plugin if it's available,
- but don't care when it's not available.
- An example is the `converse-dragresize <https://github.com/jcbrand/converse.js/blob/master/src/converse-dragresize.js>`_
- plugin, which will add drag-resize handles to the headlines box (which shows
- messages of type ``headline``) but doesn't care when that particular plugin is
- not available.
- If the :ref:`strict_plugin_dependencies` setting is set to ``false`` (which is
- its default value), then no error will be raised if the plugin is not found.
- In this case, you can't specify the plugin as a dependency in the ``define``
- statement at the top of the plugin, since it might not always be available,
- which would cause ``require.js`` to throw an error.
- Extending Converse's configuration settings
- ----------------------------------------------
- Converse comes with various :ref:`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` (see `converse.initialize </docs/html/api/converse.html#.initialize>`_)
- gets called.
- Plugins often need their own additional configuration settings and you can add
- these settings with the `_converse.api.settings.update </docs/html/api/-_converse.api.settings.html#.update>`_
- method.
- Exposing promises
- -----------------
- Converse has a `waitUntil </docs/html/api/-_converse.api.html#.waitUntil>`_ API method
- which allows you to wait for various promises to resolve before executing a
- piece of code.
- You can add new promises for your plugin by calling
- `_converse.api.promises.add </docs/html/api/-_converse.api.promises.html#.add>`_.
- Generally, your plugin will then also be responsible for making sure these
- promises are resolved. You do this by calling
- `_converse.api.trigger </docs/html/api/-_converse.api.html#.trigger>`_, which not
- only resolves the plugin but will also emit an event with the same name.
- 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.
- In many cases, when you want to execute a piece of code in a plugin, you first
- want to make sure that the supporting data-structures that your code might rely
- on have been created and populated with data.
- There are two ways of waiting for the right time before executing your code.
- You can either listen for certain events, or you can wait for promises to
- resolve.
- For example, when you want to query the message archive between you and a
- friend, you would call ``this._converse.api.archive.query({'with': 'friend@example.org'});``
- However, simply calling this immediately in the ``initialize`` method of your plugin will
- not work, since the user is not logged in yet.
- In this case, you should first listen for the ``connection`` event, and then do your query, like so:
- .. code-block:: javascript
- converse.plugins.add('myplugin', {
- initialize: function () {
- const _converse = this._converse;
- _converse.api.listen.on('connected', function () {
- _converse.api.archive.query({'with': 'admin2@localhost'});
- });
- }
- });
- Another example is in the ``Bookmarks`` plugin (in
- `src/converse-bookmarks.js <https://github.com/jcbrand/converse.js/blob/6c3aa34c23d97d679823a64376418cd0f40a8b94/src/converse-bookmarks.js#L528>`_).
- Before bookmarks can be fetched and shown to the user, we first have to wait until
- the `"Rooms"` panel of the ``ControlBox`` has been rendered and inserted into
- the DOM. Otherwise we have no place to show the bookmarks yet.
- Therefore, there are the following lines of code in the ``initialize`` method of
- `converse-bookmarks.js <https://github.com/jcbrand/converse.js/blob/6c3aa34c23d97d679823a64376418cd0f40a8b94/src/converse-bookmarks.js#L528>`_:
- .. code-block:: javascript
- Promise.all([
- _converse.api.waitUntil('chatBoxesFetched'),
- _converse.api.waitUntil('roomsPanelRendered')
- ]).then(initBookmarks);
- What this means, is that the plugin will wait until the ``chatBoxesFetched``
- and ``roomsPanelRendered`` promises have been resolved before it calls the
- ``initBookmarks`` method (which is defined inside the plugin).
- This way, we know that we have everything in place and set up correctly before
- fetching the bookmarks.
- As yet another example, there is also the following code in the ``initialize``
- method of the plugin:
- .. code-block:: javascript
- _converse.api.listen.on('chatBoxOpened', function renderMinimizeButton (view) {
- // Inserts a "minimize" button in the chatview's header
- // Implementation code removed for brevity
- // ...
- });
- In this case, the plugin waits for the ``chatBoxOpened`` event, before it then
- calls ``renderMinimizeButton``, which adds a new button to the chatbox (which
- enables you to minimize it).
- Finding the right promises and/or events to listen to, can be a bit
- challenging, and sometimes it might be necessary to create new events or
- promises.
- Please refer to the `API documentation </docs/html/api/http://localhost:8008/docs/html/api/>`_
- for an overview of what's available to you. If you need new events or promises, then
- `please open an issue or make a pull request on Github <https://github.com/jcbrand/converse.js>`_
- A full example plugin
- ---------------------
- Below follows a documented example of a plugin. This is the same code that gets
- generated by `generator-conversejs <https://github.com/jcbrand/generator-conversejs>`_.
- .. code-block:: javascript
- import converse from "@converse/headless/converse-core";
- // Commonly used utilities and variables can be found under the "env"
- // namespace of the "converse" global.
- const { Backbone, Promise, Strophe, dayjs, sizzle, _, $build, $iq, $msg, $pres } = converse.env;
- // The following line registers your plugin.
- converse.plugins.add("myplugin", {
- /* Dependencies are other plugins which might be
- * overridden or relied upon, and therefore need to be loaded before
- * this plugin. They are "optional" because they might not be
- * available, in which case any overrides applicable to them will be
- * ignored.
- *
- * NB: These plugins need to have already been imported or loaded,
- * either in your plugin or somewhere else.
- *
- * It's possible to make these dependencies "non-optional".
- * If the setting "strict_plugin_dependencies" is set to true,
- * an error will be raised if the plugin is not found.
- */
- dependencies: [],
- /* Converse's plugin mechanism will call the initialize
- * method on any plugin (if it exists) as soon as the plugin has
- * been loaded.
- */
- initialize: function () {
- /* Inside this method, you have access to the private
- * `_converse` object.
- */
- const _converse = this._converse;
- _converse.log("The \"myplugin\" plugin is being initialized");
- /* From the `_converse` object you can get any configuration
- * options that the user might have passed in via
- * `converse.initialize`.
- *
- * You can also specify new configuration settings for this
- * plugin, or override the default values of existing
- * configuration settings. This is done like so:
- */
- _converse.api.settings.update({
- 'initialize_message': 'Initializing myplugin!'
- });
- /* The user can then pass in values for the configuration
- * settings when `converse.initialize` gets called.
- * For example:
- *
- * converse.initialize({
- * "initialize_message": "My plugin has been initialized"
- * });
- */
- alert(this._converse.initialize_message);
- /* Besides `_converse.api.settings.update`, there is also a
- * `_converse.api.promises.add` method, which allows you to
- * add new promises that your plugin is obligated to fulfill.
- *
- * This method takes a string or a list of strings which
- * represent the promise names:
- *
- * _converse.api.promises.add('myPromise');
- *
- * Your plugin should then, when appropriate, resolve the
- * promise by calling `_converse.api.emit`, which will also
- * emit an event with the same name as the promise.
- * For example:
- *
- * _converse.api.trigger('operationCompleted');
- *
- * Other plugins can then either listen for the event
- * `operationCompleted` like so:
- *
- * _converse.api.listen.on('operationCompleted', function { ... });
- *
- * or they can wait for the promise to be fulfilled like so:
- *
- * _converse.api.waitUntil('operationCompleted', function { ... });
- */
- },
- /* If you want to override some function or a Backbone model or
- * view defined elsewhere in Converse, then you do that under
- * the "overrides" namespace.
- */
- overrides: {
- /* For example, the private *_converse* object has a
- * method "onConnected". You can override that method as follows:
- */
- onConnected: function () {
- // Overrides the onConnected method in Converse
- // Top-level functions in "overrides" are bound to the
- // inner "_converse" object.
- const _converse = this;
- // Your custom code can come here ...
- // You can access the original function being overridden
- // via the __super__ attribute.
- // Make sure to pass on the arguments supplied to this
- // function and also to apply the proper "this" object.
- _converse.__super__.onConnected.apply(this, arguments);
- // Your custom code can come here ...
- },
- /* Override Converse's XMPPStatus Backbone model so that we can override the
- * function that sends out the presence stanza.
- */
- XMPPStatus: {
- sendPresence: function (type, status_message, jid) {
- // The "_converse" object is available via the __super__
- // attribute.
- const _converse = this.__super__._converse;
- // Custom code can come here ...
- // You can call the original overridden method, by
- // accessing it via the __super__ attribute.
- // When calling it, you need to apply the proper
- // context as reference by the "this" variable.
- this.__super__.sendPresence.apply(this, arguments);
- // Custom code can come here ...
- }
- }
- }
- });
|