Selaa lähdekoodia

Include XEP-0172 nick in all outgoing presence subscribe stanzas

JC Brand 2 vuotta sitten
vanhempi
commit
fde55bea2c

+ 2 - 11
src/headless/plugins/roster/contact.js

@@ -1,3 +1,4 @@
+import '@converse/headless/plugins/status/api.js';
 import { Model } from '@converse/skeletor/src/model.js';
 import { _converse, api, converse } from "@converse/headless/core";
 import { getOpenPromise } from '@converse/openpromise';
@@ -93,21 +94,12 @@ const RosterContact = Model.extend({
 
     /**
      * Send a presence subscription request to this roster contact
-     * @private
      * @method _converse.RosterContacts#subscribe
      * @param { String } message - An optional message to explain the
      *      reason for the subscription request.
      */
     subscribe (message) {
-        const pres = $pres({to: this.get('jid'), type: "subscribe"});
-        if (message && message !== "") {
-            pres.c("status").t(message).up();
-        }
-        const nick = _converse.xmppstatus.getNickname() || _converse.xmppstatus.getFullname();
-        if (nick) {
-            pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
-        }
-        api.send(pres);
+        api.user.presence.send('subscribe', this.get('jid'), message);
         this.save('ask', "subscribe"); // ask === 'subscribe' Means we have asked to subscribe to them.
         return this;
     },
@@ -135,7 +127,6 @@ const RosterContact = Model.extend({
      * send notification of the subscription state change to the user.
      * @private
      * @method _converse.RosterContacts#ackUnsubscribe
-     * @param { String } jid - The Jabber ID of the user who is unsubscribing
      */
     ackUnsubscribe () {
         api.send($pres({'type': 'unsubscribe', 'to': this.get('jid')}));

+ 4 - 6
src/headless/plugins/roster/contacts.js

@@ -112,7 +112,7 @@ const RosterContacts = Collection.extend({
      * @method _converse.RosterContacts#addAndSubscribe
      * @param { String } jid - The Jabber ID of the user being added and subscribed to.
      * @param { String } name - The name of that user
-     * @param { Array.String } groups - Any roster groups the user might belong to
+     * @param { Array<String> } groups - Any roster groups the user might belong to
      * @param { String } message - An optional message to explain the reason for the subscription request.
      * @param { Object } attributes - Any additional attributes to be stored on the user's model.
      */
@@ -128,9 +128,7 @@ const RosterContacts = Collection.extend({
      * @method _converse.RosterContacts#sendContactAddIQ
      * @param { String } jid - The Jabber ID of the user being added
      * @param { String } name - The name of that user
-     * @param { Array.String } groups - Any roster groups the user might belong to
-     * @param { Function } callback - A function to call once the IQ is returned
-     * @param { Function } errback - A function to call if an error occurred
+     * @param { Array<String> } groups - Any roster groups the user might belong to
      */
     sendContactAddIQ (jid, name, groups) {
         name = name ? name : null;
@@ -148,7 +146,7 @@ const RosterContacts = Collection.extend({
      * @method _converse.RosterContacts#addContactToRoster
      * @param { String } jid - The Jabber ID of the user being added and subscribed to.
      * @param { String } name - The name of that user
-     * @param { Array.String } groups - Any roster groups the user might belong to
+     * @param { Array<String> } groups - Any roster groups the user might belong to
      * @param { Object } attributes - Any additional attributes to be stored on the user's model.
      */
     async addContactToRoster (jid, name, groups, attributes) {
@@ -190,7 +188,7 @@ const RosterContacts = Collection.extend({
      * Handle roster updates from the XMPP server.
      * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
      * @method _converse.RosterContacts#onRosterPush
-     * @param { Element } IQ - The IQ stanza received from the XMPP server.
+     * @param { Element } iq - The IQ stanza received from the XMPP server.
      */
     onRosterPush (iq) {
         const id = iq.getAttribute('id');

+ 2 - 2
src/headless/plugins/status/api.js

@@ -13,7 +13,7 @@ export default {
          * @param { String } type
          * @param { String } to
          * @param { String } [status] - An optional status message
-         * @param { Element[]|Strophe.Builder[]|Element|Strophe.Builder } [child_nodes]
+         * @param { Array<Element>|Array<Strophe.Builder>|Element|Strophe.Builder } [child_nodes]
          *  Nodes(s) to be added as child nodes of the `presence` XML element.
          */
         async send (type, to, status, child_nodes) {
@@ -85,7 +85,7 @@ export default {
             /**
              * @async
              * @method _converse.api.user.status.message.get
-             * @returns {string} The status message
+             * @returns { Promise<string> } The status message
              * @example const message = _converse.api.user.status.message.get()
              */
             async get () {

+ 33 - 15
src/headless/plugins/status/status.js

@@ -6,6 +6,7 @@ import { _converse, api, converse } from '@converse/headless/core';
 const { Strophe, $pres } = converse.env;
 
 const XMPPStatus = Model.extend({
+
     defaults () {
         return { "status":  api.settings.get("default_state") }
     },
@@ -30,40 +31,57 @@ const XMPPStatus = Model.extend({
         return '';
     },
 
+    /** Constructs a presence stanza
+     * @param { string } [type]
+     * @param { string } [to] - The JID to which this presence should be sent
+     * @param { string } [status_message]
+     */
     async constructPresence (type, to=null, status_message) {
         type = typeof type === 'string' ? type : (this.get('status') || api.settings.get("default_state"));
         status_message = typeof status_message === 'string' ? status_message : this.get('status_message');
+
         let presence;
-        const attrs = {to};
-        if ((type === 'unavailable') ||
+
+        if (type === 'subscribe') {
+            presence = $pres({ to, type });
+            const { xmppstatus } = _converse;
+            const nick = xmppstatus.getNickname();
+            if (nick) presence.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
+
+        } else if ((type === 'unavailable') ||
                 (type === 'probe') ||
                 (type === 'error') ||
                 (type === 'unsubscribe') ||
                 (type === 'unsubscribed') ||
-                (type === 'subscribe') ||
                 (type === 'subscribed')) {
-            attrs['type'] = type;
-            presence = $pres(attrs);
+            presence = $pres({ to, type });
+
         } else if (type === 'offline') {
-            attrs['type'] = 'unavailable';
-            presence = $pres(attrs);
+            presence = $pres({ to, type: 'unavailable' });
+
         } else if (type === 'online') {
-            presence = $pres(attrs);
+            presence = $pres({ to });
+
         } else {
-            presence = $pres(attrs).c('show').t(type).up();
+            presence = $pres({ to }).c('show').t(type).up();
         }
 
-        if (status_message) {
-            presence.c('status').t(status_message).up();
-        }
+        if (status_message) presence.c('status').t(status_message).up();
 
         const priority = api.settings.get("priority");
         presence.c('priority').t(isNaN(Number(priority)) ? 0 : priority).up();
-        if (_converse.idle) {
+
+        const { idle, idle_seconds } = _converse;
+        if (idle) {
             const idle_since = new Date();
-            idle_since.setSeconds(idle_since.getSeconds() - _converse.idle_seconds);
-            presence.c('idle', {xmlns: Strophe.NS.IDLE, since: idle_since.toISOString()});
+            idle_since.setSeconds(idle_since.getSeconds() - idle_seconds);
+            presence.c('idle', { xmlns: Strophe.NS.IDLE, since: idle_since.toISOString() });
         }
+
+        /**
+         * *Hook* which allows plugins to modify a presence stanza
+         * @event _converse#constructedPresence
+         */
         presence = await api.hook('constructedPresence', null, presence);
         return presence;
     }

+ 1 - 1
src/plugins/chatview/tests/me-messages.js

@@ -30,7 +30,7 @@ describe("A Message", function () {
         message = '/me is as well';
         await mock.sendMessage(view, message);
         expect(view.querySelectorAll('.chat-msg--action').length).toBe(2);
-        await u.waitUntil(() => sizzle('.chat-msg__author:last', view).pop().textContent.trim() === '**Romeo Montague');
+        await u.waitUntil(() => sizzle('.chat-msg__author:last', view).pop().textContent.trim() === '**Romeo');
         const last_el = sizzle('.chat-msg__text:last', view).pop();
         await u.waitUntil(() => last_el.textContent === 'is as well');
         expect(u.hasClass('chat-msg--followup', last_el)).toBe(false);

+ 1 - 1
src/plugins/chatview/tests/messages.js

@@ -651,7 +651,7 @@ describe("A Chat Message", function () {
         const msg_object = chatbox.messages.models[0];
 
         const msg_author = view.querySelector('.chat-content .chat-msg:last-child .chat-msg__author');
-        expect(msg_author.textContent.trim()).toBe('Romeo Montague');
+        expect(msg_author.textContent.trim()).toBe('Romeo');
 
         const msg_time = view.querySelector('.chat-content .chat-msg:last-child .chat-msg__time');
         const time = dayjs(msg_object.get('time')).format(api.settings.get('time_format'));

+ 2 - 2
src/plugins/chatview/tests/spoilers.js

@@ -139,7 +139,7 @@ describe("A spoiler message", function () {
         expect(body_el.textContent).toBe(spoiler);
 
         /* Test the HTML spoiler message */
-        expect(view.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
+        expect(view.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo');
 
         const message_content = view.querySelector('.chat-msg__text');
         await u.waitUntil(() => message_content.textContent === spoiler);
@@ -219,7 +219,7 @@ describe("A spoiler message", function () {
         const body_el = stanza.querySelector('body');
         expect(body_el.textContent).toBe(spoiler);
 
-        expect(view.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague');
+        expect(view.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo');
 
         const message_content = view.querySelector('.chat-msg__text');
         await u.waitUntil(() => message_content.textContent === spoiler);

+ 6 - 5
src/plugins/muc-views/tests/muc.js

@@ -7,7 +7,7 @@ describe("Groupchats", function () {
     describe("An instant groupchat", function () {
 
         it("will be created when muc_instant_rooms is set to true",
-                mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
+                mock.initConverse(['chatBoxesFetched'], { vcard: { nickname: '' } }, async function (_converse) {
 
             let IQ_stanzas = _converse.connection.IQ_stanzas;
             const muc_jid = 'lounge@montague.lit';
@@ -110,10 +110,11 @@ describe("Groupchats", function () {
         it("Can be configured to show cached messages before being joined",
             mock.initConverse(['discoInitialized'],
                 {
-                    'muc_show_logs_before_join': true,
-                    'archived_messages_page_size': 2,
-                    'muc_nickname_from_jid': false,
-                    'muc_clear_messages_on_leave': false,
+                    muc_show_logs_before_join: true,
+                    archived_messages_page_size: 2,
+                    muc_nickname_from_jid: false,
+                    muc_clear_messages_on_leave: false,
+                    vcard: { nickname: '' },
                 }, async function (_converse) {
 
             const { api } = _converse;

+ 7 - 3
src/plugins/muc-views/tests/nickname.js

@@ -281,7 +281,7 @@ describe("A MUC", function () {
         }));
 
         it("will render a nickname form if a nickname conflict happens and muc_nickname_from_jid=false",
-                mock.initConverse([], {}, async function (_converse) {
+                mock.initConverse([], { vcard: { nickname: '' }}, async function (_converse) {
 
             const muc_jid = 'conflicted@muc.montague.lit';
             await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
@@ -322,7 +322,7 @@ describe("A MUC", function () {
 
 
         it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true",
-                mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
+                mock.initConverse(['chatBoxesFetched'], {vcard: { nickname: '' }}, async function (_converse) {
 
             const { api } = _converse;
             const muc_jid = 'conflicting@muc.montague.lit'
@@ -417,7 +417,11 @@ describe("A MUC", function () {
         }));
 
         it("doesn't show the nickname field if locked_muc_nickname is true",
-                mock.initConverse(['chatBoxesFetched'], {'locked_muc_nickname': true, 'muc_nickname_from_jid': true}, async function (_converse) {
+            mock.initConverse(['chatBoxesFetched'], {
+                locked_muc_nickname: true,
+                muc_nickname_from_jid: true,
+                vcard: { nickname: '' },
+            }, async function (_converse) {
 
             await mock.openControlBox(_converse);
             await mock.waitForRoster(_converse, 'current', 0);

+ 1 - 1
src/plugins/muc-views/tests/retractions.js

@@ -308,7 +308,7 @@ describe("Message Retractions", function () {
 
     describe("A Sent Chat Message", function () {
 
-        it("can be retracted by its author", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
+        it("can be retracted by its author", mock.initConverse(['chatBoxesFetched'], { vcard: { nickname: ''} }, async function (_converse) {
             await mock.waitForRoster(_converse, 'current', 1);
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
             const view = await mock.openChatBoxFor(_converse, contact_jid);

+ 10 - 6
src/plugins/rosterview/tests/protocol.js

@@ -2,10 +2,12 @@
 
 // See: https://xmpp.org/rfcs/rfc3921.html
 
-const Strophe = converse.env.Strophe;
+const { Strophe, stx } = converse.env;
 
 describe("The Protocol", function () {
 
+    beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     describe("Integration of Roster Items and Presence Subscriptions", function () {
         /* Some level of integration between roster items and presence
          * subscriptions is normally expected by an instant messaging user
@@ -163,11 +165,13 @@ describe("The Protocol", function () {
              *  <presence to='contact@example.org' type='subscribe'/>
              */
             const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => s.matches('presence')).pop());
-            expect(Strophe.serialize(sent_presence)).toBe(
-                `<presence to="contact@example.org" type="subscribe" xmlns="jabber:client">`+
-                    `<nick xmlns="http://jabber.org/protocol/nick">Romeo Montague</nick>`+
-                `</presence>`
-            );
+            expect(sent_presence).toEqualStanza(stx`
+                <presence to="contact@example.org" type="subscribe" xmlns="jabber:client">
+                    <nick xmlns="http://jabber.org/protocol/nick">Romeo</nick>
+                    <priority>0</priority>
+                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                </presence>
+            `);
 
             /* As a result, the user's server MUST initiate a second roster
              * push to all of the user's available resources that have

+ 18 - 9
src/shared/tests/mock.js

@@ -12,7 +12,9 @@ jasmine.toEqualStanza = function toEqualStanza () {
         compare (actual, expected) {
             const result = { pass: u.isEqualNode(actual, expected) };
             if (!result.pass) {
-                result.message = `Stanzas don't match:\nActual:\n${actual.outerHTML}\nExpected:\n${expected.outerHTML}`;
+                result.message = `Stanzas don't match:\n`+
+                    `Actual:\n${(actual.tree?.() ?? actual).outerHTML}\n`+
+                    `Expected:\n${expected.tree().outerHTML}`;
             }
             return result;
         }
@@ -672,10 +674,13 @@ async function _initConverse (settings) {
         } else if (!model.get('vcard_updated') || force) {
             jid = model.get('jid') || model.get('muc_jid');
         }
+
         let fullname;
+        let nickname;
         if (!jid || jid == 'romeo@montague.lit') {
-            jid = 'romeo@montague.lit';
-            fullname = 'Romeo Montague' ;
+            jid = settings?.vcard?.jid ?? 'romeo@montague.lit';
+            fullname = settings?.vcard?.display_name ?? 'Romeo Montague' ;
+            nickname = settings?.vcard?.nickname ?? 'Romeo';
         } else {
             const name = jid.split('@')[0].replace(/\./g, ' ').split(' ');
             const last = name.length-1;
@@ -683,13 +688,17 @@ async function _initConverse (settings) {
             name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
             fullname = name.join(' ');
         }
-        const vcard = $iq().c('vCard').c('FN').t(fullname).tree();
+        const vcard = $iq().c('vCard').c('FN').t(fullname).up();
+        if (nickname) vcard.c('NICKNAME').t(nickname);
+        const vcard_el = vcard.tree();
+
         return {
-            'stanza': vcard,
-            'fullname': vcard.querySelector('FN')?.textContent,
-            'image': vcard.querySelector('PHOTO BINVAL')?.textContent,
-            'image_type': vcard.querySelector('PHOTO TYPE')?.textContent,
-            'url': vcard.querySelector('URL')?.textContent,
+            'stanza': vcard_el,
+            'fullname': vcard_el.querySelector('FN')?.textContent,
+            'nickname': vcard_el.querySelector('NICKNAME')?.textContent,
+            'image': vcard_el.querySelector('PHOTO BINVAL')?.textContent,
+            'image_type': vcard_el.querySelector('PHOTO TYPE')?.textContent,
+            'url': vcard_el.querySelector('URL')?.textContent,
             'vcard_updated': dayjs().format(),
             'vcard_error': undefined
         };