Include XEP-0172 nick in all outgoing presence subscribe stanzas

This commit is contained in:
JC Brand 2023-02-24 04:33:15 +01:00
parent 3530ccc35d
commit fde55bea2c
12 changed files with 87 additions and 62 deletions

View File

@ -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')}));

View File

@ -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');

View File

@ -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 () {

View File

@ -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;
}

View File

@ -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);

View File

@ -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'));

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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
};