瀏覽代碼

Move message parsing code out of ChatBox into new stanza-utils plugin

JC Brand 5 年之前
父節點
當前提交
8523cae8d0
共有 6 個文件被更改,包括 224 次插入182 次删除
  1. 9 8
      spec/muc_messages.js
  2. 37 136
      src/headless/converse-chat.js
  3. 4 15
      src/headless/converse-core.js
  4. 0 10
      src/headless/converse-mam.js
  5. 2 13
      src/headless/converse-muc.js
  6. 172 0
      src/headless/utils/stanza.js

+ 9 - 8
spec/muc_messages.js

@@ -6,7 +6,7 @@
         ], factory);
 } (this, function (jasmine, mock, test_utils) {
     "use strict";
-    const { Promise, Strophe, $msg, $pres, sizzle } = converse.env;
+    const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env;
     const u = converse.env.utils;
 
     describe("A Groupchat Message", function () {
@@ -621,9 +621,9 @@
                     <origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
                 </message>`);
             spyOn(_converse.api, "trigger").and.callThrough();
-            spyOn(view.model, "isReceipt").and.callThrough();
+            spyOn(stanza_utils, "isReceipt").and.callThrough();
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
-            await u.waitUntil(() => view.model.isReceipt.calls.count() === 1);
+            await u.waitUntil(() => stanza_utils.isReceipt.calls.count() === 1);
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
             expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
             expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
@@ -657,9 +657,10 @@
                          from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
                     <received xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
                 </message>`);
-            spyOn(view.model, "isChatMarker").and.callThrough();
+            const stanza_utils = converse.env.stanza_utils;
+            spyOn(stanza_utils, "isChatMarker").and.callThrough();
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
-            await u.waitUntil(() => view.model.isChatMarker.calls.count() === 1);
+            await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 1);
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
             expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
 
@@ -669,7 +670,7 @@
                     <displayed xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
                 </message>`);
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
-            await u.waitUntil(() => view.model.isChatMarker.calls.count() === 2);
+            await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 2);
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
             expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
 
@@ -680,7 +681,7 @@
                 </message>`);
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-            await u.waitUntil(() => view.model.isChatMarker.calls.count() === 3);
+            await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 3);
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
             expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
 
@@ -691,7 +692,7 @@
                     <markable xmlns="urn:xmpp:chat-markers:0"/>
                 </message>`);
             _converse.connection._dataRecv(test_utils.createRequest(stanza));
-            await u.waitUntil(() => view.model.isChatMarker.calls.count() === 4);
+            await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 4);
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
             expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
             done();

+ 37 - 136
src/headless/converse-chat.js

@@ -1,3 +1,4 @@
+import "./utils/stanza";
 import { get, isObject, isString, propertyOf } from "lodash";
 import converse from "./converse-core";
 import filesize from "filesize";
@@ -20,7 +21,7 @@ converse.plugins.add('converse-chat', {
      *
      * NB: These plugins need to have already been loaded via require.js.
      */
-    dependencies: ["converse-chatboxes", "converse-disco"],
+    dependencies: ["stanza-utils", "converse-chatboxes", "converse-disco"],
 
     initialize () {
         /* The initialize function gets called as soon as the plugin is
@@ -28,6 +29,7 @@ converse.plugins.add('converse-chat', {
          */
         const { _converse } = this;
         const { __ } = _converse;
+        const { stanza_utils } = _converse;
 
         // Configuration values for this plugin
         // ====================================
@@ -433,7 +435,6 @@ converse.plugins.add('converse-chat', {
                 }
             },
 
-
             getMostRecentMessage () {
                 for (let i=this.messages.length-1; i>=0; i--) {
                     const message = this.messages.at(i);
@@ -444,8 +445,9 @@ converse.plugins.add('converse-chat', {
             },
 
             getUpdatedMessageAttributes (message, stanza) {  // eslint-disable-line no-unused-vars
-                // Overridden in converse-muc and converse-mam
-                return {};
+                return {
+                    'is_archived': stanza_utils.isArchived(stanza),
+                }
             },
 
             updateMessage (message, stanza) {
@@ -515,18 +517,13 @@ converse.plugins.add('converse-chat', {
                 return true;
             },
 
-            /**
-             * If the passed in `message` stanza contains an
-             * [XEP-0308](https://xmpp.org/extensions/xep-0308.html#usecase)
-             * `<replace>` element, return its `id` attribute.
-             * @private
-             * @method _converse.ChatBox#getReplaceId
-             * @param { XMLElement } stanza
-             */
-            getReplaceId (stanza) {
-                const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
-                if (el) {
-                    return el.getAttribute('id');
+            retractMessage (attrs) {
+                if (!attrs.moderated !== 'retracted' && !attrs.retracted) {
+                    return;
+                }
+                const message = this.messages.findWhere({'msgid': attrs.replaced_id, 'from': attrs.from});
+                if (!message) {
+                    return;
                 }
             },
 
@@ -596,7 +593,7 @@ converse.plugins.add('converse-chat', {
             },
 
             findDuplicateFromMessage (stanza) {
-                const text = this.getMessageBody(stanza) || undefined;
+                const text = stanza_utils.getMessageBody(stanza) || undefined;
                 if (!text) { return false; }
                 const id = stanza.getAttribute('id');
                 if (!id) { return false; }
@@ -892,75 +889,6 @@ converse.plugins.add('converse-chat', {
                 });
             },
 
-            getReferencesFromStanza (stanza) {
-                const text = propertyOf(stanza.querySelector('body'))('textContent');
-                return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
-                    const begin = ref.getAttribute('begin'),
-                          end = ref.getAttribute('end');
-                    return  {
-                        'begin': begin,
-                        'end': end,
-                        'type': ref.getAttribute('type'),
-                        'value': text.slice(begin, end),
-                        'uri': ref.getAttribute('uri')
-                    };
-                });
-            },
-
-            /**
-             * Extract the XEP-0359 stanza IDs from the passed in stanza
-             * and return a map containing them.
-             * @private
-             * @method _converse.ChatBox#getStanzaIDs
-             * @param { XMLElement } stanza - The message stanza
-             */
-            getStanzaIDs (stanza) {
-                const attrs = {};
-                const stanza_ids = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza);
-                if (stanza_ids.length) {
-                    stanza_ids.forEach(s => (attrs[`stanza_id ${s.getAttribute('by')}`] = s.getAttribute('id')));
-                }
-                const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop();
-                if (result) {
-                    const by_jid = stanza.getAttribute('from');
-                    attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
-                }
-
-                const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
-                if (origin_id) {
-                    attrs['origin_id'] = origin_id.getAttribute('id');
-                }
-                return attrs;
-            },
-
-            isArchived (original_stanza) {
-                return !!sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
-            },
-
-            getErrorMessage (stanza) {
-                const error = stanza.querySelector('error');
-                return propertyOf(error.querySelector('text'))('textContent') ||
-                    __('Sorry, an error occurred:') + ' ' + error.innerHTML;
-            },
-
-            /**
-             * Given a message stanza, return the text contained in its body.
-             * @private
-             * @param { XMLElement } stanza
-             */
-            getMessageBody (stanza) {
-                const type = stanza.getAttribute('type');
-                if (type === 'error') {
-                    return this.getErrorMessage(stanza);
-                } else {
-                    const body = stanza.querySelector('body');
-                    if (body) {
-                        return body.textContent.trim();
-                    }
-                }
-            },
-
-
             /**
              * Parses a passed in message stanza and returns an object
              * of attributes.
@@ -970,65 +898,38 @@ converse.plugins.add('converse-chat', {
              * @param { XMLElement } delay - The <delay> node from the stanza, if there was one.
              * @param { XMLElement } original_stanza - The original stanza, that contains the
              *  message stanza, if it was contained, otherwise it's the message stanza itself.
+             * @returns { Object }
              */
             async getMessageAttributesFromStanza (stanza, original_stanza) {
-                const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, original_stanza).pop();
                 const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
-                const text = this.getMessageBody(stanza) || undefined;
+                const text = stanza_utils.getMessageBody(stanza) || undefined;
                 const chat_state = stanza.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING ||
                             stanza.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED ||
                             stanza.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE ||
                             stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
                             stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
 
-                const replaced_id = this.getReplaceId(stanza)
-                const msgid = replaced_id || stanza.getAttribute('id') || original_stanza.getAttribute('id');
-                const attrs = Object.assign({
-                    'chat_state': chat_state,
-                    'is_archived': this.isArchived(original_stanza),
-                    'is_delayed': !!delay,
-                    'is_single_emoji': text ? await u.isOnlyEmojis(text) : false,
-                    'is_spoiler': !!spoiler,
-                    'message': text,
-                    'msgid': msgid,
-                    'replaced_id': replaced_id,
-                    'references': this.getReferencesFromStanza(stanza),
-                    'subject': propertyOf(stanza.querySelector('subject'))('textContent'),
-                    'thread': propertyOf(stanza.querySelector('thread'))('textContent'),
-                    'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(),
-                    'type': stanza.getAttribute('type')
-                }, this.getStanzaIDs(original_stanza));
-
-                if (attrs.type === 'groupchat') {
-                    attrs.from = stanza.getAttribute('from');
-                    attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from));
-                    attrs.sender = attrs.nick === this.get('nick') ? 'me': 'them';
-                    attrs.received = (new Date()).toISOString();
-                } else {
-                    attrs.from = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
-                    if (attrs.from === _converse.bare_jid) {
-                        attrs.sender = 'me';
-                        attrs.fullname = _converse.xmppstatus.get('fullname');
-                    } else {
-                        attrs.sender = 'them';
-                        attrs.fullname = this.get('fullname');
-                    }
-                }
-                sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza).forEach(xform => {
-                    attrs['oob_url'] = xform.querySelector('url').textContent;
-                    attrs['oob_desc'] = xform.querySelector('url').textContent;
-                });
-                if (spoiler) {
-                    attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
-                }
-                if (replaced_id) {
-                    attrs['edited'] = (new Date()).toISOString();
-                }
-                // We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
-                attrs['id'] = attrs['origin_id'] ||
-                    attrs[`stanza_id ${attrs.from}`] ||
-                    u.getUniqueId();
-                return attrs;
+                return Object.assign(
+                    {
+                        'chat_state': chat_state,
+                        'is_archived': stanza_utils.isArchived(original_stanza),
+                        'is_delayed': !!delay,
+                        'is_single_emoji': text ? await u.isSingleEmoji(text) : false,
+                        'message': text,
+                        'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
+                        'references': stanza_utils.getReferences(stanza),
+                        'subject': propertyOf(stanza.querySelector('subject'))('textContent'),
+                        'thread': propertyOf(stanza.querySelector('thread'))('textContent'),
+                        'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(),
+                        'type': stanza.getAttribute('type')
+                    },
+                    stanza_utils.getStanzaIDs(original_stanza),
+                    stanza_utils.getSenderAttributes(stanza, this),
+                    stanza_utils.getOutOfBandAttributes(stanza),
+                    stanza_utils.getMessageFasteningAttributes(stanza),
+                    stanza_utils.getSpoilerAttributes(stanza),
+                    stanza_utils.getCorrectionAttributes(stanza, original_stanza)
+                );
             },
 
             maybeShow () {

+ 4 - 15
src/headless/converse-core.js

@@ -18,6 +18,7 @@ import i18n from './i18n';
 import log from '@converse/headless/log';
 import pluggable from 'pluggable.js/src/pluggable';
 import sizzle from 'sizzle';
+import stanza_utils from "@converse/headless/utils/stanza";
 import u from '@converse/headless/utils/core';
 
 const Strophe = strophe.default.Strophe;
@@ -91,7 +92,8 @@ const CORE_PLUGINS = [
     'converse-rsm',
     'converse-smacks',
     'converse-status',
-    'converse-vcard'
+    'converse-vcard',
+    'stanza-utils'
 ];
 
 
@@ -1800,20 +1802,7 @@ Object.assign(window.converse, {
      * @property {function} converse.env.sizzle    - [Sizzle](https://sizzlejs.com) CSS selector engine.
      * @property {object} converse.env.utils       - Module containing common utility methods used by Converse.
      */
-    'env': {
-        '$build': $build,
-        '$iq': $iq,
-        '$msg': $msg,
-        '$pres': $pres,
-        'Backbone': Backbone,
-        'Promise': Promise,
-        'Strophe': Strophe,
-        '_': _,
-        'log': log,
-        'dayjs': dayjs,
-        'sizzle': sizzle,
-        'utils': u
-    }
+    'env': { $build, $iq, $msg, $pres, Backbone, Promise, Strophe, _, dayjs, log, sizzle, stanza_utils, u, 'utils': u }
 });
 
 /**

+ 0 - 10
src/headless/converse-mam.js

@@ -38,16 +38,6 @@ converse.plugins.add('converse-mam', {
                     return this.findDuplicateFromArchiveID(stanza);
                 }
                 return message;
-            },
-
-            getUpdatedMessageAttributes (message, stanza) {
-                const attrs = this.__super__.getUpdatedMessageAttributes.apply(this, arguments);
-                if (message && !message.get('is_archived')) {
-                    return Object.assign(attrs, {
-                        'is_archived': this.isArchived(stanza)
-                    }, this.getStanzaIDs(stanza))
-                }
-                return attrs;
             }
         }
     },

+ 2 - 13
src/headless/converse-muc.js

@@ -15,6 +15,7 @@ import "./utils/muc";
 import { clone, get, intersection, invoke, isElement, isObject, isString, uniq, zipObject } from "lodash";
 import converse from "./converse-core";
 import log from "./log";
+import stanza_utils from "./utils/stanza";
 import u from "./utils/form";
 
 const MUC_ROLE_WEIGHTS = {
@@ -1330,16 +1331,6 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
-            isReceipt (stanza) {
-                return sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).length > 0;
-            },
-
-            isChatMarker (stanza) {
-                return sizzle(
-                    `received[xmlns="${Strophe.NS.MARKERS}"],
-                     displayed[xmlns="${Strophe.NS.MARKERS}"],
-                     acknowledged[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length > 0;
-            },
 
             /**
              * Handle a subject change and return `true` if so.
@@ -1527,9 +1518,7 @@ converse.plugins.add('converse-muc', {
                 if (message) {
                     this.updateMessage(message, original_stanza);
                 }
-                if (message ||
-                        this.isReceipt(stanza) ||
-                        this.isChatMarker(stanza)) {
+                if (message || stanza_utils.isReceipt(stanza) || stanza_utils.isChatMarker(stanza)) {
                     return _converse.api.trigger('message', {'stanza': original_stanza});
                 }
                 const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);

+ 172 - 0
src/headless/utils/stanza.js

@@ -0,0 +1,172 @@
+import * as strophe from 'strophe.js/src/core';
+import { get, propertyOf } from "lodash";
+import sizzle from 'sizzle';
+import u from '@converse/headless/utils/core';
+
+const Strophe = strophe.default.Strophe;
+
+/**
+ * The stanza utils object. Contains utility functions related to stanza
+ * processing.
+ * @namespace stanza_utils
+ */
+const stanza_utils = {
+
+    isReceipt (stanza) {
+        return sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).length > 0;
+    },
+
+    isChatMarker (stanza) {
+        return sizzle(
+            `received[xmlns="${Strophe.NS.MARKERS}"],
+             displayed[xmlns="${Strophe.NS.MARKERS}"],
+             acknowledged[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length > 0;
+    },
+
+    /**
+     * Determines whether the passed in stanza represents a XEP-0313 MAM stanza
+     * @private
+     * @method stanza_utils#isArchived
+     * @param { XMLElement } stanza - The message stanza
+     * @returns { Boolean }
+     */
+    isArchived (original_stanza) {
+        return !!sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
+    },
+
+    /**
+     * Extract the XEP-0359 stanza IDs from the passed in stanza
+     * and return a map containing them.
+     * @private
+     * @method _converse.stanza_utils#getStanzaIDs
+     * @param { XMLElement } stanza - The message stanza
+     * @returns { Object }
+     */
+    getStanzaIDs (stanza) {
+        const attrs = {};
+        const stanza_ids = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza);
+        if (stanza_ids.length) {
+            stanza_ids.forEach(s => (attrs[`stanza_id ${s.getAttribute('by')}`] = s.getAttribute('id')));
+        }
+        const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop();
+        if (result) {
+            const by_jid = stanza.getAttribute('from');
+            attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
+        }
+
+        const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
+        if (origin_id) {
+            attrs['origin_id'] = origin_id.getAttribute('id');
+        }
+        // We prefer to use one of the XEP-0359 unique and stable stanza IDs
+        // as the Model id, to avoid duplicates.
+        attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from}`] || u.getUniqueId();
+        return attrs;
+    },
+
+    /**
+     * Parses a passed in message stanza and returns an object of known attributes related to
+     * XEP-0422 Message Fastening.
+     * @private
+     * @method _converse.stanza_utils#getMessageFasteningAttributes
+     * @param { XMLElement } stanza - The message stanza
+     * @returns { Object }
+     */
+    getMessageFasteningAttributes (stanza) {
+        const substanza = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
+        if (substanza === null) {
+            return {};
+        }
+        const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE}"]`, substanza).pop();
+        if (moderated) {
+            const retracted = !!sizzle(`retract[xmlns="${Strophe.NS.RETRACT}"]`, moderated).length;
+            return {
+                'moderated': retracted ? 'retracted' : 'unknown',
+                'moderated_by': moderated.get('by'),
+                'moderated_reason': get(moderated.querySelector('reason'), 'textContent')
+            }
+        }
+    },
+
+    getReferences (stanza) {
+        const text = propertyOf(stanza.querySelector('body'))('textContent');
+        return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
+            const begin = ref.getAttribute('begin'),
+                  end = ref.getAttribute('end');
+            return  {
+                'begin': begin,
+                'end': end,
+                'type': ref.getAttribute('type'),
+                'value': text.slice(begin, end),
+                'uri': ref.getAttribute('uri')
+            };
+        });
+    },
+
+
+    getSenderAttributes (stanza, chatbox, _converse) {
+        const type = stanza.getAttribute('type');
+        if (type === 'groupchat') {
+            const from = stanza.getAttribute('from');
+            const nick = Strophe.unescapeNode(Strophe.getResourceFromJid(from));
+            return {
+                'from':  from,
+                'nick': nick,
+                'sender': nick === chatbox.get('nick') ? 'me': 'them',
+                'received': (new Date()).toISOString(),
+            }
+        } else {
+            const from = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
+            if (from === _converse.bare_jid) {
+                return {
+                    from,
+                    'sender': 'me',
+                    'fullname': _converse.xmppstatus.get('fullname')
+                }
+            } else {
+                return {
+                    from,
+                    'sender': 'them',
+                    'fullname': chatbox.get('fullname')
+                }
+            }
+        }
+    },
+
+    getSpoilerAttributes (stanza) {
+        const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
+        return {
+            'is_spoiler': !!spoiler,
+            'spoiler_hint': get(spoiler, 'textContent')
+        }
+    },
+
+    getOutOfBandAttributes (stanza) {
+        const xform = sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza).pop();
+        if (xform) {
+            return {
+                'oob_url': get(xform.querySelector('url'), 'textContent'),
+                'oob_desc': get(xform.querySelector('desc'), 'textContent')
+            }
+        }
+        return {};
+    },
+
+    getCorrectionAttributes (stanza) {
+        const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
+        if (el) {
+            const replaced_id = el.getAttribute('id');
+            const msgid = replaced_id;
+            if (replaced_id) {
+                return {
+                    msgid,
+                    replaced_id,
+                    'edited': new Date().toISOString()
+                }
+            }
+        }
+        return {};
+    }
+}
+
+export default stanza_utils;