index.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /**
  2. * @module converse-chat
  3. * @copyright 2020, the Converse.js contributors
  4. * @license Mozilla Public License (MPLv2)
  5. */
  6. import ChatBox from './model.js';
  7. import MessageMixin from './message.js';
  8. import ModelWithContact from './model-with-contact.js';
  9. import chat_api from './api.js';
  10. import log from '../../log.js';
  11. import st from '../../utils/stanza';
  12. import { Collection } from "@converse/skeletor/src/collection";
  13. import { _converse, api, converse } from '../../core.js';
  14. const { Strophe, sizzle, utils } = converse.env;
  15. const u = converse.env.utils;
  16. async function handleErrorMessage (stanza) {
  17. const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
  18. if (utils.isSameBareJID(from_jid, _converse.bare_jid)) {
  19. return;
  20. }
  21. const chatbox = await api.chatboxes.get(from_jid);
  22. chatbox?.handleErrorMessageStanza(stanza);
  23. }
  24. converse.plugins.add('converse-chat', {
  25. /* Optional dependencies are other plugins which might be
  26. * overridden or relied upon, and therefore need to be loaded before
  27. * this plugin. They are called "optional" because they might not be
  28. * available, in which case any overrides applicable to them will be
  29. * ignored.
  30. *
  31. * It's possible however to make optional dependencies non-optional.
  32. * If the setting "strict_plugin_dependencies" is set to true,
  33. * an error will be raised if the plugin is not found.
  34. *
  35. * NB: These plugins need to have already been loaded via require.js.
  36. */
  37. dependencies: ['converse-chatboxes', 'converse-disco'],
  38. initialize () {
  39. /* The initialize function gets called as soon as the plugin is
  40. * loaded by converse.js's plugin machinery.
  41. */
  42. Object.assign(api, chat_api);
  43. // Configuration values for this plugin
  44. // ====================================
  45. // Refer to docs/source/configuration.rst for explanations of these
  46. // configuration settings.
  47. api.settings.extend({
  48. 'allow_message_corrections': 'all',
  49. 'allow_message_retraction': 'all',
  50. 'allow_message_styling': true,
  51. 'auto_join_private_chats': [],
  52. 'clear_messages_on_reconnection': false,
  53. 'filter_by_resource': false,
  54. 'send_chat_state_notifications': true
  55. });
  56. _converse.Message = ModelWithContact.extend(MessageMixin);
  57. _converse.Messages = Collection.extend({
  58. model: _converse.Message,
  59. comparator: 'time'
  60. });
  61. _converse.ChatBox = ChatBox;
  62. /**
  63. * Handler method for all incoming single-user chat "message" stanzas.
  64. * @private
  65. * @method _converse#handleMessageStanza
  66. * @param { MessageAttributes } attrs - The message attributes
  67. */
  68. _converse.handleMessageStanza = async function (stanza) {
  69. if (st.isServerMessage(stanza)) {
  70. // Prosody sends headline messages with type `chat`, so we need to filter them out here.
  71. const from = stanza.getAttribute('from');
  72. return log.info(`handleMessageStanza: Ignoring incoming server message from JID: ${from}`);
  73. }
  74. const attrs = await st.parseMessage(stanza, _converse);
  75. if (u.isErrorObject(attrs)) {
  76. attrs.stanza && log.error(attrs.stanza);
  77. return log.error(attrs.message);
  78. }
  79. const has_body = !!sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length;
  80. const chatbox = await api.chats.get(attrs.contact_jid, { 'nickname': attrs.nick }, has_body);
  81. await chatbox?.queueMessage(attrs);
  82. /**
  83. * @typedef { Object } MessageData
  84. * An object containing the original message stanza, as well as the
  85. * parsed attributes.
  86. * @property { XMLElement } stanza
  87. * @property { MessageAttributes } stanza
  88. * @property { ChatBox } chatbox
  89. */
  90. const data = { stanza, attrs, chatbox };
  91. /**
  92. * Triggered when a message stanza is been received and processed.
  93. * @event _converse#message
  94. * @type { object }
  95. * @property { module:converse-chat~MessageData } data
  96. */
  97. api.trigger('message', data);
  98. };
  99. function registerMessageHandlers () {
  100. _converse.connection.addHandler(
  101. stanza => {
  102. if (sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop()) {
  103. // MAM messages are handled in converse-mam.
  104. // We shouldn't get MAM messages here because
  105. // they shouldn't have a `type` attribute.
  106. log.warn(`Received a MAM message with type "chat".`);
  107. return true;
  108. }
  109. _converse.handleMessageStanza(stanza);
  110. return true;
  111. },
  112. null,
  113. 'message',
  114. 'chat'
  115. );
  116. _converse.connection.addHandler(
  117. stanza => {
  118. // Message receipts are usually without the `type` attribute. See #1353
  119. if (stanza.getAttribute('type') !== null) {
  120. // TODO: currently Strophe has no way to register a handler
  121. // for stanzas without a `type` attribute.
  122. // We could update it to accept null to mean no attribute,
  123. // but that would be a backward-incompatible change
  124. return true; // Gets handled above.
  125. }
  126. _converse.handleMessageStanza(stanza);
  127. return true;
  128. },
  129. Strophe.NS.RECEIPTS,
  130. 'message'
  131. );
  132. _converse.connection.addHandler(
  133. stanza => {
  134. handleErrorMessage(stanza);
  135. return true;
  136. },
  137. null,
  138. 'message',
  139. 'error'
  140. );
  141. }
  142. function autoJoinChats () {
  143. // Automatically join private chats, based on the
  144. // "auto_join_private_chats" configuration setting.
  145. api.settings.get('auto_join_private_chats').forEach(jid => {
  146. if (_converse.chatboxes.where({ 'jid': jid }).length) {
  147. return;
  148. }
  149. if (typeof jid === 'string') {
  150. api.chats.open(jid);
  151. } else {
  152. log.error('Invalid jid criteria specified for "auto_join_private_chats"');
  153. }
  154. });
  155. /**
  156. * Triggered once any private chats have been automatically joined as
  157. * specified by the `auto_join_private_chats` setting.
  158. * See: https://conversejs.org/docs/html/configuration.html#auto-join-private-chats
  159. * @event _converse#privateChatsAutoJoined
  160. * @example _converse.api.listen.on('privateChatsAutoJoined', () => { ... });
  161. * @example _converse.api.waitUntil('privateChatsAutoJoined').then(() => { ... });
  162. */
  163. api.trigger('privateChatsAutoJoined');
  164. }
  165. /************************ BEGIN Route Handlers ************************/
  166. function openChat (jid) {
  167. if (!utils.isValidJID(jid)) {
  168. return log.warn(`Invalid JID "${jid}" provided in URL fragment`);
  169. }
  170. api.chats.open(jid);
  171. }
  172. _converse.router.route('converse/chat?jid=:jid', openChat);
  173. /************************ END Route Handlers ************************/
  174. /************************ BEGIN Event Handlers ************************/
  175. api.listen.on('chatBoxesFetched', autoJoinChats);
  176. api.listen.on('presencesInitialized', registerMessageHandlers);
  177. api.listen.on('clearSession', async () => {
  178. if (_converse.shouldClearCache()) {
  179. await Promise.all(
  180. _converse.chatboxes.map(c => c.messages && c.messages.clearStore({ 'silent': true }))
  181. );
  182. const filter = o => o.get('type') !== _converse.CONTROLBOX_TYPE;
  183. _converse.chatboxes.clearStore({ 'silent': true }, filter);
  184. }
  185. });
  186. /************************ END Event Handlers ************************/
  187. }
  188. });