Move various funcitons related to MUC member lists to utils

and out of the MUC views plugin.

Refs #1032
This commit is contained in:
JC Brand 2018-03-29 11:56:24 +02:00
parent 06141b3212
commit ebfd0a8f77
7 changed files with 343 additions and 328 deletions

View File

@ -882,7 +882,7 @@
_converse.api.rooms.open('coven@chat.shakespeare.lit', {'nick': 'some1'}); _converse.api.rooms.open('coven@chat.shakespeare.lit', {'nick': 'some1'});
view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
spyOn(view, 'saveAffiliationAndRole').and.callThrough(); spyOn(view.model, 'saveAffiliationAndRole').and.callThrough();
// We pretend this is a new room, so no disco info is returned. // We pretend this is a new room, so no disco info is returned.
var features_stanza = $iq({ var features_stanza = $iq({
@ -913,7 +913,7 @@
}).up() }).up()
.c('status', {code: '110'}); .c('status', {code: '110'});
_converse.connection._dataRecv(test_utils.createRequest(presence)); _converse.connection._dataRecv(test_utils.createRequest(presence));
expect(view.saveAffiliationAndRole).toHaveBeenCalled(); expect(view.model.saveAffiliationAndRole).toHaveBeenCalled();
expect($(view.el.querySelector('.toggle-chatbox-button')).is(':visible')).toBeTruthy(); expect($(view.el.querySelector('.toggle-chatbox-button')).is(':visible')).toBeTruthy();
test_utils.waitUntil(function () { test_utils.waitUntil(function () {
@ -1348,7 +1348,7 @@
// receiving the features for the room. // receiving the features for the room.
view.model.set('open', 'true'); view.model.set('open', 'true');
spyOn(view, 'directInvite').and.callThrough(); spyOn(view.model, 'directInvite').and.callThrough();
var $input; var $input;
$(view.el).find('.chat-area').remove(); $(view.el).find('.chat-area').remove();
@ -1376,7 +1376,7 @@
evt.button = 0; // For some reason awesomplete wants this evt.button = 0; // For some reason awesomplete wants this
$hint[0].dispatchEvent(evt); $hint[0].dispatchEvent(evt);
expect(window.prompt).toHaveBeenCalled(); expect(window.prompt).toHaveBeenCalled();
expect(view.directInvite).toHaveBeenCalled(); expect(view.model.directInvite).toHaveBeenCalled();
expect(sent_stanza.toLocaleString()).toBe( expect(sent_stanza.toLocaleString()).toBe(
"<message from='dummy@localhost/resource' to='felix.amsel@localhost' id='" + "<message from='dummy@localhost/resource' to='felix.amsel@localhost' id='" +
sent_stanza.nodeTree.getAttribute('id') + sent_stanza.nodeTree.getAttribute('id') +
@ -2140,7 +2140,7 @@
}); });
var view = _converse.chatboxviews.get('lounge@localhost'); var view = _converse.chatboxviews.get('lounge@localhost');
spyOn(view, 'onMessageSubmitted').and.callThrough(); spyOn(view, 'onMessageSubmitted').and.callThrough();
spyOn(view, 'setAffiliation').and.callThrough(); spyOn(view.model, 'setAffiliation').and.callThrough();
spyOn(view, 'showStatusNotification').and.callThrough(); spyOn(view, 'showStatusNotification').and.callThrough();
spyOn(view, 'validateRoleChangeCommand').and.callThrough(); spyOn(view, 'validateRoleChangeCommand').and.callThrough();
var textarea = view.el.querySelector('.chat-textarea') var textarea = view.el.querySelector('.chat-textarea')
@ -2156,7 +2156,7 @@
"Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason.", "Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason.",
true true
); );
expect(view.setAffiliation).not.toHaveBeenCalled(); expect(view.model.setAffiliation).not.toHaveBeenCalled();
// Call now with the correct amount of arguments. // Call now with the correct amount of arguments.
// XXX: Calling onMessageSubmitted directly, trying // XXX: Calling onMessageSubmitted directly, trying
@ -2165,7 +2165,7 @@
view.onMessageSubmitted('/owner annoyingGuy@localhost You\'re responsible'); view.onMessageSubmitted('/owner annoyingGuy@localhost You\'re responsible');
expect(view.validateRoleChangeCommand.calls.count()).toBe(2); expect(view.validateRoleChangeCommand.calls.count()).toBe(2);
expect(view.showStatusNotification.calls.count()).toBe(1); expect(view.showStatusNotification.calls.count()).toBe(1);
expect(view.setAffiliation).toHaveBeenCalled(); expect(view.model.setAffiliation).toHaveBeenCalled();
// Check that the member list now gets updated // Check that the member list now gets updated
expect(sent_IQ.toLocaleString()).toBe( expect(sent_IQ.toLocaleString()).toBe(
"<iq to='lounge@localhost' type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+ "<iq to='lounge@localhost' type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
@ -2193,7 +2193,7 @@
}); });
var view = _converse.chatboxviews.get('lounge@localhost'); var view = _converse.chatboxviews.get('lounge@localhost');
spyOn(view, 'onMessageSubmitted').and.callThrough(); spyOn(view, 'onMessageSubmitted').and.callThrough();
spyOn(view, 'setAffiliation').and.callThrough(); spyOn(view.model, 'setAffiliation').and.callThrough();
spyOn(view, 'showStatusNotification').and.callThrough(); spyOn(view, 'showStatusNotification').and.callThrough();
spyOn(view, 'validateRoleChangeCommand').and.callThrough(); spyOn(view, 'validateRoleChangeCommand').and.callThrough();
var textarea = view.el.querySelector('.chat-textarea') var textarea = view.el.querySelector('.chat-textarea')
@ -2209,7 +2209,7 @@
"Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.", "Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.",
true true
); );
expect(view.setAffiliation).not.toHaveBeenCalled(); expect(view.model.setAffiliation).not.toHaveBeenCalled();
// Call now with the correct amount of arguments. // Call now with the correct amount of arguments.
// XXX: Calling onMessageSubmitted directly, trying // XXX: Calling onMessageSubmitted directly, trying
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
@ -2217,7 +2217,7 @@
view.onMessageSubmitted('/ban annoyingGuy@localhost You\'re annoying'); view.onMessageSubmitted('/ban annoyingGuy@localhost You\'re annoying');
expect(view.validateRoleChangeCommand.calls.count()).toBe(2); expect(view.validateRoleChangeCommand.calls.count()).toBe(2);
expect(view.showStatusNotification.calls.count()).toBe(1); expect(view.showStatusNotification.calls.count()).toBe(1);
expect(view.setAffiliation).toHaveBeenCalled(); expect(view.model.setAffiliation).toHaveBeenCalled();
// Check that the member list now gets updated // Check that the member list now gets updated
expect(sent_IQ.toLocaleString()).toBe( expect(sent_IQ.toLocaleString()).toBe(
"<iq to='lounge@localhost' type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+ "<iq to='lounge@localhost' type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
@ -2902,7 +2902,7 @@
var name = mock.cur_names[0]; var name = mock.cur_names[0];
var invitee_jid = name.replace(/ /g,'.').toLowerCase() + '@localhost'; var invitee_jid = name.replace(/ /g,'.').toLowerCase() + '@localhost';
var reason = "Please join this chat room"; var reason = "Please join this chat room";
view.directInvite(invitee_jid, reason); view.model.directInvite(invitee_jid, reason);
// Check in reverse order that we requested all three lists // Check in reverse order that we requested all three lists
// (member, owner and admin). // (member, owner and admin).
@ -3023,26 +3023,26 @@
var remove_absentees = false; var remove_absentees = false;
var new_list = []; var new_list = [];
var old_list = []; var old_list = [];
var delta = roomview.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); var delta = u.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list);
expect(delta.length).toBe(0); expect(delta.length).toBe(0);
new_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; new_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}];
old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}];
delta = roomview.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); delta = u.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list);
expect(delta.length).toBe(0); expect(delta.length).toBe(0);
// When remove_absentees is false, then affiliations in the old // When remove_absentees is false, then affiliations in the old
// list which are not in the new one won't be removed. // list which are not in the new one won't be removed.
old_list = [{'jid': 'oldhag666@shakespeare.lit', 'affiliation': 'owner'}, old_list = [{'jid': 'oldhag666@shakespeare.lit', 'affiliation': 'owner'},
{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; {'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}];
delta = roomview.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); delta = u.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list);
expect(delta.length).toBe(0); expect(delta.length).toBe(0);
// With exclude_existing set to false, any changed affiliations // With exclude_existing set to false, any changed affiliations
// will be included in the delta (i.e. existing affiliations // will be included in the delta (i.e. existing affiliations
// are included in the comparison). // are included in the comparison).
old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'owner'}]; old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'owner'}];
delta = roomview.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); delta = u.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list);
expect(delta.length).toBe(1); expect(delta.length).toBe(1);
expect(delta[0].jid).toBe('wiccarocks@shakespeare.lit'); expect(delta[0].jid).toBe('wiccarocks@shakespeare.lit');
expect(delta[0].affiliation).toBe('member'); expect(delta[0].affiliation).toBe('member');
@ -3052,12 +3052,12 @@
remove_absentees = true; remove_absentees = true;
old_list = [{'jid': 'oldhag666@shakespeare.lit', 'affiliation': 'owner'}, old_list = [{'jid': 'oldhag666@shakespeare.lit', 'affiliation': 'owner'},
{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; {'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}];
delta = roomview.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); delta = u.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list);
expect(delta.length).toBe(1); expect(delta.length).toBe(1);
expect(delta[0].jid).toBe('oldhag666@shakespeare.lit'); expect(delta[0].jid).toBe('oldhag666@shakespeare.lit');
expect(delta[0].affiliation).toBe('none'); expect(delta[0].affiliation).toBe('none');
delta = roomview.computeAffiliationsDelta(exclude_existing, remove_absentees, [], old_list); delta = u.computeAffiliationsDelta(exclude_existing, remove_absentees, [], old_list);
expect(delta.length).toBe(2); expect(delta.length).toBe(2);
expect(delta[0].jid).toBe('oldhag666@shakespeare.lit'); expect(delta[0].jid).toBe('oldhag666@shakespeare.lit');
expect(delta[0].affiliation).toBe('none'); expect(delta[0].affiliation).toBe('none');
@ -3068,7 +3068,7 @@
// affiliation, we set 'exclude_existing' to true // affiliation, we set 'exclude_existing' to true
exclude_existing = true; exclude_existing = true;
old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'owner'}]; old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'owner'}];
delta = roomview.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); delta = u.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list);
expect(delta.length).toBe(0); expect(delta.length).toBe(0);
done(); done();
})); }));

View File

@ -29,7 +29,7 @@ require.config({
"emojione": "node_modules/emojione/lib/js/emojione", "emojione": "node_modules/emojione/lib/js/emojione",
"es6-promise": "node_modules/es6-promise/dist/es6-promise.auto", "es6-promise": "node_modules/es6-promise/dist/es6-promise.auto",
"eventemitter": "node_modules/otr/build/dep/eventemitter", "eventemitter": "node_modules/otr/build/dep/eventemitter",
"form-utils": "src/form-utils", "form-utils": "src/utils/form",
"i18n": "src/i18n", "i18n": "src/i18n",
"jed": "node_modules/jed/jed", "jed": "node_modules/jed/jed",
"jquery": "src/jquery-stub", "jquery": "src/jquery-stub",
@ -37,19 +37,10 @@ require.config({
"lodash.converter": "3rdparty/lodash.fp", "lodash.converter": "3rdparty/lodash.fp",
"lodash.fp": "src/lodash.fp", "lodash.fp": "src/lodash.fp",
"lodash.noconflict": "src/lodash.noconflict", "lodash.noconflict": "src/lodash.noconflict",
"muc-utils": "src/utils/muc",
"pluggable": "node_modules/pluggable.js/dist/pluggable", "pluggable": "node_modules/pluggable.js/dist/pluggable",
"polyfill": "src/polyfill", "polyfill": "src/polyfill",
"sizzle": "node_modules/sizzle/dist/sizzle", "sizzle": "node_modules/sizzle/dist/sizzle",
"strophe": "node_modules/strophe.js/strophe",
"strophe.disco": "node_modules/strophejs-plugin-disco/strophe.disco",
"strophe.ping": "node_modules/strophejs-plugin-ping/strophe.ping",
"strophe.rsm": "node_modules/strophejs-plugin-rsm/strophe.rsm",
"strophe.vcard": "node_modules/strophejs-plugin-vcard/strophe.vcard",
"text": "node_modules/text/text",
"tpl": "node_modules/lodash-template-loader/loader",
"underscore": "src/underscore-shim",
"utils": "src/utils",
"vdom-parser": "node_modules/vdom-parser/dist",
"snabbdom": "node_modules/snabbdom/dist/snabbdom", "snabbdom": "node_modules/snabbdom/dist/snabbdom",
"snabbdom-attributes": "node_modules/snabbdom/dist/snabbdom-attributes", "snabbdom-attributes": "node_modules/snabbdom/dist/snabbdom-attributes",
"snabbdom-class": "node_modules/snabbdom/dist/snabbdom-class", "snabbdom-class": "node_modules/snabbdom/dist/snabbdom-class",
@ -57,7 +48,17 @@ require.config({
"snabbdom-eventlisteners": "node_modules/snabbdom/dist/snabbdom-eventlisteners", "snabbdom-eventlisteners": "node_modules/snabbdom/dist/snabbdom-eventlisteners",
"snabbdom-props": "node_modules/snabbdom/dist/snabbdom-props", "snabbdom-props": "node_modules/snabbdom/dist/snabbdom-props",
"snabbdom-style": "node_modules/snabbdom/dist/snabbdom-style", "snabbdom-style": "node_modules/snabbdom/dist/snabbdom-style",
"strophe": "node_modules/strophe.js/strophe",
"strophe.disco": "node_modules/strophejs-plugin-disco/strophe.disco",
"strophe.ping": "node_modules/strophejs-plugin-ping/strophe.ping",
"strophe.rsm": "node_modules/strophejs-plugin-rsm/strophe.rsm",
"strophe.vcard": "node_modules/strophejs-plugin-vcard/strophe.vcard",
"text": "node_modules/text/text",
"tovnode": "node_modules/snabbdom/dist/tovnode", "tovnode": "node_modules/snabbdom/dist/tovnode",
"tpl": "node_modules/lodash-template-loader/loader",
"underscore": "src/underscore-shim",
"utils": "src/utils/core",
"vdom-parser": "node_modules/vdom-parser/dist",
"xss": "node_modules/xss/dist/xss", "xss": "node_modules/xss/dist/xss",
"xss.noconflict": "src/xss.noconflict", "xss.noconflict": "src/xss.noconflict",

View File

@ -11,6 +11,7 @@
(function (root, factory) { (function (root, factory) {
define([ define([
"converse-core", "converse-core",
"muc-utils",
"emojione", "emojione",
"tpl!add_chatroom_modal", "tpl!add_chatroom_modal",
"tpl!chatarea", "tpl!chatarea",
@ -37,6 +38,7 @@
], factory); ], factory);
}(this, function ( }(this, function (
converse, converse,
muc_utils,
emojione, emojione,
tpl_add_chatroom_modal, tpl_add_chatroom_modal,
tpl_chatarea, tpl_chatarea,
@ -627,269 +629,6 @@
this.insertIntoTextArea(ev.target.textContent); this.insertIntoTextArea(ev.target.textContent);
}, },
requestMemberList (chatroom_jid, affiliation) {
/* Send an IQ stanza to the server, asking it for the
* member-list of this room.
*
* See: http://xmpp.org/extensions/xep-0045.html#modifymember
*
* Parameters:
* (String) chatroom_jid: The JID of the chatroom for
* which the member-list is being requested
* (String) affiliation: The specific member list to
* fetch. 'admin', 'owner' or 'member'.
*
* Returns:
* A promise which resolves once the list has been
* retrieved.
*/
return new Promise((resolve, reject) => {
affiliation = affiliation || 'member';
const iq = $iq({to: chatroom_jid, type: "get"})
.c("query", {xmlns: Strophe.NS.MUC_ADMIN})
.c("item", {'affiliation': affiliation});
_converse.connection.sendIQ(iq, resolve, reject);
});
},
parseMemberListIQ (iq) {
/* Given an IQ stanza with a member list, create an array of member
* objects.
*/
return _.map(
sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq),
(item) => ({
'jid': item.getAttribute('jid'),
'affiliation': item.getAttribute('affiliation'),
})
);
},
computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
/* Given two lists of objects with 'jid', 'affiliation' and
* 'reason' properties, return a new list containing
* those objects that are new, changed or removed
* (depending on the 'remove_absentees' boolean).
*
* The affiliations for new and changed members stay the
* same, for removed members, the affiliation is set to 'none'.
*
* The 'reason' property is not taken into account when
* comparing whether affiliations have been changed.
*
* Parameters:
* (Boolean) exclude_existing: Indicates whether JIDs from
* the new list which are also in the old list
* (regardless of affiliation) should be excluded
* from the delta. One reason to do this
* would be when you want to add a JID only if it
* doesn't have *any* existing affiliation at all.
* (Boolean) remove_absentees: Indicates whether JIDs
* from the old list which are not in the new list
* should be considered removed and therefore be
* included in the delta with affiliation set
* to 'none'.
* (Array) new_list: Array containing the new affiliations
* (Array) old_list: Array containing the old affiliations
*/
const new_jids = _.map(new_list, 'jid');
const old_jids = _.map(old_list, 'jid');
// Get the new affiliations
let delta = _.map(
_.difference(new_jids, old_jids),
(jid) => new_list[_.indexOf(new_jids, jid)]
);
if (!exclude_existing) {
// Get the changed affiliations
delta = delta.concat(_.filter(new_list, function (item) {
const idx = _.indexOf(old_jids, item.jid);
if (idx >= 0) {
return item.affiliation !== old_list[idx].affiliation;
}
return false;
}));
}
if (remove_absentees) {
// Get the removed affiliations
delta = delta.concat(
_.map(
_.difference(old_jids, new_jids),
(jid) => ({'jid': jid, 'affiliation': 'none'})
)
);
}
return delta;
},
sendAffiliationIQ (chatroom_jid, affiliation, member) {
/* Send an IQ stanza specifying an affiliation change.
*
* Paremeters:
* (String) chatroom_jid: JID of the relevant room
* (String) affiliation: affiliation (could also be stored
* on the member object).
* (Object) member: Map containing the member's jid and
* optionally a reason and affiliation.
*/
return new Promise((resolve, reject) => {
const iq = $iq({to: chatroom_jid, type: "set"})
.c("query", {xmlns: Strophe.NS.MUC_ADMIN})
.c("item", {
'affiliation': member.affiliation || affiliation,
'jid': member.jid
});
if (!_.isUndefined(member.reason)) {
iq.c("reason", member.reason);
}
_converse.connection.sendIQ(iq, resolve, reject);
});
},
setAffiliation (affiliation, members) {
/* Send IQ stanzas to the server to set an affiliation for
* the provided JIDs.
*
* See: http://xmpp.org/extensions/xep-0045.html#modifymember
*
* XXX: Prosody doesn't accept multiple JIDs' affiliations
* being set in one IQ stanza, so as a workaround we send
* a separate stanza for each JID.
* Related ticket: https://prosody.im/issues/issue/795
*
* Parameters:
* (String) affiliation: The affiliation
* (Object) members: A map of jids, affiliations and
* optionally reasons. Only those entries with the
* same affiliation as being currently set will be
* considered.
*
* Returns:
* A promise which resolves and fails depending on the
* XMPP server response.
*/
members = _.filter(members, (member) =>
// We only want those members who have the right
// affiliation (or none, which implies the provided
// one).
_.isUndefined(member.affiliation) ||
member.affiliation === affiliation
);
const promises = _.map(
members,
_.partial(this.sendAffiliationIQ, this.model.get('jid'), affiliation)
);
return Promise.all(promises);
},
setAffiliations (members) {
/* Send IQ stanzas to the server to modify the
* affiliations in this room.
*
* See: http://xmpp.org/extensions/xep-0045.html#modifymember
*
* Parameters:
* (Object) members: A map of jids, affiliations and optionally reasons
* (Function) onSuccess: callback for a succesful response
* (Function) onError: callback for an error response
*/
const affiliations = _.uniq(_.map(members, 'affiliation'));
_.each(affiliations, _.partial(this.setAffiliation.bind(this), _, members));
},
marshallAffiliationIQs () {
/* Marshall a list of IQ stanzas into a map of JIDs and
* affiliations.
*
* Parameters:
* Any amount of XMLElement objects, representing the IQ
* stanzas.
*/
return _.flatMap(arguments[0], this.parseMemberListIQ);
},
getJidsWithAffiliations (affiliations) {
/* Returns a map of JIDs that have the affiliations
* as provided.
*/
if (_.isString(affiliations)) {
affiliations = [affiliations];
}
return new Promise((resolve, reject) => {
const promises = _.map(
affiliations,
_.partial(this.requestMemberList, this.model.get('jid'))
);
Promise.all(promises).then(
_.flow(this.marshallAffiliationIQs.bind(this), resolve),
_.flow(this.marshallAffiliationIQs.bind(this), resolve)
);
});
},
updateMemberLists (members, affiliations, deltaFunc) {
/* Fetch the lists of users with the given affiliations.
* Then compute the delta between those users and
* the passed in members, and if it exists, send the delta
* to the XMPP server to update the member list.
*
* Parameters:
* (Object) members: Map of member jids and affiliations.
* (String|Array) affiliation: An array of affiliations or
* a string if only one affiliation.
* (Function) deltaFunc: The function to compute the delta
* between old and new member lists.
*
* Returns:
* A promise which is resolved once the list has been
* updated or once it's been established there's no need
* to update the list.
*/
this.getJidsWithAffiliations(affiliations).then((old_members) => {
this.setAffiliations(deltaFunc(members, old_members));
});
},
directInvite (recipient, reason) {
/* Send a direct invitation as per XEP-0249
*
* Parameters:
* (String) recipient - JID of the person being invited
* (String) reason - Optional reason for the invitation
*/
if (this.model.get('membersonly')) {
// When inviting to a members-only room, we first add
// the person to the member list by giving them an
// affiliation of 'member' (if they're not affiliated
// already), otherwise they won't be able to join.
const map = {}; map[recipient] = 'member';
const deltaFunc = _.partial(this.computeAffiliationsDelta, true, false);
this.updateMemberLists(
[{'jid': recipient, 'affiliation': 'member', 'reason': reason}],
['member', 'owner', 'admin'],
deltaFunc
);
}
const attrs = {
'xmlns': 'jabber:x:conference',
'jid': this.model.get('jid')
};
if (reason !== null) { attrs.reason = reason; }
if (this.model.get('password')) { attrs.password = this.model.get('password'); }
const invitation = $msg({
from: _converse.connection.jid,
to: recipient,
id: _converse.connection.getUniqueId()
}).c('x', attrs);
_converse.connection.send(invitation);
_converse.emit('roomInviteSent', {
'room': this,
'recipient': recipient,
'reason': reason
});
},
handleChatStateMessage (message) { handleChatStateMessage (message) {
/* Override the method on the ChatBoxView base class to /* Override the method on the ChatBoxView base class to
* ignore <gone/> notifications in groupchats. * ignore <gone/> notifications in groupchats.
@ -1009,14 +748,14 @@
switch (command) { switch (command) {
case 'admin': case 'admin':
if (!this.validateRoleChangeCommand(command, args)) { break; } if (!this.validateRoleChangeCommand(command, args)) { break; }
this.setAffiliation('admin', this.model.setAffiliation('admin',
[{ 'jid': args[0], [{ 'jid': args[0],
'reason': args[1] 'reason': args[1]
}]).then(null, this.onCommandError.bind(this)); }]).then(null, this.onCommandError.bind(this));
break; break;
case 'ban': case 'ban':
if (!this.validateRoleChangeCommand(command, args)) { break; } if (!this.validateRoleChangeCommand(command, args)) { break; }
this.setAffiliation('outcast', this.model.setAffiliation('outcast',
[{ 'jid': args[0], [{ 'jid': args[0],
'reason': args[1] 'reason': args[1]
}]).then(null, this.onCommandError.bind(this)); }]).then(null, this.onCommandError.bind(this));
@ -1064,7 +803,7 @@
break; break;
case 'member': case 'member':
if (!this.validateRoleChangeCommand(command, args)) { break; } if (!this.validateRoleChangeCommand(command, args)) { break; }
this.setAffiliation('member', this.model.setAffiliation('member',
[{ 'jid': args[0], [{ 'jid': args[0],
'reason': args[1] 'reason': args[1]
}]).then(null, this.onCommandError.bind(this)); }]).then(null, this.onCommandError.bind(this));
@ -1078,7 +817,7 @@
break; break;
case 'owner': case 'owner':
if (!this.validateRoleChangeCommand(command, args)) { break; } if (!this.validateRoleChangeCommand(command, args)) { break; }
this.setAffiliation('owner', this.model.setAffiliation('owner',
[{ 'jid': args[0], [{ 'jid': args[0],
'reason': args[1] 'reason': args[1]
}]).then(null, this.onCommandError.bind(this)); }]).then(null, this.onCommandError.bind(this));
@ -1091,7 +830,7 @@
break; break;
case 'revoke': case 'revoke':
if (!this.validateRoleChangeCommand(command, args)) { break; } if (!this.validateRoleChangeCommand(command, args)) { break; }
this.setAffiliation('none', this.model.setAffiliation('none',
[{ 'jid': args[0], [{ 'jid': args[0],
'reason': args[1] 'reason': args[1]
}]).then(null, this.onCommandError.bind(this)); }]).then(null, this.onCommandError.bind(this));
@ -1640,27 +1379,6 @@
return; return;
}, },
saveAffiliationAndRole (pres) {
/* Parse the presence stanza for the current user's
* affiliation.
*
* Parameters:
* (XMLElement) pres: A <presence> stanza.
*/
const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, pres).pop();
const is_self = pres.querySelector("status[code='110']");
if (is_self && !_.isNil(item)) {
const affiliation = item.getAttribute('affiliation');
const role = item.getAttribute('role');
if (affiliation) {
this.model.save({'affiliation': affiliation});
}
if (role) {
this.model.save({'role': role});
}
}
},
parseXUserElement (x, stanza, is_self) { parseXUserElement (x, stanza, is_self) {
/* Parse the passed-in <x xmlns='http://jabber.org/protocol/muc#user'> /* Parse the passed-in <x xmlns='http://jabber.org/protocol/muc#user'>
* element and construct a map containing relevant * element and construct a map containing relevant
@ -1944,7 +1662,7 @@
* Parameters: * Parameters:
* (XMLElement) pres: The stanza * (XMLElement) pres: The stanza
*/ */
this.saveAffiliationAndRole(pres); this.model.saveAffiliationAndRole(pres);
const locked_room = pres.querySelector("status[code='201']"); const locked_room = pres.querySelector("status[code='201']");
if (locked_room) { if (locked_room) {
@ -2409,7 +2127,7 @@
suggestion.text.label, this.model.get('id')) suggestion.text.label, this.model.get('id'))
); );
if (reason !== null) { if (reason !== null) {
this.chatroomview.directInvite(suggestion.text.value, reason); this.chatroomview.model.directInvite(suggestion.text.value, reason);
} }
const form = suggestion.target.form, const form = suggestion.target.form,
error = form.querySelector('.pure-form-message.error'); error = form.querySelector('.pure-form-message.error');

View File

@ -17,7 +17,8 @@
"converse-disco", "converse-disco",
"backbone.overview", "backbone.overview",
"backbone.orderedlistview", "backbone.orderedlistview",
"backbone.vdomview" "backbone.vdomview",
"muc-utils"
], factory); ], factory);
}(this, function (u, converse) { }(this, function (u, converse) {
"use strict"; "use strict";
@ -56,6 +57,7 @@
ENTERED: 5 ENTERED: 5
}; };
converse.plugins.add('converse-muc', { converse.plugins.add('converse-muc', {
/* Optional dependencies are other plugins which might be /* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before * overridden or relied upon, and therefore need to be loaded before
@ -273,6 +275,46 @@
); );
}, },
directInvite (recipient, reason) {
/* Send a direct invitation as per XEP-0249
*
* Parameters:
* (String) recipient - JID of the person being invited
* (String) reason - Optional reason for the invitation
*/
if (this.get('membersonly')) {
// When inviting to a members-only room, we first add
// the person to the member list by giving them an
// affiliation of 'member' (if they're not affiliated
// already), otherwise they won't be able to join.
const map = {}; map[recipient] = 'member';
const deltaFunc = _.partial(u.computeAffiliationsDelta, true, false);
this.updateMemberLists(
[{'jid': recipient, 'affiliation': 'member', 'reason': reason}],
['member', 'owner', 'admin'],
deltaFunc
);
}
const attrs = {
'xmlns': 'jabber:x:conference',
'jid': this.get('jid')
};
if (reason !== null) { attrs.reason = reason; }
if (this.get('password')) { attrs.password = this.get('password'); }
const invitation = $msg({
from: _converse.connection.jid,
to: recipient,
id: _converse.connection.getUniqueId()
}).c('x', attrs);
_converse.connection.send(invitation);
_converse.emit('roomInviteSent', {
'room': this,
'recipient': recipient,
'reason': reason
});
},
sendConfiguration (config, callback, errback) { sendConfiguration (config, callback, errback) {
/* Send an IQ stanza with the room configuration. /* Send an IQ stanza with the room configuration.
* *
@ -335,6 +377,162 @@
this.save(features); this.save(features);
}, },
requestMemberList (affiliation) {
/* Send an IQ stanza to the server, asking it for the
* member-list of this room.
*
* See: http://xmpp.org/extensions/xep-0045.html#modifymember
*
* Parameters:
* (String) affiliation: The specific member list to
* fetch. 'admin', 'owner' or 'member'.
*
* Returns:
* A promise which resolves once the list has been
* retrieved.
*/
return new Promise((resolve, reject) => {
affiliation = affiliation || 'member';
const iq = $iq({to: this.get('jid'), type: "get"})
.c("query", {xmlns: Strophe.NS.MUC_ADMIN})
.c("item", {'affiliation': affiliation});
_converse.connection.sendIQ(iq, resolve, reject);
});
},
setAffiliation (affiliation, members) {
/* Send IQ stanzas to the server to set an affiliation for
* the provided JIDs.
*
* See: http://xmpp.org/extensions/xep-0045.html#modifymember
*
* XXX: Prosody doesn't accept multiple JIDs' affiliations
* being set in one IQ stanza, so as a workaround we send
* a separate stanza for each JID.
* Related ticket: https://prosody.im/issues/issue/795
*
* Parameters:
* (String) affiliation: The affiliation
* (Object) members: A map of jids, affiliations and
* optionally reasons. Only those entries with the
* same affiliation as being currently set will be
* considered.
*
* Returns:
* A promise which resolves and fails depending on the
* XMPP server response.
*/
members = _.filter(members, (member) =>
// We only want those members who have the right
// affiliation (or none, which implies the provided one).
_.isUndefined(member.affiliation) ||
member.affiliation === affiliation
);
const promises = _.map(members, _.bind(this.sendAffiliationIQ, this, affiliation));
return Promise.all(promises);
},
saveAffiliationAndRole (pres) {
/* Parse the presence stanza for the current user's
* affiliation.
*
* Parameters:
* (XMLElement) pres: A <presence> stanza.
*/
const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, pres).pop();
const is_self = pres.querySelector("status[code='110']");
if (is_self && !_.isNil(item)) {
const affiliation = item.getAttribute('affiliation');
const role = item.getAttribute('role');
if (affiliation) {
this.save({'affiliation': affiliation});
}
if (role) {
this.save({'role': role});
}
}
},
sendAffiliationIQ (affiliation, member) {
/* Send an IQ stanza specifying an affiliation change.
*
* Paremeters:
* (String) affiliation: affiliation (could also be stored
* on the member object).
* (Object) member: Map containing the member's jid and
* optionally a reason and affiliation.
*/
return new Promise((resolve, reject) => {
const iq = $iq({to: this.get('jid'), type: "set"})
.c("query", {xmlns: Strophe.NS.MUC_ADMIN})
.c("item", {
'affiliation': member.affiliation || affiliation,
'jid': member.jid
});
if (!_.isUndefined(member.reason)) {
iq.c("reason", member.reason);
}
_converse.connection.sendIQ(iq, resolve, reject);
});
},
setAffiliations (members) {
/* Send IQ stanzas to the server to modify the
* affiliations in this room.
*
* See: http://xmpp.org/extensions/xep-0045.html#modifymember
*
* Parameters:
* (Object) members: A map of jids, affiliations and optionally reasons
* (Function) onSuccess: callback for a succesful response
* (Function) onError: callback for an error response
*/
const affiliations = _.uniq(_.map(members, 'affiliation'));
_.each(affiliations, _.partial(this.setAffiliation.bind(this), _, members));
},
getJidsWithAffiliations (affiliations) {
/* Returns a map of JIDs that have the affiliations
* as provided.
*/
if (_.isString(affiliations)) {
affiliations = [affiliations];
}
return new Promise((resolve, reject) => {
const promises = _.map(
affiliations,
_.partial(this.requestMemberList.bind(this))
);
Promise.all(promises).then(
_.flow(u.marshallAffiliationIQs, resolve),
_.flow(u.marshallAffiliationIQs, resolve)
);
});
},
updateMemberLists (members, affiliations, deltaFunc) {
/* Fetch the lists of users with the given affiliations.
* Then compute the delta between those users and
* the passed in members, and if it exists, send the delta
* to the XMPP server to update the member list.
*
* Parameters:
* (Object) members: Map of member jids and affiliations.
* (String|Array) affiliation: An array of affiliations or
* a string if only one affiliation.
* (Function) deltaFunc: The function to compute the delta
* between old and new member lists.
*
* Returns:
* A promise which is resolved once the list has been
* updated or once it's been established there's no need
* to update the list.
*/
this.getJidsWithAffiliations(affiliations).then((old_members) => {
this.setAffiliations(deltaFunc(members, old_members));
});
},
checkForReservedNick (callback, errback) { checkForReservedNick (callback, errback) {
/* Use service-discovery to ask the XMPP server whether /* Use service-discovery to ask the XMPP server whether
* this user has a reserved nickname for this room. * this user has a reserved nickname for this room.

98
src/utils/muc.js Normal file
View File

@ -0,0 +1,98 @@
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// This is the utilities module.
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define, escape, Jed */
(function (root, factory) {
define(["converse-core", "utils"], factory);
}(this, function (converse, u) {
"use strict";
const { Strophe, sizzle, _ } = converse.env;
u.computeAffiliationsDelta = function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
/* Given two lists of objects with 'jid', 'affiliation' and
* 'reason' properties, return a new list containing
* those objects that are new, changed or removed
* (depending on the 'remove_absentees' boolean).
*
* The affiliations for new and changed members stay the
* same, for removed members, the affiliation is set to 'none'.
*
* The 'reason' property is not taken into account when
* comparing whether affiliations have been changed.
*
* Parameters:
* (Boolean) exclude_existing: Indicates whether JIDs from
* the new list which are also in the old list
* (regardless of affiliation) should be excluded
* from the delta. One reason to do this
* would be when you want to add a JID only if it
* doesn't have *any* existing affiliation at all.
* (Boolean) remove_absentees: Indicates whether JIDs
* from the old list which are not in the new list
* should be considered removed and therefore be
* included in the delta with affiliation set
* to 'none'.
* (Array) new_list: Array containing the new affiliations
* (Array) old_list: Array containing the old affiliations
*/
const new_jids = _.map(new_list, 'jid');
const old_jids = _.map(old_list, 'jid');
// Get the new affiliations
let delta = _.map(
_.difference(new_jids, old_jids),
(jid) => new_list[_.indexOf(new_jids, jid)]
);
if (!exclude_existing) {
// Get the changed affiliations
delta = delta.concat(_.filter(new_list, function (item) {
const idx = _.indexOf(old_jids, item.jid);
if (idx >= 0) {
return item.affiliation !== old_list[idx].affiliation;
}
return false;
}));
}
if (remove_absentees) {
// Get the removed affiliations
delta = delta.concat(
_.map(
_.difference(old_jids, new_jids),
(jid) => ({'jid': jid, 'affiliation': 'none'})
)
);
}
return delta;
};
u.parseMemberListIQ = function parseMemberListIQ (iq) {
/* Given an IQ stanza with a member list, create an array of member
* objects.
*/
return _.map(
sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq),
(item) => ({
'jid': item.getAttribute('jid'),
'affiliation': item.getAttribute('affiliation'),
})
);
};
u.marshallAffiliationIQs = function marshallAffiliationIQs () {
/* Marshall a list of IQ stanzas into a map of JIDs and
* affiliations.
*
* Parameters:
* Any amount of XMLElement objects, representing the IQ
* stanzas.
*/
return _.flatMap(arguments[0], u.parseMemberListIQ);
}
}));