plugin_development.rst 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. .. raw:: html
  2. <div id="banner"><a href="https://github.com/jcbrand/converse.js/blob/master/docs/source/theming.rst">Edit me on GitHub</a></div>
  3. .. _`writing-a-plugin`:
  4. Writing a plugin
  5. ================
  6. Introduction
  7. ------------
  8. Converse.js exposes a plugin architecture through which developers can modify
  9. and extend its functionality.
  10. Using plugins is good engineering practice, and using them is the *only* recommended
  11. way of changing converse.js or adding new features to it.
  12. In particular, plugins have the following advantages:
  13. The main benefit of plugins is their *isolation of concerns* (and features).
  14. From this benefit flows various 2nd degree advantages, such as the ability to
  15. make smaller production builds (by excluding unused plugins) and an easier
  16. upgrade path by avoiding touching converse.js's internals.
  17. Each plugin comes in its own file, and converse.js's plugin architecture,
  18. called `pluggable.js <https://github.com/jcbrand/pluggable.js/>`_, provides you
  19. with the ability to "hook in" to the core code and other plugins.
  20. Converse.js itself is composed out of plugins and uses pluggable.js. Take a look at the
  21. `src <https://github.com/jcbrand/converse.js/tree/master/src>`_ directory. All
  22. the files that follow the pattern `converse-*.js` are plugins.
  23. Plugins (by way of Pluggable.js) enable developers to extend and override existing objects,
  24. functions and the `Backbone <http://backbonejs.org/>`_ models and views that make up
  25. Converse.js.
  26. Besides that, in plugins you can also write new Backbone (or other) models and views,
  27. in order to add a new functionality.
  28. To more deeply understand how this plugin architecture works, please read the
  29. `pluggable.js documentation <https://jcbrand.github.io/pluggable.js/>`_
  30. and to understand its inner workings, please refer to the `annotated source code
  31. <https://jcbrand.github.io/pluggable.js/docs/pluggable.html>`_.
  32. .. note:: **Trying out a plugin in JSFiddle**
  33. Because Converse.js consists only of JavaScript, HTML and CSS (with no backend
  34. code required like PHP, Python or Ruby) it runs fine in JSFiddle.
  35. Here's a Fiddle with a Converse.js plugin that calls ``alert`` once it gets
  36. initialized and also when a chat message gets rendered: https://jsfiddle.net/4drfaok0/15/
  37. .. note:: **Generating a plugin with Yeoman**
  38. The rest of this document explains how to write a plugin for Converse.js and
  39. ends with a documented example of a plugin.
  40. There is a `Yeoman <http://yeoman.io/>`_ code generator, called
  41. `generator-conversejs <https://github.com/jcbrand/generator-conversejs>`_, which
  42. you can use to generate plugin scaffolding/boilerplate that serves as a
  43. starting point and basis for writing your plugin.
  44. Please refer to the `generator-conversejs <https://github.com/jcbrand/generator-conversejs>`_
  45. README for information on how to use it.
  46. Registering a plugin
  47. --------------------
  48. Plugins need to be registered (and whitelisted) before they can be loaded and
  49. initialized.
  50. You register a converse.js plugin by calling ``converse.plugins.add``.
  51. The plugin itself is a JavaScript object which usually has at least an
  52. ``initialize`` method, which gets called at the end of the
  53. ``converse.initialize`` method which is the top-level method that gets called
  54. by the website to configure and initialize Converse.js itself.
  55. Here's an example code snippet:
  56. .. code-block:: javascript
  57. converse.plugins.add('myplugin', {
  58. initialize: function () {
  59. // This method gets called once converse.initialize has been called
  60. // and the plugin itself has been loaded.
  61. // Inside this method, you have access to the closured
  62. // _converse object as an attribute on "this".
  63. // E.g. this._converse
  64. },
  65. });
  66. .. note:: It's important that `converse.plugins.add` is called **before**
  67. `converse.initialize` is called. Otherwise the plugin will never get
  68. registered and never get called.
  69. Whitelisting of plugins
  70. -----------------------
  71. As of converse.js 3.0.0 and higher, plugins need to be whitelisted before they
  72. can be used. This is because plugins have access to a powerful API. For
  73. example, they can read all messages and send messages on the user's behalf.
  74. To avoid malicious plugins being registered (i.e. by malware infected
  75. advertising networks) we now require whitelisting.
  76. To whitelist a plugin simply means to specify :ref:`whitelisted_plugins` when
  77. you call ``converse.initialize``.
  78. If you're adding a "core" plugin, which means a plugin that will be
  79. included in the default, open-source version of converse.js, then you'll
  80. instead whitelist the plugin by adding its name to the `core_plugins` array in
  81. `./src/converse-core.js <https://github.com/jcbrand/converse.js/blob/master/src/converse-core.js>`_.
  82. Security and access to the inner workings
  83. -----------------------------------------
  84. The globally available ``converse`` object, which exposes the API methods, such
  85. as ``initialize`` and ``plugins.add``, is a wrapper that encloses and protects
  86. a sensitive inner object, named ``_converse`` (not the underscore prefix).
  87. This inner ``_converse`` object contains all the Backbone models and views,
  88. as well as various other attributes and functions.
  89. Within a plugin, you will have access to this internal
  90. `"closured" <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures>`_
  91. ``_converse`` object, which is normally not exposed in the global variable scope.
  92. The inner ``_converse`` object is made private in order to safely hide and
  93. encapsulate sensitive information and methods which should not be exposed
  94. to any 3rd-party scripts that might be running in the same page.
  95. Loading a plugin module
  96. -----------------------
  97. Converse.js uses the UMD (Universal Modules Definition) as its module syntax.
  98. This makes modules loadable via `require.js`, `webpack` or other module
  99. loaders, but also includable as old-school `<script>` tags in your HTML.
  100. Here's an example of the plugin shown above wrapped inside a UMD module:
  101. .. code-block:: javascript
  102. (function (root, factory) {
  103. if (typeof define === 'function' && define.amd) {
  104. // AMD. Register as a module called "myplugin"
  105. define(["converse"], factory);
  106. } else {
  107. // Browser globals. If you're not using a module loader such as require.js,
  108. // then this line below executes. Make sure that your plugin's <script> tag
  109. // appears after the one from converse.js.
  110. factory(converse);
  111. }
  112. }(this, function (converse) {
  113. converse.plugins.add('myplugin', {
  114. initialize: function () {
  115. // This method gets called once converse.initialize has been called
  116. // and the plugin itself has been loaded.
  117. // Inside this method, you have access to the closured
  118. // _converse object as an attribute on "this".
  119. // E.g. this._converse
  120. },
  121. });
  122. });
  123. Accessing 3rd party libraries
  124. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  125. Immediately inside the module shown above you can access 3rd party libraries (such
  126. moment and lodash) via the ``converse.env`` map.
  127. The code for it would look something like this:
  128. .. code-block:: javascript
  129. // Commonly used utilities and variables can be found under the "env"
  130. // namespace of the "converse" global.
  131. var Strophe = converse.env.Strophe,
  132. $iq = converse.env.$iq,
  133. $msg = converse.env.$msg,
  134. $pres = converse.env.$pres,
  135. $build = converse.env.$build,
  136. b64_sha1 = converse.env.b64_sha1,
  137. _ = converse.env._,
  138. moment = converse.env.moment;
  139. These dependencies are closured so that they don't pollute the global
  140. namespace, that's why you need to access them in such a way inside the module.
  141. Overrides
  142. ---------
  143. Plugins can override core code or code from other plugins. Refer to the full
  144. example at the bottom for code details.
  145. Use the ``overrides`` functionality with caution. It basically resorts to
  146. monkey patching which pollutes the call stack and can make your code fragile
  147. and prone to bugs when Converse.js gets updated. Too much use of ``overrides``
  148. is therefore a "code smell" which should ideally be avoided.
  149. A better approach is to listen to the events emitted by Converse.js, and to add
  150. your code in event handlers. This is however not always possible, in which case
  151. the overrides are a powerful tool.
  152. .. _`dependencies`:
  153. Plugin dependencies
  154. ~~~~~~~~~~~~~~~~~~~
  155. When using ``overrides``, the code that you want to override (which is either
  156. in ``converse-core`` or in other plugins), needs to be parsed already by the
  157. time your ``overrides`` are being parsed.
  158. Additionally, when you register event or promise handlers in your plugin for
  159. events/promises that fire in other plugins, then you want those plugins to have
  160. been loaded before your plugin gets loaded.
  161. To resolve this problem we have the ``dependencies`` Array attribute.
  162. With this you can specify those dependencies which need to be loaded before
  163. your plugin is loaded.
  164. In some cases, you might want to depend on another plugin if it's available,
  165. but don't care when it's not available.
  166. An example is the `converse-dragresize <https://github.com/jcbrand/converse.js/blob/master/src/converse-dragresize.js>`_
  167. plugin, which will add drag-resize handles to the headlines box (which shows
  168. messages of type ``headline``) but doesn't care when that particular plugin is
  169. not available.
  170. If the :ref:`strict_plugin_dependencies` setting is set to ``false`` (which is
  171. its default value), then no error will be raised if the plugin is not found.
  172. In this case, you can't specify the plugin as a dependency in the ``define``
  173. statement at the top of the plugin, since it might not always be available,
  174. which would cause ``require.js`` to throw an error.
  175. Extending converse.js's configuration settings
  176. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  177. Converse.js comes with various :ref:`configuration-settings` that can be used to
  178. modify its functionality and behavior.
  179. All configuration settings have default values which can be overridden when
  180. `converse.initialize` (see :ref:`initialize`) gets called.
  181. Plugins often need their own additional configuration settings and you can add
  182. these settings with the `_converse.api.settings.update` method (see
  183. :ref:`settings-update`).
  184. Exposing promises
  185. ~~~~~~~~~~~~~~~~~
  186. Converse.js has a ``waitUntil`` API method (see :ref:`waituntil-grouping`)
  187. which allows you to wait for various promises to resolve before executing a
  188. piece of code.
  189. You can add new promises for your plugin by calling
  190. ``_converse.api.promises.add`` (see :ref:`promises-grouping`).
  191. Generally, your plugin will then also be responsible for making sure these
  192. promises are resolved. You do this by calling ``_converse.api.emit``, which not
  193. only resolves the plugin but will also emit an event with the same name.
  194. Dealing with asynchronicity
  195. ---------------------------
  196. Due to the asynchronous nature of XMPP, many subroutines in Converse.js execute
  197. at different times and not necessarily in the same order.
  198. In many cases, when you want to execute a piece of code in a plugin, you first
  199. want to make sure that the supporting data-structures that your code might rely
  200. on have been created and populated with data.
  201. There are two ways of waiting for the right time before executing your code.
  202. You can either listen for certain events, or you can wait for promises to
  203. resolve.
  204. For example, when you want to query the message archive between you and a
  205. friend, you would call ``this._converse.api.archive.query({'with': 'friend@example.org'});``
  206. However, simply calling this immediately in the ``initialize`` method of your plugin will
  207. not work, since the user is not logged in yet.
  208. In this case, you should first listen for the ``connection`` event, and then do your query, like so:
  209. .. code-block:: javascript
  210. converse.plugins.add('myplugin', {
  211. initialize: function () {
  212. var _converse = this._converse;
  213. _converse.api.listen.on('connected', function () {
  214. _converse.api.archive.query({'with': 'admin2@localhost'});
  215. });
  216. }
  217. });
  218. Another example is in the ``Bookmarks`` plugin (in
  219. `src/converse-bookmarks.js <https://github.com/jcbrand/converse.js/blob/6c3aa34c23d97d679823a64376418cd0f40a8b94/src/converse-bookmarks.js#L528>`_).
  220. Before bookmarks can be fetched and shown to the user, we first have to wait until
  221. the `"Rooms"` panel of the ``ControlBox`` has been rendered and inserted into
  222. the DOM. Otherwise we have no place to show the bookmarks yet.
  223. Therefore, there are the following lines of code in the ``initialize`` method of
  224. `converse-bookmarks.js <https://github.com/jcbrand/converse.js/blob/6c3aa34c23d97d679823a64376418cd0f40a8b94/src/converse-bookmarks.js#L528>`_:
  225. .. code-block:: javascript
  226. Promise.all([
  227. _converse.api.waitUntil('chatBoxesFetched'),
  228. _converse.api.waitUntil('roomsPanelRendered')
  229. ]).then(initBookmarks);
  230. What this means, is that the plugin will wait until the ``chatBoxesFetched``
  231. and ``roomsPanelRendered`` promises have been resolved before it calls the
  232. ``initBookmarks`` method (which is defined inside the plugin).
  233. This way, we know that we have everything in place and set up correctly before
  234. fetching the bookmarks.
  235. As yet another example, there is also the following code in the ``initialize``
  236. method of the plugin:
  237. .. code-block:: javascript
  238. _converse.api.listen.on('chatBoxOpened', function renderMinimizeButton (view) {
  239. // Inserts a "minimize" button in the chatview's header
  240. // Implementation code removed for brevity
  241. // ...
  242. });
  243. In this case, the plugin waits for the ``chatBoxOpened`` event, before it then
  244. calls ``renderMinimizeButton``, which adds a new button to the chatbox (which
  245. enables you to minimize it).
  246. Finding the right promises and/or events to listen to, can be a bit
  247. challenging, and sometimes it might be necessary to create new events or
  248. promises.
  249. Please refer to the :ref:`events-API` section of the documentation for an
  250. overview of what's available to you. If you need new events or promises, then
  251. `please open an issue or make a pull request on Github <https://github.com/jcbrand/converse.js>`_
  252. A full example plugin
  253. ---------------------
  254. Below follows a documented example of a plugin. This is the same code that gets
  255. generated by `generator-conversejs <https://github.com/jcbrand/generator-conversejs>`_.
  256. .. code-block:: javascript
  257. (function (root, factory) {
  258. if (typeof define === 'function' && define.amd) {
  259. // AMD. Register as a module called "myplugin"
  260. define(["converse"], factory);
  261. } else {
  262. // Browser globals. If you're not using a module loader such as require.js,
  263. // then this line below executes. Make sure that your plugin's <script> tag
  264. // appears after the one from converse.js.
  265. factory(converse);
  266. }
  267. }(this, function (converse) {
  268. // Commonly used utilities and variables can be found under the "env"
  269. // namespace of the "converse" global.
  270. var Strophe = converse.env.Strophe,
  271. $iq = converse.env.$iq,
  272. $msg = converse.env.$msg,
  273. $pres = converse.env.$pres,
  274. $build = converse.env.$build,
  275. b64_sha1 = converse.env.b64_sha1,
  276. _ = converse.env._,
  277. moment = converse.env.moment;
  278. // The following line registers your plugin.
  279. converse.plugins.add("myplugin", {
  280. /* Dependencies are other plugins which might be
  281. * overridden or relied upon, and therefore need to be loaded before
  282. * this plugin. They are "optional" because they might not be
  283. * available, in which case any overrides applicable to them will be
  284. * ignored.
  285. *
  286. * NB: These plugins need to have already been loaded via require.js.
  287. *
  288. * It's possible to make these dependencies "non-optional".
  289. * If the setting "strict_plugin_dependencies" is set to true,
  290. * an error will be raised if the plugin is not found.
  291. */
  292. 'dependencies': [],
  293. /* Converse.js's plugin mechanism will call the initialize
  294. * method on any plugin (if it exists) as soon as the plugin has
  295. * been loaded.
  296. */
  297. 'initialize': function () {
  298. /* Inside this method, you have access to the private
  299. * `_converse` object.
  300. */
  301. var _converse = this._converse;
  302. _converse.log("The \"myplugin\" plugin is being initialized");
  303. /* From the `_converse` object you can get any configuration
  304. * options that the user might have passed in via
  305. * `converse.initialize`.
  306. *
  307. * You can also specify new configuration settings for this
  308. * plugin, or override the default values of existing
  309. * configuration settings. This is done like so:
  310. */
  311. _converse.api.settings.update({
  312. 'initialize_message': 'Initializing myplugin!'
  313. });
  314. /* The user can then pass in values for the configuration
  315. * settings when `converse.initialize` gets called.
  316. * For example:
  317. *
  318. * converse.initialize({
  319. * "initialize_message": "My plugin has been initialized"
  320. * });
  321. */
  322. alert(this._converse.initialize_message);
  323. /* Besides `_converse.api.settings.update`, there is also a
  324. * `_converse.api.promises.add` method, which allows you to
  325. * add new promises that your plugin is obligated to fulfill.
  326. *
  327. * This method takes a string or a list of strings which
  328. * represent the promise names:
  329. *
  330. * _converse.api.promises.add('myPromise');
  331. *
  332. * Your plugin should then, when appropriate, resolve the
  333. * promise by calling `_converse.api.emit`, which will also
  334. * emit an event with the same name as the promise.
  335. * For example:
  336. *
  337. * _converse.api.trigger('operationCompleted');
  338. *
  339. * Other plugins can then either listen for the event
  340. * `operationCompleted` like so:
  341. *
  342. * _converse.api.listen.on('operationCompleted', function { ... });
  343. *
  344. * or they can wait for the promise to be fulfilled like so:
  345. *
  346. * _converse.api.waitUntil('operationCompleted', function { ... });
  347. */
  348. },
  349. /* If you want to override some function or a Backbone model or
  350. * view defined elsewhere in converse.js, then you do that under
  351. * the "overrides" namespace.
  352. */
  353. 'overrides': {
  354. /* For example, the private *_converse* object has a
  355. * method "onConnected". You can override that method as follows:
  356. */
  357. 'onConnected': function () {
  358. // Overrides the onConnected method in converse.js
  359. // Top-level functions in "overrides" are bound to the
  360. // inner "_converse" object.
  361. var _converse = this;
  362. // Your custom code can come here ...
  363. // You can access the original function being overridden
  364. // via the __super__ attribute.
  365. // Make sure to pass on the arguments supplied to this
  366. // function and also to apply the proper "this" object.
  367. _converse.__super__.onConnected.apply(this, arguments);
  368. // Your custom code can come here ...
  369. },
  370. /* Override converse.js's XMPPStatus Backbone model so that we can override the
  371. * function that sends out the presence stanza.
  372. */
  373. 'XMPPStatus': {
  374. 'sendPresence': function (type, status_message, jid) {
  375. // The "_converse" object is available via the __super__
  376. // attribute.
  377. var _converse = this.__super__._converse;
  378. // Custom code can come here ...
  379. // You can call the original overridden method, by
  380. // accessing it via the __super__ attribute.
  381. // When calling it, you need to apply the proper
  382. // context as reference by the "this" variable.
  383. this.__super__.sendPresence.apply(this, arguments);
  384. // Custom code can come here ...
  385. }
  386. }
  387. }
  388. });
  389. }));