muc: Store room configuration (e.g. disco#info fields
) on the MUC
This will make it easier to add config-based functionality, such as allowing/showing the `/topic` slash command only to those users who are allowed to set the subject.
This commit is contained in:
parent
929a00e1cd
commit
aa86a8be32
36
spec/muc.js
36
spec/muc.js
@ -102,10 +102,10 @@
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
// Mock 'getRoomFeatures', otherwise the room won't be
|
||||
// Mock 'getDiscoInfo', otherwise the room won't be
|
||||
// displayed as it waits first for the features to be returned
|
||||
// (when it's a new room being created).
|
||||
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve());
|
||||
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
|
||||
|
||||
let jid = 'lounge@montague.lit';
|
||||
let chatroomview, IQ_id;
|
||||
@ -3009,13 +3009,13 @@
|
||||
textarea.value = '/help';
|
||||
view.onKeyDown(enter);
|
||||
info_messages = sizzle('.chat-info:not(.chat-event)', view.el);
|
||||
expect(info_messages.length).toBe(19);
|
||||
expect(info_messages.length).toBe(17);
|
||||
let commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
||||
expect(commands).toEqual([
|
||||
"You can run the following commands",
|
||||
"/admin", "/ban", "/clear", "/deop", "/destroy",
|
||||
"/help", "/kick", "/me", "/member", "/modtools", "/mute", "/nick",
|
||||
"/op", "/register", "/revoke", "/subject", "/topic", "/voice"
|
||||
"/op", "/register", "/revoke", "/voice"
|
||||
]);
|
||||
occupant.set('affiliation', 'member');
|
||||
textarea.value = '/clear';
|
||||
@ -3025,9 +3025,9 @@
|
||||
textarea.value = '/help';
|
||||
view.onKeyDown(enter);
|
||||
info_messages = sizzle('.chat-info', view.el).slice(1);
|
||||
expect(info_messages.length).toBe(11);
|
||||
expect(info_messages.length).toBe(9);
|
||||
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
||||
expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/subject", "/topic", "/voice"]);
|
||||
expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/voice"]);
|
||||
|
||||
occupant.set('role', 'participant');
|
||||
textarea = view.el.querySelector('.chat-textarea');
|
||||
@ -3035,6 +3035,20 @@
|
||||
view.onKeyDown(enter);
|
||||
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0);
|
||||
|
||||
textarea.value = '/help';
|
||||
view.onKeyDown(enter);
|
||||
info_messages = sizzle('.chat-info', view.el).slice(1);
|
||||
expect(info_messages.length).toBe(5);
|
||||
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
|
||||
expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register"]);
|
||||
|
||||
// Test that /topic is available if all users may change the subject
|
||||
// Note: we're making a shortcut here, this value should never be set manually
|
||||
view.model.config.set('changesubject', true);
|
||||
textarea.value = '/clear';
|
||||
view.onKeyDown(enter);
|
||||
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0);
|
||||
|
||||
textarea.value = '/help';
|
||||
view.onKeyDown(enter);
|
||||
info_messages = sizzle('.chat-info', view.el).slice(1);
|
||||
@ -4572,7 +4586,7 @@
|
||||
nick_input.value = 'romeo';
|
||||
|
||||
expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
|
||||
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve());
|
||||
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
|
||||
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
|
||||
modal.el.querySelector('input[name="chatroom"]').value = 'lounce@muc.montague.lit';
|
||||
modal.el.querySelector('form input[type="submit"]').click();
|
||||
@ -4660,7 +4674,7 @@
|
||||
const modal = roomspanel.add_room_modal;
|
||||
await u.waitUntil(() => u.isVisible(modal.el), 1000)
|
||||
expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
|
||||
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve());
|
||||
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
|
||||
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
|
||||
const label_name = modal.el.querySelector('label[for="chatroom"]');
|
||||
expect(label_name.textContent.trim()).toBe('Groupchat name:');
|
||||
@ -4700,7 +4714,7 @@
|
||||
const modal = roomspanel.add_room_modal;
|
||||
await u.waitUntil(() => u.isVisible(modal.el), 1000)
|
||||
expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
|
||||
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve());
|
||||
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
|
||||
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
|
||||
const label_name = modal.el.querySelector('label[for="chatroom"]');
|
||||
expect(label_name.textContent.trim()).toBe('Groupchat name:');
|
||||
@ -4742,7 +4756,7 @@
|
||||
test_utils.closeControlBox(_converse);
|
||||
const modal = roomspanel.list_rooms_modal;
|
||||
await u.waitUntil(() => u.isVisible(modal.el), 1000);
|
||||
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve());
|
||||
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
|
||||
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
|
||||
|
||||
// See: https://xmpp.org/extensions/xep-0045.html#disco-rooms
|
||||
@ -4836,7 +4850,7 @@
|
||||
test_utils.closeControlBox(_converse);
|
||||
const modal = roomspanel.list_rooms_modal;
|
||||
await u.waitUntil(() => u.isVisible(modal.el), 1000);
|
||||
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve());
|
||||
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
|
||||
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
|
||||
|
||||
expect(modal.el.querySelector('input[name="server"]')).toBe(null);
|
||||
|
@ -625,6 +625,7 @@ converse.plugins.add('converse-muc-views', {
|
||||
this.model.toJSON(), {
|
||||
'_': _,
|
||||
'__': __,
|
||||
'config': this.model.config.toJSON(),
|
||||
'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),
|
||||
'features': this.model.features.toJSON(),
|
||||
'num_occupants': this.model.occupants.length,
|
||||
@ -1126,7 +1127,7 @@ converse.plugins.add('converse-muc-views', {
|
||||
'info_close': __('Close and leave this groupchat'),
|
||||
'info_configure': __('Configure this groupchat'),
|
||||
'info_details': __('Show more details about this groupchat'),
|
||||
'description': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})),
|
||||
'subject': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})),
|
||||
}));
|
||||
},
|
||||
|
||||
@ -1365,7 +1366,10 @@ converse.plugins.add('converse-muc-views', {
|
||||
|
||||
onCommandError (err) {
|
||||
log.fatal(err);
|
||||
this.showErrorMessage(__("Sorry, an error happened while running the command. Check your browser's developer console for details."));
|
||||
this.showErrorMessage(
|
||||
__("Sorry, an error happened while running the command.") + " " +
|
||||
__("Check your browser's developer console for details.")
|
||||
);
|
||||
},
|
||||
|
||||
getAllowedCommands () {
|
||||
@ -2063,9 +2067,20 @@ converse.plugins.add('converse-muc-views', {
|
||||
});
|
||||
},
|
||||
|
||||
submitConfigForm (ev) {
|
||||
async submitConfigForm (ev) {
|
||||
ev.preventDefault();
|
||||
this.model.saveConfiguration(ev.target).then(() => this.model.refreshRoomFeatures());
|
||||
const inputs = sizzle(':input:not([type=button]):not([type=submit])', ev.target);
|
||||
const configArray = inputs.map(u.webForm2xForm);
|
||||
try {
|
||||
await this.model.sendConfiguration(configArray);
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
this.showErrorMessage(
|
||||
__("Sorry, an error occurred while trying to submit the config form.") + " " +
|
||||
__("Check your browser's developer console for details.")
|
||||
);
|
||||
}
|
||||
await this.model.refreshDiscoInfo();
|
||||
this.chatroomview.closeForm();
|
||||
},
|
||||
|
||||
|
@ -692,18 +692,17 @@ converse.plugins.add('converse-disco', {
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the features (and fields and identities) associated with a
|
||||
* Refresh the features, fields and identities associated with a
|
||||
* disco entity by refetching them from the server
|
||||
*
|
||||
* @method _converse.api.disco.refreshFeatures
|
||||
* @method _converse.api.disco.refresh
|
||||
* @param {string} jid The JID of the entity whose features are refreshed.
|
||||
* @returns {promise} A promise which resolves once the features have been refreshed
|
||||
* @example
|
||||
* await _converse.api.disco.refreshFeatures('room@conference.example.org');
|
||||
* await _converse.api.disco.refresh('room@conference.example.org');
|
||||
*/
|
||||
async refreshFeatures (jid) {
|
||||
async refresh (jid) {
|
||||
if (!jid) {
|
||||
throw new TypeError('api.disco.refreshFeatures: You need to provide an entity JID');
|
||||
throw new TypeError('api.disco.refresh: You need to provide an entity JID');
|
||||
}
|
||||
await _converse.api.waitUntil('discoInitialized');
|
||||
let entity = await _converse.api.disco.entities.get(jid);
|
||||
@ -722,6 +721,14 @@ converse.plugins.add('converse-disco', {
|
||||
return entity.waitUntilFeaturesDiscovered;
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link _converse.api.disco.refresh} instead.
|
||||
* @method _converse.api.disco.refreshFeatures
|
||||
*/
|
||||
refreshFeatures (jid) {
|
||||
return _converse.api.refresh(jid);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return all the features associated with a disco entity
|
||||
*
|
||||
|
@ -337,7 +337,6 @@ converse.plugins.add('converse-muc', {
|
||||
|
||||
'bookmarked': false,
|
||||
'chat_state': undefined,
|
||||
'description': '',
|
||||
'hidden': ['mobile', 'fullscreen'].includes(_converse.view_mode),
|
||||
'message_type': 'groupchat',
|
||||
'name': '',
|
||||
@ -354,7 +353,7 @@ converse.plugins.add('converse-muc', {
|
||||
this.set('box_id', `box-${btoa(this.get('jid'))}`);
|
||||
this.initMessages();
|
||||
this.initOccupants();
|
||||
this.initFeatures(); // sendChatState depends on this.features
|
||||
this.initDiscoModels(); // sendChatState depends on this.features
|
||||
this.registerHandlers();
|
||||
|
||||
this.on('change:chat_state', this.sendChatState, this);
|
||||
@ -407,7 +406,7 @@ converse.plugins.add('converse-muc', {
|
||||
// so we don't send out a presence stanza again.
|
||||
return this;
|
||||
}
|
||||
await this.refreshRoomFeatures();
|
||||
await this.refreshDiscoInfo();
|
||||
nick = await this.getAndPersistNickname(nick);
|
||||
if (!nick) {
|
||||
u.safeSave(this.session, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
|
||||
@ -488,12 +487,16 @@ converse.plugins.add('converse-muc', {
|
||||
return new Promise(r => this.session.fetch({'success': r, 'error': r}));
|
||||
},
|
||||
|
||||
initFeatures () {
|
||||
const id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
|
||||
initDiscoModels () {
|
||||
let id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
|
||||
this.features = new Backbone.Model(
|
||||
Object.assign({id}, zipObject(converse.ROOM_FEATURES, converse.ROOM_FEATURES.map(() => false)))
|
||||
);
|
||||
this.features.browserStorage = _converse.createStore(id, "session");
|
||||
|
||||
id = `converse.muc-config-{_converse.bare_jid}-${this.get('jid')}`;
|
||||
this.config = new Backbone.Model();
|
||||
this.config.browserStorage = _converse.createStore(id, "session");
|
||||
},
|
||||
|
||||
initOccupants () {
|
||||
@ -917,10 +920,10 @@ converse.plugins.add('converse-muc', {
|
||||
* After the user has sent out a direct invitation (as per XEP-0249),
|
||||
* to a roster contact, asking them to join a room.
|
||||
* @event _converse#chatBoxMaximized
|
||||
* @type { object }
|
||||
* @property { _converse.ChatRoom } room
|
||||
* @property { string } recipient - The JID of the person being invited
|
||||
* @property { string } reason - The original reason for the invitation
|
||||
* @type {object}
|
||||
* @property {_converse.ChatRoom} room
|
||||
* @property {string} recipient - The JID of the person being invited
|
||||
* @property {string} reason - The original reason for the invitation
|
||||
* @example _converse.api.listen.on('chatBoxMaximized', view => { ... });
|
||||
*/
|
||||
_converse.api.trigger('roomInviteSent', {
|
||||
@ -930,26 +933,63 @@ converse.plugins.add('converse-muc', {
|
||||
});
|
||||
},
|
||||
|
||||
async refreshRoomFeatures () {
|
||||
await _converse.api.disco.refreshFeatures(this.get('jid'));
|
||||
return this.getRoomFeatures();
|
||||
/**
|
||||
* Refresh the disco identity, features and fields for this {@link _converse.ChatRoom}.
|
||||
* *features* are stored on the features {@link Model} attribute on this {@link _converse.ChatRoom}.
|
||||
* *fields* are stored on the config {@link Model} attribute on this {@link _converse.ChatRoom}.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
refreshDiscoInfo () {
|
||||
return _converse.api.disco.refresh(this.get('jid'))
|
||||
.then(() => this.getDiscoInfo())
|
||||
.catch(e => log.error(e));
|
||||
},
|
||||
|
||||
async getRoomFeatures () {
|
||||
let identity;
|
||||
try {
|
||||
identity = await _converse.api.disco.getIdentity('conference', 'text', this.get('jid'));
|
||||
} catch (e) {
|
||||
// Getting the identity probably failed because this room doesn't exist yet.
|
||||
return log.error(e);
|
||||
}
|
||||
const fields = await _converse.api.disco.getFields(this.get('jid'));
|
||||
this.save({
|
||||
'name': identity && identity.get('name'),
|
||||
'description': get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value')
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Fetch the *extended* MUC info from the server and cache it locally
|
||||
* https://xmpp.org/extensions/xep-0045.html#disco-roominfo
|
||||
* @private
|
||||
* @method _converse.ChatRoom#getDiscoInfo
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getDiscoInfo () {
|
||||
return _converse.api.disco.getIdentity('conference', 'text', this.get('jid'))
|
||||
.then(identity => this.save({'name': identity && identity.get('name')}))
|
||||
.then(() => this.getDiscoInfoFields())
|
||||
.then(() => this.getDiscoInfoFeatures())
|
||||
.catch(e => log.error(e));
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the *extended* MUC info fields from the server and store them locally
|
||||
* in the `config` {@link Model} attribute.
|
||||
* See: https://xmpp.org/extensions/xep-0045.html#disco-roominfo
|
||||
* @private
|
||||
* @method _converse.ChatRoom#getDiscoInfoFields
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async getDiscoInfoFields () {
|
||||
const fields = await _converse.api.disco.getFields(this.get('jid'));
|
||||
const config = fields.reduce((config, f) => {
|
||||
const name = f.get('var');
|
||||
if (name && name.startsWith('muc#roominfo_')) {
|
||||
config[name.replace('muc#roominfo_', '')] = f.get('value');
|
||||
}
|
||||
return config;
|
||||
}, {});
|
||||
this.config.save(config);
|
||||
},
|
||||
|
||||
/**
|
||||
* Use converse-disco to populate the features {@link Model} which
|
||||
* is stored as an attibute on this {@link _converse.ChatRoom}.
|
||||
* The results may be cached. If you want to force fetching the features from the
|
||||
* server, call {@link _converse.ChatRoom#refreshDiscoInfo} instead.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async getDiscoInfoFeatures () {
|
||||
const features = await _converse.api.disco.getFeatures(this.get('jid'));
|
||||
const attrs = Object.assign(
|
||||
zipObject(converse.ROOM_FEATURES, converse.ROOM_FEATURES.map(() => false)),
|
||||
@ -965,7 +1005,6 @@ converse.plugins.add('converse-muc', {
|
||||
}
|
||||
attrs[fieldname.replace('muc_', '')] = true;
|
||||
});
|
||||
attrs.description = get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value');
|
||||
this.features.save(attrs);
|
||||
},
|
||||
|
||||
@ -992,24 +1031,6 @@ converse.plugins.add('converse-muc', {
|
||||
return Promise.all(members.map(m => this.sendAffiliationIQ(affiliation, m)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit the groupchat configuration form by sending an IQ
|
||||
* stanza to the server.
|
||||
* @private
|
||||
* @method _converse.ChatRoom#saveConfiguration
|
||||
* @param { HTMLElement } form - The configuration form DOM element.
|
||||
* If no form is provided, the default configuration
|
||||
* values will be used.
|
||||
* @returns { Promise<XMLElement> }
|
||||
* Returns a promise which resolves once the XMPP server
|
||||
* has return a response IQ.
|
||||
*/
|
||||
saveConfiguration (form) {
|
||||
const inputs = form ? sizzle(':input:not([type=button]):not([type=submit])', form) : [];
|
||||
const configArray = inputs.map(u.webForm2xForm);
|
||||
return this.sendConfiguration(configArray);
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a <field> element, return a copy with a <value> child if
|
||||
* we can find a value for it in this rooms config.
|
||||
@ -1433,7 +1454,7 @@ converse.plugins.add('converse-muc', {
|
||||
// 174: room now fully anonymous
|
||||
const codes = ['104', '170', '171', '172', '173', '174'];
|
||||
if (sizzle('status', stanza).filter(e => codes.includes(e.getAttribute('status'))).length) {
|
||||
this.refreshRoomFeatures();
|
||||
this.refreshDiscoInfo();
|
||||
}
|
||||
},
|
||||
|
||||
@ -1983,10 +2004,10 @@ converse.plugins.add('converse-muc', {
|
||||
const locked_room = stanza.querySelector("status[code='201']");
|
||||
if (locked_room) {
|
||||
if (this.get('auto_configure')) {
|
||||
this.autoConfigureChatRoom().then(() => this.refreshRoomFeatures());
|
||||
this.autoConfigureChatRoom().then(() => this.refreshDiscoInfo());
|
||||
} else if (_converse.muc_instant_rooms) {
|
||||
// Accept default configuration
|
||||
this.saveConfiguration().then(() => this.refreshRoomFeatures());
|
||||
this.sendConfiguration().then(() => this.refreshDiscoInfo());
|
||||
} else {
|
||||
/**
|
||||
* Triggered when a new room has been created which first needs to be configured
|
||||
@ -2006,9 +2027,9 @@ converse.plugins.add('converse-muc', {
|
||||
// otherwise the features would have been fetched in
|
||||
// the "initialize" method already.
|
||||
if (this.getOwnAffiliation() === 'owner' && this.get('auto_configure')) {
|
||||
this.autoConfigureChatRoom().then(() => this.refreshRoomFeatures());
|
||||
this.autoConfigureChatRoom().then(() => this.refreshDiscoInfo());
|
||||
} else {
|
||||
this.getRoomFeatures();
|
||||
this.getDiscoInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
<div class="room-info">
|
||||
<p class="room-info"><strong>{{{o.__('Name')}}}</strong>: {{{o.name}}}</p>
|
||||
<p class="room-info"><strong>{{{o.__('Groupchat address (JID)')}}}</strong>: {{{o.jid}}}</p>
|
||||
<p class="room-info"><strong>{{{o.__('Description')}}}</strong>: {{{o.description}}}</p>
|
||||
<p class="room-info"><strong>{{{o.__('Description')}}}</strong>: {{{o.config.description}}}</p>
|
||||
{[ if (o.subject) { ]}
|
||||
<p class="room-info"><strong>{{{o.__('Topic')}}}</strong>: {{o.topic}}</p> <!-- Sanitized in converse-muc-views. We want to render links. -->
|
||||
<p class="room-info"><strong>{{{o.__('Topic author')}}}</strong>: {{{o._.get(o.subject, 'author')}}}</p>
|
||||
|
@ -14,4 +14,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sanitized in converse-muc-views. We want to render links. -->
|
||||
<p class="chat-head__desc">{{o.description}}</p>
|
||||
<p class="chat-head__desc">{{o.subject}}</p>
|
||||
|
@ -28,7 +28,7 @@
|
||||
persistent_store: 'IndexedDB',
|
||||
muc_domain: 'conference.chat.example.org',
|
||||
muc_respect_autojoin: true,
|
||||
view_mode: 'overlayed',
|
||||
view_mode: 'fullscreen',
|
||||
websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
|
||||
// bosh_service_url: 'http://chat.example.org:5280/http-bind',
|
||||
muc_show_logs_before_join: true,
|
||||
|
Loading…
Reference in New Issue
Block a user