xmpp.chapril.org-conversejs/src/converse-muc.js
2018-03-13 13:42:00 +01:00

557 lines
25 KiB
JavaScript

// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
/* This is a Converse.js plugin which add support for multi-user chat rooms, as
* specified in XEP-0045 Multi-user chat.
*/
(function (root, factory) {
define([
"form-utils",
"converse-core",
"converse-chatview",
"converse-disco",
"backbone.overview",
"backbone.orderedlistview",
"backbone.vdomview"
], factory);
}(this, function (u, converse) {
"use strict";
const MUC_ROLE_WEIGHTS = {
'moderator': 1,
'participant': 2,
'visitor': 3,
'none': 4,
};
const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, b64_sha1, sizzle, _, moment } = converse.env;
// Add Strophe Namespaces
Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner");
Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register");
Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
converse.CHATROOMS_TYPE = 'chatroom';
converse.ROOM_FEATURES = [
'passwordprotected', 'unsecured', 'hidden',
'publicroom', 'membersonly', 'open', 'persistent',
'temporary', 'nonanonymous', 'semianonymous',
'moderated', 'unmoderated', 'mam_enabled'
];
converse.ROOMSTATUS = {
CONNECTED: 0,
CONNECTING: 1,
NICKNAME_REQUIRED: 2,
PASSWORD_REQUIRED: 3,
DISCONNECTED: 4,
ENTERED: 5
};
converse.plugins.add('converse-muc', {
/* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are called "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* It's possible however to make optional dependencies non-optional.
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-controlbox", "converse-chatview"],
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
_tearDown () {
const rooms = this.chatboxes.where({'type': converse.CHATROOMS_TYPE});
_.each(rooms, function (room) {
u.safeSave(room, {'connection_status': converse.ROOMSTATUS.DISCONNECTED});
});
this.__super__._tearDown.call(this, arguments);
},
ChatBoxes: {
model (attrs, options) {
const { _converse } = this.__super__;
if (attrs.type == converse.CHATROOMS_TYPE) {
return new _converse.ChatRoom(attrs, options);
} else {
return this.__super__.model.apply(this, arguments);
}
},
}
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
function ___ (str) {
/* This is part of a hack to get gettext to scan strings to be
* translated. Strings we cannot send to the function above because
* they require variable interpolation and we don't yet have the
* variables at scan time.
*
* See actionInfoMessages further below.
*/
return str;
}
// XXX: Inside plugins, all calls to the translation machinery
// (e.g. u.__) should only be done in the initialize function.
// If called before, we won't know what language the user wants,
// and it'll fall back to English.
/* http://xmpp.org/extensions/xep-0045.html
* ----------------------------------------
* 100 message Entering a room Inform user that any occupant is allowed to see the user's full JID
* 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the room
* 102 message Configuration change Inform occupants that room now shows unavailable members
* 103 message Configuration change Inform occupants that room now does not show unavailable members
* 104 message Configuration change Inform occupants that a non-privacy-related room configuration change has occurred
* 110 presence Any room presence Inform user that presence refers to one of its own room occupants
* 170 message or initial presence Configuration change Inform occupants that room logging is now enabled
* 171 message Configuration change Inform occupants that room logging is now disabled
* 172 message Configuration change Inform occupants that the room is now non-anonymous
* 173 message Configuration change Inform occupants that the room is now semi-anonymous
* 174 message Configuration change Inform occupants that the room is now fully-anonymous
* 201 presence Entering a room Inform user that a new room has been created
* 210 presence Entering a room Inform user that the service has assigned or modified the occupant's roomnick
* 301 presence Removal from room Inform user that he or she has been banned from the room
* 303 presence Exiting a room Inform all occupants of new room nickname
* 307 presence Removal from room Inform user that he or she has been kicked from the room
* 321 presence Removal from room Inform user that he or she is being removed from the room because of an affiliation change
* 322 presence Removal from room Inform user that he or she is being removed from the room because the room has been changed to members-only and the user is not a member
* 332 presence Removal from room Inform user that he or she is being removed from the room because of a system shutdown
*/
_converse.muc = {
info_messages: {
100: __('This room is not anonymous'),
102: __('This room now shows unavailable members'),
103: __('This room does not show unavailable members'),
104: __('The room configuration has changed'),
170: __('Room logging is now enabled'),
171: __('Room logging is now disabled'),
172: __('This room is now no longer anonymous'),
173: __('This room is now semi-anonymous'),
174: __('This room is now fully-anonymous'),
201: __('A new room has been created')
},
disconnect_messages: {
301: __('You have been banned from this room'),
307: __('You have been kicked from this room'),
321: __("You have been removed from this room because of an affiliation change"),
322: __("You have been removed from this room because the room has changed to members-only and you're not a member"),
332: __("You have been removed from this room because the MUC (Multi-user chat) service is being shut down")
},
action_info_messages: {
/* XXX: Note the triple underscore function and not double
* underscore.
*
* This is a hack. We can't pass the strings to __ because we
* don't yet know what the variable to interpolate is.
*
* Triple underscore will just return the string again, but we
* can then at least tell gettext to scan for it so that these
* strings are picked up by the translation machinery.
*/
301: ___("%1$s has been banned"),
303: ___("%1$s's nickname has changed"),
307: ___("%1$s has been kicked out"),
321: ___("%1$s has been removed because of an affiliation change"),
322: ___("%1$s has been removed for not being a member")
},
new_nickname_messages: {
210: ___('Your nickname has been automatically set to %1$s'),
303: ___('Your nickname has been changed to %1$s')
}
};
// Configuration values for this plugin
// ====================================
// Refer to docs/source/configuration.rst for explanations of these
// configuration settings.
_converse.api.settings.update({
allow_muc: true,
allow_muc_invitations: true,
auto_join_on_invite: false,
auto_join_rooms: [],
auto_list_rooms: false,
hide_muc_server: false,
muc_disable_moderator_commands: false,
muc_domain: undefined,
muc_history_max_stanzas: undefined,
muc_instant_rooms: true,
muc_nickname_from_jid: false,
muc_show_join_leave: true,
visible_toolbar_buttons: {
'toggle_occupants': true
},
});
_converse.api.promises.add(['roomsAutoJoined']);
function openRoom (jid) {
if (!u.isValidMUCJID(jid)) {
return _converse.log(
`Invalid JID "${jid}" provided in URL fragment`,
Strophe.LogLevel.WARN
);
}
const promises = [_converse.api.waitUntil('roomsAutoJoined')]
if (_converse.allow_bookmarks) {
promises.push( _converse.api.waitUntil('bookmarksInitialized'));
}
Promise.all(promises).then(() => {
_converse.api.rooms.open(jid);
});
}
_converse.router.route('converse/room?jid=:jid', openRoom);
_converse.openChatRoom = function (jid, settings, bring_to_foreground) {
/* Opens a chat room, making sure that certain attributes
* are correct, for example that the "type" is set to
* "chatroom".
*/
settings.type = converse.CHATROOMS_TYPE;
settings.id = jid;
settings.box_id = b64_sha1(jid)
const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
chatbox.trigger('show', true);
return chatbox;
}
_converse.ChatRoom = _converse.ChatBox.extend({
defaults () {
return _.assign(
_.clone(_converse.ChatBox.prototype.defaults),
_.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)),
{
// For group chats, we distinguish between generally unread
// messages and those ones that specifically mention the
// user.
//
// To keep things simple, we reuse `num_unread` from
// _converse.ChatBox to indicate unread messages which
// mention the user and `num_unread_general` to indicate
// generally unread messages (which *includes* mentions!).
'num_unread_general': 0,
'affiliation': null,
'connection_status': converse.ROOMSTATUS.DISCONNECTED,
'name': '',
'description': '',
'features_fetched': false,
'roomconfig': {},
'type': converse.CHATROOMS_TYPE,
}
);
},
isUserMentioned (message) {
/* Returns a boolean to indicate whether the current user
* was mentioned in a message.
*
* Parameters:
* (String): The text message
*/
return (new RegExp(`\\b${this.get('nick')}\\b`)).test(message);
},
incrementUnreadMsgCounter (stanza) {
/* Given a newly received message, update the unread counter if
* necessary.
*
* Parameters:
* (XMLElement): The <messsage> stanza
*/
const body = stanza.querySelector('body');
if (_.isNull(body)) {
return; // The message has no text
}
if (u.isNewMessage(stanza) && this.newMessageWillBeHidden()) {
this.save({'num_unread_general': this.get('num_unread_general') + 1});
if (this.isUserMentioned(body.textContent)) {
this.save({'num_unread': this.get('num_unread') + 1});
_converse.incrementMsgCounter();
}
}
},
clearUnreadMsgCounter() {
u.safeSave(this, {
'num_unread': 0,
'num_unread_general': 0
});
}
});
_converse.ChatRoomOccupant = Backbone.Model.extend({
initialize (attributes) {
this.set(_.extend({
'id': _converse.connection.getUniqueId(),
}, attributes));
}
});
_converse.ChatRoomOccupants = Backbone.Collection.extend({
model: _converse.ChatRoomOccupant,
comparator (occupant1, occupant2) {
const role1 = occupant1.get('role') || 'none';
const role2 = occupant2.get('role') || 'none';
if (MUC_ROLE_WEIGHTS[role1] === MUC_ROLE_WEIGHTS[role2]) {
const nick1 = occupant1.get('nick').toLowerCase();
const nick2 = occupant2.get('nick').toLowerCase();
return nick1 < nick2 ? -1 : (nick1 > nick2? 1 : 0);
} else {
return MUC_ROLE_WEIGHTS[role1] < MUC_ROLE_WEIGHTS[role2] ? -1 : 1;
}
},
});
_converse.RoomsPanelModel = Backbone.Model.extend({
defaults: {
'muc_domain': '',
},
});
_converse.onDirectMUCInvitation = function (message) {
/* A direct MUC invitation to join a room has been received
* See XEP-0249: Direct MUC invitations.
*
* Parameters:
* (XMLElement) message: The message stanza containing the
* invitation.
*/
const x_el = sizzle('x[xmlns="jabber:x:conference"]', message).pop(),
from = Strophe.getBareJidFromJid(message.getAttribute('from')),
room_jid = x_el.getAttribute('jid'),
reason = x_el.getAttribute('reason');
let contact = _converse.roster.get(from),
result;
if (_converse.auto_join_on_invite) {
result = true;
} else {
// Invite request might come from someone not your roster list
contact = contact? contact.get('fullname'): Strophe.getNodeFromJid(from);
if (!reason) {
result = confirm(
__("%1$s has invited you to join a chat room: %2$s", contact, room_jid)
);
} else {
result = confirm(
__('%1$s has invited you to join a chat room: %2$s, and left the following reason: "%3$s"',
contact, room_jid, reason)
);
}
}
if (result === true) {
const chatroom = _converse.openChatRoom(
room_jid, {'password': x_el.getAttribute('password') });
if (chatroom.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
_converse.chatboxviews.get(room_jid).join();
}
}
};
if (_converse.allow_muc_invitations) {
const registerDirectInvitationHandler = function () {
_converse.connection.addHandler(
function (message) {
_converse.onDirectMUCInvitation(message);
return true;
}, 'jabber:x:conference', 'message');
};
_converse.on('connected', registerDirectInvitationHandler);
_converse.on('reconnected', registerDirectInvitationHandler);
}
const getChatRoom = function (jid, attrs, create) {
jid = jid.toLowerCase();
attrs.type = converse.CHATROOMS_TYPE;
attrs.id = jid;
attrs.box_id = b64_sha1(jid)
return _converse.chatboxes.getChatBox(jid, attrs, create);
};
const createChatRoom = function (jid, attrs) {
return getChatRoom(jid, attrs, true);
};
function autoJoinRooms () {
/* Automatically join chat rooms, based on the
* "auto_join_rooms" configuration setting, which is an array
* of strings (room JIDs) or objects (with room JID and other
* settings).
*/
_.each(_converse.auto_join_rooms, function (room) {
if (_converse.chatboxes.where({'jid': room}).length) {
return;
}
if (_.isString(room)) {
_converse.api.rooms.open(room);
} else if (_.isObject(room)) {
_converse.api.rooms.open(room.jid, room.nick);
} else {
_converse.log(
'Invalid room criteria specified for "auto_join_rooms"',
Strophe.LogLevel.ERROR);
}
});
_converse.emit('roomsAutoJoined');
}
function reconnectToChatRooms () {
/* Upon a reconnection event from converse, join again
* all the open chat rooms.
*/
_converse.chatboxviews.each(function (view) {
if (view.model.get('type') === converse.CHATROOMS_TYPE) {
view.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
view.registerHandlers();
view.join();
view.fetchMessages();
}
});
}
function disconnectChatRooms () {
/* When disconnecting, or reconnecting, mark all chat rooms as
* disconnected, so that they will be properly entered again
* when fetched from session storage.
*/
_converse.chatboxes.each(function (model) {
if (model.get('type') === converse.CHATROOMS_TYPE) {
model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
}
});
}
/************************ BEGIN Event Handlers ************************/
_converse.on('addClientFeatures', () => {
if (_converse.allow_muc) {
_converse.connection.disco.addFeature(Strophe.NS.MUC);
}
if (_converse.allow_muc_invitations) {
_converse.connection.disco.addFeature('jabber:x:conference'); // Invites
}
});
_converse.on('chatBoxesFetched', autoJoinRooms);
_converse.on('reconnected', reconnectToChatRooms);
_converse.on('reconnecting', disconnectChatRooms);
_converse.on('disconnecting', disconnectChatRooms);
/************************ END Event Handlers ************************/
/************************ BEGIN API ************************/
// We extend the default converse.js API to add methods specific to MUC chat rooms.
_.extend(_converse.api, {
'rooms': {
'close' (jids) {
if (_.isUndefined(jids)) {
// FIXME: can't access views here
_converse.chatboxviews.each(function (view) {
if (view.is_chatroom && view.model) {
view.close();
}
});
} else if (_.isString(jids)) {
const view = _converse.chatboxviews.get(jids);
if (view) { view.close(); }
} else {
_.each(jids, function (jid) {
const view = _converse.chatboxviews.get(jid);
if (view) { view.close(); }
});
}
},
'create' (jids, attrs) {
if (_.isString(attrs)) {
attrs = {'nick': attrs};
} else if (_.isUndefined(attrs)) {
attrs = {};
}
if (_.isUndefined(attrs.maximize)) {
attrs.maximize = false;
}
if (!attrs.nick && _converse.muc_nickname_from_jid) {
attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
}
if (_.isUndefined(jids)) {
throw new TypeError('rooms.create: You need to provide at least one JID');
} else if (_.isString(jids)) {
return createChatRoom(jids, attrs);
}
return _.map(jids, _.partial(createChatRoom, _, attrs));
},
'open' (jids, attrs) {
if (_.isUndefined(jids)) {
throw new TypeError('rooms.open: You need to provide at least one JID');
} else if (_.isString(jids)) {
return _converse.api.rooms.create(jids, attrs).trigger('show');
}
return _.map(jids, (jid) => _converse.api.rooms.create(jid, attrs).trigger('show'));
},
'get' (jids, attrs, create) {
if (_.isString(attrs)) {
attrs = {'nick': attrs};
} else if (_.isUndefined(attrs)) {
attrs = {};
}
if (_.isUndefined(jids)) {
const result = [];
_converse.chatboxes.each(function (chatbox) {
if (chatbox.get('type') === converse.CHATROOMS_TYPE) {
result.push(chatbox);
}
});
return result;
}
if (!attrs.nick) {
attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
}
if (_.isString(jids)) {
return getChatRoom(jids, attrs);
}
return _.map(jids, _.partial(getChatRoom, _, attrs));
}
}
});
/************************ END API ************************/
}
});
}));