2018-04-24 11:08:26 +02:00
|
|
|
// Converse.js
|
2016-02-16 08:46:47 +01:00
|
|
|
// http://conversejs.org
|
|
|
|
//
|
2018-04-24 11:08:26 +02:00
|
|
|
// Copyright (c) 2012-2018, the Converse.js developers
|
2016-02-16 08:46:47 +01:00
|
|
|
// Licensed under the Mozilla Public License (MPLv2)
|
|
|
|
|
|
|
|
(function (root, factory) {
|
2017-02-13 15:37:17 +01:00
|
|
|
define([
|
2017-08-16 15:19:41 +02:00
|
|
|
"form-utils",
|
2017-02-14 15:08:39 +01:00
|
|
|
"converse-core",
|
2018-04-24 11:08:26 +02:00
|
|
|
"emojione",
|
2017-07-21 12:41:16 +02:00
|
|
|
"converse-chatview",
|
2017-11-10 21:20:13 +01:00
|
|
|
"converse-disco",
|
2017-12-20 20:29:57 +01:00
|
|
|
"backbone.overview",
|
|
|
|
"backbone.orderedlistview",
|
2018-03-29 11:56:24 +02:00
|
|
|
"backbone.vdomview",
|
|
|
|
"muc-utils"
|
2016-02-28 20:24:06 +01:00
|
|
|
], factory);
|
2018-04-24 11:08:26 +02:00
|
|
|
}(this, function (u, converse, emojione) {
|
2016-02-19 11:43:46 +01:00
|
|
|
"use strict";
|
2016-11-02 23:08:20 +01:00
|
|
|
|
2017-12-14 18:43:00 +01:00
|
|
|
const MUC_ROLE_WEIGHTS = {
|
|
|
|
'moderator': 1,
|
|
|
|
'participant': 2,
|
|
|
|
'visitor': 3,
|
|
|
|
'none': 4,
|
|
|
|
};
|
|
|
|
|
2017-08-16 15:19:41 +02:00
|
|
|
const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, b64_sha1, sizzle, _, moment } = converse.env;
|
2016-02-16 08:46:47 +01:00
|
|
|
|
|
|
|
// 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");
|
|
|
|
|
2018-04-08 19:44:53 +02:00
|
|
|
converse.MUC_NICK_CHANGED_CODE = "303";
|
2018-02-21 22:40:51 +01:00
|
|
|
converse.CHATROOMS_TYPE = 'chatroom';
|
|
|
|
|
|
|
|
converse.ROOM_FEATURES = [
|
2017-02-28 22:34:16 +01:00
|
|
|
'passwordprotected', 'unsecured', 'hidden',
|
2017-11-05 18:47:30 +01:00
|
|
|
'publicroom', 'membersonly', 'open', 'persistent',
|
2017-02-28 22:34:16 +01:00
|
|
|
'temporary', 'nonanonymous', 'semianonymous',
|
|
|
|
'moderated', 'unmoderated', 'mam_enabled'
|
|
|
|
];
|
2017-07-21 15:05:22 +02:00
|
|
|
|
|
|
|
converse.ROOMSTATUS = {
|
2017-02-24 11:54:54 +01:00
|
|
|
CONNECTED: 0,
|
|
|
|
CONNECTING: 1,
|
2017-03-02 23:28:22 +01:00
|
|
|
NICKNAME_REQUIRED: 2,
|
2017-04-04 16:45:50 +02:00
|
|
|
PASSWORD_REQUIRED: 3,
|
|
|
|
DISCONNECTED: 4,
|
|
|
|
ENTERED: 5
|
2017-02-24 11:54:54 +01:00
|
|
|
};
|
|
|
|
|
2018-03-29 11:56:24 +02:00
|
|
|
|
2016-12-20 11:42:20 +01:00
|
|
|
converse.plugins.add('converse-muc', {
|
2016-06-10 00:05:55 +02:00
|
|
|
/* Optional dependencies are other plugins which might be
|
2017-03-08 12:15:19 +01:00
|
|
|
* 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.
|
2016-06-09 11:01:54 +02:00
|
|
|
*
|
2017-03-08 12:15:19 +01:00
|
|
|
* It's possible however to make optional dependencies non-optional.
|
|
|
|
* If the setting "strict_plugin_dependencies" is set to true,
|
2016-06-10 00:05:55 +02:00
|
|
|
* an error will be raised if the plugin is not found.
|
2016-06-09 11:01:54 +02:00
|
|
|
*
|
2016-06-10 00:05:55 +02:00
|
|
|
* NB: These plugins need to have already been loaded via require.js.
|
2016-06-09 11:01:54 +02:00
|
|
|
*/
|
2018-01-10 13:59:45 +01:00
|
|
|
dependencies: ["converse-controlbox", "converse-chatview"],
|
2016-02-16 08:46:47 +01:00
|
|
|
|
|
|
|
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.
|
2016-02-13 23:58:50 +01:00
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
_tearDown () {
|
2018-02-21 22:40:51 +01:00
|
|
|
const rooms = this.chatboxes.where({'type': converse.CHATROOMS_TYPE});
|
2017-06-23 18:21:50 +02:00
|
|
|
_.each(rooms, function (room) {
|
2017-12-06 14:59:01 +01:00
|
|
|
u.safeSave(room, {'connection_status': converse.ROOMSTATUS.DISCONNECTED});
|
2017-06-23 18:21:50 +02:00
|
|
|
});
|
|
|
|
this.__super__._tearDown.call(this, arguments);
|
|
|
|
},
|
|
|
|
|
2017-05-23 18:16:47 +02:00
|
|
|
ChatBoxes: {
|
2017-07-10 17:46:22 +02:00
|
|
|
model (attrs, options) {
|
|
|
|
const { _converse } = this.__super__;
|
2018-02-21 22:40:51 +01:00
|
|
|
if (attrs.type == converse.CHATROOMS_TYPE) {
|
2017-05-23 18:36:40 +02:00
|
|
|
return new _converse.ChatRoom(attrs, options);
|
2017-05-23 18:16:47 +02:00
|
|
|
} else {
|
|
|
|
return this.__super__.model.apply(this, arguments);
|
|
|
|
}
|
|
|
|
},
|
2016-02-28 20:24:06 +01:00
|
|
|
}
|
2016-02-16 08:46:47 +01:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
initialize () {
|
2016-02-16 08:46:47 +01:00
|
|
|
/* The initialize function gets called as soon as the plugin is
|
|
|
|
* loaded by converse.js's plugin machinery.
|
|
|
|
*/
|
2017-07-10 17:46:22 +02:00
|
|
|
const { _converse } = this,
|
2017-09-26 18:27:41 +02:00
|
|
|
{ __ } = _converse;
|
|
|
|
|
2016-02-16 08:46:47 +01:00
|
|
|
// Configuration values for this plugin
|
2016-08-11 18:03:02 +02:00
|
|
|
// ====================================
|
|
|
|
// Refer to docs/source/configuration.rst for explanations of these
|
|
|
|
// configuration settings.
|
2017-07-05 11:03:13 +02:00
|
|
|
_converse.api.settings.update({
|
2016-02-13 23:58:50 +01:00
|
|
|
allow_muc: true,
|
2016-08-11 18:03:02 +02:00
|
|
|
allow_muc_invitations: true,
|
|
|
|
auto_join_on_invite: false,
|
|
|
|
auto_join_rooms: [],
|
2016-10-26 15:21:36 +02:00
|
|
|
muc_domain: undefined,
|
2016-08-11 18:03:02 +02:00
|
|
|
muc_history_max_stanzas: undefined,
|
|
|
|
muc_instant_rooms: true,
|
2018-04-08 19:44:53 +02:00
|
|
|
muc_nickname_from_jid: false
|
2016-03-13 17:16:53 +01:00
|
|
|
});
|
2018-02-21 22:40:51 +01:00
|
|
|
_converse.api.promises.add(['roomsAutoJoined']);
|
2017-09-29 00:07:16 +02:00
|
|
|
|
2017-10-31 22:08:06 +01:00
|
|
|
|
2017-10-31 23:04:46 +01:00
|
|
|
function openRoom (jid) {
|
2018-01-29 14:51:49 +01:00
|
|
|
if (!u.isValidMUCJID(jid)) {
|
|
|
|
return _converse.log(
|
2017-10-31 23:04:46 +01:00
|
|
|
`Invalid JID "${jid}" provided in URL fragment`,
|
|
|
|
Strophe.LogLevel.WARN
|
|
|
|
);
|
|
|
|
}
|
2017-10-31 22:08:06 +01:00
|
|
|
const promises = [_converse.api.waitUntil('roomsAutoJoined')]
|
2018-02-07 17:30:44 +01:00
|
|
|
if (_converse.allow_bookmarks) {
|
|
|
|
promises.push( _converse.api.waitUntil('bookmarksInitialized'));
|
|
|
|
}
|
2017-10-31 22:08:06 +01:00
|
|
|
Promise.all(promises).then(() => {
|
2017-10-31 23:04:46 +01:00
|
|
|
_converse.api.rooms.open(jid);
|
2017-10-31 22:08:06 +01:00
|
|
|
});
|
|
|
|
}
|
2017-10-31 23:04:46 +01:00
|
|
|
_converse.router.route('converse/room?jid=:jid', openRoom);
|
2017-09-29 00:07:16 +02:00
|
|
|
|
2017-05-07 20:16:13 +02:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
_converse.openChatRoom = function (jid, settings, bring_to_foreground) {
|
2017-06-15 16:22:49 +02:00
|
|
|
/* Opens a chat room, making sure that certain attributes
|
2016-12-04 10:43:39 +01:00
|
|
|
* are correct, for example that the "type" is set to
|
|
|
|
* "chatroom".
|
|
|
|
*/
|
2018-02-21 22:40:51 +01:00
|
|
|
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;
|
2017-10-31 22:07:40 +01:00
|
|
|
}
|
2016-02-16 08:46:47 +01:00
|
|
|
|
2017-05-23 18:36:40 +02:00
|
|
|
_converse.ChatRoom = _converse.ChatBox.extend({
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
defaults () {
|
2017-06-15 16:22:49 +02:00
|
|
|
return _.assign(
|
|
|
|
_.clone(_converse.ChatBox.prototype.defaults),
|
2018-02-21 22:40:51 +01:00
|
|
|
_.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)),
|
2017-06-15 16:22:49 +02:00
|
|
|
{
|
|
|
|
// 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,
|
2017-07-21 15:05:22 +02:00
|
|
|
'connection_status': converse.ROOMSTATUS.DISCONNECTED,
|
2017-07-19 08:30:04 +02:00
|
|
|
'name': '',
|
2018-03-27 16:05:38 +02:00
|
|
|
'nick': _converse.xmppstatus.get('nickname'),
|
2017-06-15 16:22:49 +02:00
|
|
|
'description': '',
|
|
|
|
'features_fetched': false,
|
|
|
|
'roomconfig': {},
|
2018-02-21 22:40:51 +01:00
|
|
|
'type': converse.CHATROOMS_TYPE,
|
2018-04-17 15:17:39 +02:00
|
|
|
'message_type': 'groupchat'
|
2017-06-15 16:22:49 +02:00
|
|
|
}
|
|
|
|
);
|
2017-06-07 17:47:06 +02:00
|
|
|
},
|
2017-05-24 08:40:09 +02:00
|
|
|
|
2018-04-08 19:44:53 +02:00
|
|
|
initialize() {
|
|
|
|
this.constructor.__super__.initialize.apply(this, arguments);
|
|
|
|
this.occupants = new _converse.ChatRoomOccupants();
|
|
|
|
this.registerHandlers();
|
2018-04-22 04:20:14 +02:00
|
|
|
|
|
|
|
this.on('change:chat_state', this.sendChatState, this);
|
2018-04-08 19:44:53 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
registerHandlers () {
|
|
|
|
/* Register presence and message handlers for this chat
|
|
|
|
* room
|
|
|
|
*/
|
|
|
|
const room_jid = this.get('jid');
|
|
|
|
this.removeHandlers();
|
|
|
|
this.presence_handler = _converse.connection.addHandler((stanza) => {
|
|
|
|
_.each(_.values(this.handlers.presence), (callback) => callback(stanza));
|
|
|
|
this.onPresence(stanza);
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
Strophe.NS.MUC, 'presence', null, null, room_jid,
|
|
|
|
{'ignoreNamespaceFragment': true, 'matchBareFromJid': true}
|
|
|
|
);
|
|
|
|
this.message_handler = _converse.connection.addHandler((stanza) => {
|
|
|
|
_.each(_.values(this.handlers.message), (callback) => callback(stanza));
|
|
|
|
this.onMessage(stanza);
|
|
|
|
return true;
|
|
|
|
}, null, 'message', 'groupchat', null, room_jid,
|
|
|
|
{'matchBareFromJid': true}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeHandlers () {
|
|
|
|
/* Remove the presence and message handlers that were
|
|
|
|
* registered for this chat room.
|
|
|
|
*/
|
|
|
|
if (this.message_handler) {
|
|
|
|
_converse.connection.deleteHandler(this.message_handler);
|
|
|
|
delete this.message_handler;
|
|
|
|
}
|
|
|
|
if (this.presence_handler) {
|
|
|
|
_converse.connection.deleteHandler(this.presence_handler);
|
|
|
|
delete this.presence_handler;
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
addHandler (type, name, callback) {
|
|
|
|
/* Allows 'presence' and 'message' handlers to be
|
|
|
|
* registered. These will be executed once presence or
|
|
|
|
* message stanzas are received, and *before* this model's
|
|
|
|
* own handlers are executed.
|
|
|
|
*/
|
|
|
|
if (_.isNil(this.handlers)) {
|
|
|
|
this.handlers = {};
|
|
|
|
}
|
|
|
|
if (_.isNil(this.handlers[type])) {
|
|
|
|
this.handlers[type] = {};
|
|
|
|
}
|
|
|
|
this.handlers[type][name] = callback;
|
|
|
|
},
|
|
|
|
|
|
|
|
join (nick, password) {
|
|
|
|
/* Join the chat room.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (String) nick: The user's nickname
|
|
|
|
* (String) password: Optional password, if required by
|
|
|
|
* the room.
|
|
|
|
*/
|
|
|
|
nick = nick ? nick : this.get('nick');
|
|
|
|
if (!nick) {
|
|
|
|
throw new TypeError('join: You need to provide a valid nickname');
|
|
|
|
}
|
|
|
|
if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
|
|
|
|
// We have restored a chat room from session storage,
|
|
|
|
// so we don't send out a presence stanza again.
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
const stanza = $pres({
|
|
|
|
'from': _converse.connection.jid,
|
|
|
|
'to': this.getRoomJIDAndNick(nick)
|
|
|
|
}).c("x", {'xmlns': Strophe.NS.MUC})
|
|
|
|
.c("history", {'maxstanzas': _converse.muc_history_max_stanzas}).up();
|
|
|
|
if (password) {
|
|
|
|
stanza.cnode(Strophe.xmlElement("password", [], password));
|
|
|
|
}
|
|
|
|
this.save('connection_status', converse.ROOMSTATUS.CONNECTING);
|
|
|
|
_converse.connection.send(stanza);
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
leave (exit_msg) {
|
|
|
|
/* Leave the chat room.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (String) exit_msg: Optional message to indicate your
|
|
|
|
* reason for leaving.
|
|
|
|
*/
|
|
|
|
this.occupants.reset();
|
|
|
|
this.occupants.browserStorage._clear();
|
|
|
|
if (_converse.connection.connected) {
|
|
|
|
this.sendUnavailablePresence(exit_msg);
|
|
|
|
}
|
|
|
|
u.safeSave(this, {'connection_status': converse.ROOMSTATUS.DISCONNECTED});
|
|
|
|
this.removeHandlers();
|
|
|
|
},
|
|
|
|
|
|
|
|
sendUnavailablePresence (exit_msg) {
|
|
|
|
const presence = $pres({
|
|
|
|
type: "unavailable",
|
|
|
|
from: _converse.connection.jid,
|
|
|
|
to: this.getRoomJIDAndNick()
|
|
|
|
});
|
|
|
|
if (exit_msg !== null) {
|
|
|
|
presence.c("status", exit_msg);
|
|
|
|
}
|
|
|
|
_converse.connection.sendPresence(presence);
|
|
|
|
},
|
|
|
|
|
2018-04-24 11:08:26 +02:00
|
|
|
getOutgoingMessageAttributes (text, spoiler_hint) {
|
|
|
|
const is_spoiler = this.get('composing_spoiler');
|
|
|
|
return {
|
|
|
|
'fullname': this.get('nick'),
|
|
|
|
'is_spoiler': is_spoiler,
|
|
|
|
'message': text ? u.httpToGeoUri(emojione.shortnameToUnicode(text), _converse) : undefined,
|
|
|
|
'sender': 'me',
|
|
|
|
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
|
|
|
|
'type': 'groupchat',
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2018-04-08 19:44:53 +02:00
|
|
|
getRoomFeatures () {
|
|
|
|
/* Fetch the room disco info, parse it and then save it.
|
|
|
|
*/
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
_converse.connection.disco.info(
|
|
|
|
this.get('jid'),
|
|
|
|
null,
|
|
|
|
_.flow(this.parseRoomFeatures.bind(this), resolve),
|
|
|
|
() => { reject(new Error("Could not parse the room features")) },
|
|
|
|
5000
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
getRoomJIDAndNick (nick) {
|
|
|
|
/* Utility method to construct the JID for the current user
|
|
|
|
* as occupant of the room.
|
|
|
|
*
|
|
|
|
* This is the room JID, with the user's nick added at the
|
|
|
|
* end.
|
|
|
|
*
|
|
|
|
* For example: room@conference.example.org/nickname
|
|
|
|
*/
|
|
|
|
if (nick) {
|
|
|
|
this.save({'nick': nick});
|
|
|
|
} else {
|
|
|
|
nick = this.get('nick');
|
|
|
|
}
|
|
|
|
const room = this.get('jid');
|
|
|
|
const jid = Strophe.getBareJidFromJid(room);
|
|
|
|
return jid + (nick !== null ? `/${nick}` : "");
|
|
|
|
},
|
2018-04-12 07:43:39 +02:00
|
|
|
|
2018-04-22 04:20:14 +02:00
|
|
|
sendChatState () {
|
|
|
|
/* Sends a message with the status of the user in this chat session
|
|
|
|
* as taken from the 'chat_state' attribute of the chat box.
|
|
|
|
* See XEP-0085 Chat State Notifications.
|
|
|
|
*/
|
|
|
|
if (this.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const chat_state = this.get('chat_state');
|
|
|
|
if (chat_state === _converse.GONE) {
|
|
|
|
// <gone/> is not applicable within MUC context
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_converse.connection.send(
|
|
|
|
$msg({'to':this.get('jid'), 'type': 'groupchat'})
|
|
|
|
.c(chat_state, {'xmlns': Strophe.NS.CHATSTATES}).up()
|
|
|
|
.c('no-store', {'xmlns': Strophe.NS.HINTS}).up()
|
|
|
|
.c('no-permanent-store', {'xmlns': Strophe.NS.HINTS})
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2018-03-29 11:56:24 +02:00
|
|
|
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
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2018-03-29 10:16:41 +02:00
|
|
|
parseRoomFeatures (iq) {
|
|
|
|
/* Parses an IQ stanza containing the room's features.
|
|
|
|
*
|
|
|
|
* See http://xmpp.org/extensions/xep-0045.html#disco-roominfo
|
|
|
|
*
|
|
|
|
* <identity
|
|
|
|
* category='conference'
|
|
|
|
* name='A Dark Cave'
|
|
|
|
* type='text'/>
|
|
|
|
* <feature var='http://jabber.org/protocol/muc'/>
|
|
|
|
* <feature var='muc_passwordprotected'/>
|
|
|
|
* <feature var='muc_hidden'/>
|
|
|
|
* <feature var='muc_temporary'/>
|
|
|
|
* <feature var='muc_open'/>
|
|
|
|
* <feature var='muc_unmoderated'/>
|
|
|
|
* <feature var='muc_nonanonymous'/>
|
|
|
|
* <feature var='urn:xmpp:mam:0'/>
|
|
|
|
*/
|
|
|
|
const features = {
|
|
|
|
'features_fetched': true,
|
|
|
|
'name': iq.querySelector('identity').getAttribute('name')
|
|
|
|
}
|
|
|
|
_.each(iq.querySelectorAll('feature'), function (field) {
|
|
|
|
const fieldname = field.getAttribute('var');
|
|
|
|
if (!fieldname.startsWith('muc_')) {
|
|
|
|
if (fieldname === Strophe.NS.MAM) {
|
|
|
|
features.mam_enabled = true;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
features[fieldname.replace('muc_', '')] = true;
|
|
|
|
});
|
|
|
|
const desc_field = iq.querySelector('field[var="muc#roominfo_description"] value');
|
|
|
|
if (!_.isNull(desc_field)) {
|
|
|
|
features.description = desc_field.textContent;
|
|
|
|
}
|
|
|
|
this.save(features);
|
|
|
|
},
|
|
|
|
|
2018-03-29 11:56:24 +02:00
|
|
|
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);
|
|
|
|
},
|
|
|
|
|
2018-04-08 19:44:53 +02:00
|
|
|
saveConfiguration (form) {
|
|
|
|
/* Submit the room configuration form by sending an IQ
|
|
|
|
* stanza to the server.
|
|
|
|
*
|
|
|
|
* Returns a promise which resolves once the XMPP server
|
|
|
|
* has return a response IQ.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (HTMLElement) form: The configuration form DOM element.
|
|
|
|
* If no form is provided, the default configuration
|
|
|
|
* values will be used.
|
|
|
|
*/
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const inputs = form ? sizzle(':input:not([type=button]):not([type=submit])', form) : [],
|
|
|
|
configArray = _.map(inputs, u.webForm2xForm);
|
|
|
|
this.sendConfiguration(configArray, resolve, reject);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
autoConfigureChatRoom () {
|
|
|
|
/* Automatically configure room based on this model's
|
|
|
|
* 'roomconfig' data.
|
|
|
|
*
|
|
|
|
* Returns a promise which resolves once a response IQ has
|
|
|
|
* been received.
|
|
|
|
*/
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.fetchRoomConfiguration().then((stanza) => {
|
|
|
|
const configArray = [],
|
|
|
|
fields = stanza.querySelectorAll('field'),
|
|
|
|
config = this.get('roomconfig');
|
|
|
|
let count = fields.length;
|
|
|
|
|
|
|
|
_.each(fields, (field) => {
|
|
|
|
const fieldname = field.getAttribute('var').replace('muc#roomconfig_', ''),
|
|
|
|
type = field.getAttribute('type');
|
|
|
|
let value;
|
|
|
|
if (fieldname in config) {
|
|
|
|
switch (type) {
|
|
|
|
case 'boolean':
|
|
|
|
value = config[fieldname] ? 1 : 0;
|
|
|
|
break;
|
|
|
|
case 'list-multi':
|
|
|
|
// TODO: we don't yet handle "list-multi" types
|
|
|
|
value = field.innerHTML;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
value = config[fieldname];
|
|
|
|
}
|
|
|
|
field.innerHTML = $build('value').t(value);
|
|
|
|
}
|
|
|
|
configArray.push(field);
|
|
|
|
if (!--count) {
|
|
|
|
this.sendConfiguration(configArray, resolve, reject);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
fetchRoomConfiguration () {
|
|
|
|
/* Send an IQ stanza to fetch the room configuration data.
|
|
|
|
* Returns a promise which resolves once the response IQ
|
|
|
|
* has been received.
|
|
|
|
*/
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
_converse.connection.sendIQ(
|
|
|
|
$iq({
|
|
|
|
'to': this.get('jid'),
|
|
|
|
'type': "get"
|
|
|
|
}).c("query", {xmlns: Strophe.NS.MUC_OWNER}),
|
|
|
|
resolve,
|
|
|
|
reject
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
sendConfiguration (config, callback, errback) {
|
|
|
|
/* Send an IQ stanza with the room configuration.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (Array) config: The room configuration
|
|
|
|
* (Function) callback: Callback upon succesful IQ response
|
|
|
|
* The first parameter passed in is IQ containing the
|
|
|
|
* room configuration.
|
|
|
|
* The second is the response IQ from the server.
|
|
|
|
* (Function) errback: Callback upon error IQ response
|
|
|
|
* The first parameter passed in is IQ containing the
|
|
|
|
* room configuration.
|
|
|
|
* The second is the response IQ from the server.
|
|
|
|
*/
|
|
|
|
const iq = $iq({to: this.get('jid'), type: "set"})
|
|
|
|
.c("query", {xmlns: Strophe.NS.MUC_OWNER})
|
|
|
|
.c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
|
|
|
|
_.each(config || [], function (node) { iq.cnode(node).up(); });
|
|
|
|
callback = _.isUndefined(callback) ? _.noop : _.partial(callback, iq.nodeTree);
|
|
|
|
errback = _.isUndefined(errback) ? _.noop : _.partial(errback, iq.nodeTree);
|
|
|
|
return _converse.connection.sendIQ(iq, callback, errback);
|
|
|
|
},
|
|
|
|
|
2018-03-29 11:56:24 +02:00
|
|
|
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));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2018-03-29 10:16:41 +02:00
|
|
|
checkForReservedNick (callback, errback) {
|
|
|
|
/* Use service-discovery to ask the XMPP server whether
|
|
|
|
* this user has a reserved nickname for this room.
|
|
|
|
* If so, we'll use that, otherwise we render the nickname form.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (Function) callback: Callback upon succesful IQ response
|
|
|
|
* (Function) errback: Callback upon error IQ response
|
|
|
|
*/
|
|
|
|
_converse.connection.sendIQ(
|
|
|
|
$iq({
|
|
|
|
'to': this.get('jid'),
|
|
|
|
'from': _converse.connection.jid,
|
|
|
|
'type': "get"
|
|
|
|
}).c("query", {
|
|
|
|
'xmlns': Strophe.NS.DISCO_INFO,
|
|
|
|
'node': 'x-roomuser-item'
|
|
|
|
}),
|
|
|
|
callback, errback);
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2018-04-08 19:44:53 +02:00
|
|
|
findOccupant (data) {
|
|
|
|
/* Try to find an existing occupant based on the passed in
|
|
|
|
* data object.
|
|
|
|
*
|
|
|
|
* If we have a JID, we use that as lookup variable,
|
|
|
|
* otherwise we use the nick. We don't always have both,
|
|
|
|
* but should have at least one or the other.
|
|
|
|
*/
|
|
|
|
const jid = Strophe.getBareJidFromJid(data.jid);
|
|
|
|
if (jid !== null) {
|
|
|
|
return this.occupants.where({'jid': jid}).pop();
|
|
|
|
} else {
|
|
|
|
return this.occupants.where({'nick': data.nick}).pop();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
updateOccupantsOnPresence (pres) {
|
|
|
|
/* Given a presence stanza, update the occupant model
|
|
|
|
* based on its contents.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (XMLElement) pres: The presence stanza
|
|
|
|
*/
|
|
|
|
const data = this.parsePresence(pres);
|
|
|
|
if (data.type === 'error') {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
const occupant = this.findOccupant(data);
|
|
|
|
if (data.type === 'unavailable') {
|
|
|
|
if (occupant) {
|
|
|
|
// Even before destroying, we set the new data, so
|
|
|
|
// that we can for example show the
|
|
|
|
// disconnection message.
|
|
|
|
occupant.set(data);
|
|
|
|
}
|
|
|
|
if (!_.includes(data.states, converse.MUC_NICK_CHANGED_CODE)) {
|
|
|
|
// We only destroy the occupant if this is not a
|
|
|
|
// nickname change operation.
|
|
|
|
if (occupant) {
|
|
|
|
occupant.destroy();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const jid = Strophe.getBareJidFromJid(data.jid);
|
|
|
|
const attributes = _.extend(data, {
|
|
|
|
'jid': jid ? jid : undefined,
|
|
|
|
'resource': data.jid ? Strophe.getResourceFromJid(data.jid) : undefined
|
|
|
|
});
|
|
|
|
if (occupant) {
|
|
|
|
occupant.save(attributes);
|
|
|
|
} else {
|
|
|
|
this.occupants.create(attributes);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
parsePresence (pres) {
|
|
|
|
const id = Strophe.getResourceFromJid(pres.getAttribute("from"));
|
|
|
|
const data = {
|
|
|
|
nick: id,
|
|
|
|
type: pres.getAttribute("type"),
|
|
|
|
states: []
|
|
|
|
};
|
|
|
|
_.each(pres.childNodes, function (child) {
|
|
|
|
switch (child.nodeName) {
|
|
|
|
case "status":
|
|
|
|
data.status = child.textContent || null;
|
|
|
|
break;
|
|
|
|
case "show":
|
|
|
|
data.show = child.textContent || 'online';
|
|
|
|
break;
|
|
|
|
case "x":
|
|
|
|
if (child.getAttribute("xmlns") === Strophe.NS.MUC_USER) {
|
|
|
|
_.each(child.childNodes, function (item) {
|
|
|
|
switch (item.nodeName) {
|
|
|
|
case "item":
|
|
|
|
data.affiliation = item.getAttribute("affiliation");
|
|
|
|
data.role = item.getAttribute("role");
|
|
|
|
data.jid = item.getAttribute("jid");
|
|
|
|
data.nick = item.getAttribute("nick") || data.nick;
|
|
|
|
break;
|
|
|
|
case "status":
|
|
|
|
if (item.getAttribute("code")) {
|
|
|
|
data.states.push(item.getAttribute("code"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
|
|
|
|
isDuplicate (message, original_stanza) {
|
|
|
|
const msgid = message.getAttribute('id'),
|
|
|
|
jid = message.getAttribute('from'),
|
|
|
|
resource = Strophe.getResourceFromJid(jid),
|
|
|
|
sender = resource && Strophe.unescapeNode(resource) || '';
|
|
|
|
if (msgid) {
|
|
|
|
return this.messages.filter(
|
|
|
|
// Some bots (like HAL in the prosody chatroom)
|
|
|
|
// respond to commands with the same ID as the
|
|
|
|
// original message. So we also check the sender.
|
|
|
|
(msg) => msg.get('msgid') === msgid && msg.get('fullname') === sender
|
|
|
|
).length > 0;
|
|
|
|
}
|
2018-04-10 20:56:40 +02:00
|
|
|
return false;
|
2018-04-08 19:44:53 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
fetchFeaturesIfConfigurationChanged (stanza) {
|
|
|
|
const configuration_changed = stanza.querySelector("status[code='104']"),
|
|
|
|
logging_enabled = stanza.querySelector("status[code='170']"),
|
|
|
|
logging_disabled = stanza.querySelector("status[code='171']"),
|
|
|
|
room_no_longer_anon = stanza.querySelector("status[code='172']"),
|
|
|
|
room_now_semi_anon = stanza.querySelector("status[code='173']"),
|
|
|
|
room_now_fully_anon = stanza.querySelector("status[code='173']");
|
|
|
|
|
|
|
|
if (configuration_changed || logging_enabled || logging_disabled ||
|
|
|
|
room_no_longer_anon || room_now_semi_anon || room_now_fully_anon) {
|
|
|
|
this.getRoomFeatures();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onMessage (stanza) {
|
|
|
|
/* Handler for all MUC messages sent to this chat room.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (XMLElement) stanza: The message stanza.
|
|
|
|
*/
|
|
|
|
this.fetchFeaturesIfConfigurationChanged(stanza);
|
|
|
|
|
|
|
|
const original_stanza = stanza,
|
|
|
|
forwarded = stanza.querySelector('forwarded');
|
|
|
|
let delay;
|
|
|
|
if (!_.isNull(forwarded)) {
|
|
|
|
stanza = forwarded.querySelector('message');
|
|
|
|
delay = forwarded.querySelector('delay');
|
|
|
|
}
|
|
|
|
const jid = stanza.getAttribute('from'),
|
|
|
|
resource = Strophe.getResourceFromJid(jid),
|
|
|
|
sender = resource && Strophe.unescapeNode(resource) || '',
|
|
|
|
subject = _.propertyOf(stanza.querySelector('subject'))('textContent');
|
|
|
|
|
|
|
|
if (this.isDuplicate(stanza, original_stanza)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (subject) {
|
|
|
|
u.safeSave(this, {'subject': {'author': sender, 'text': subject}});
|
|
|
|
}
|
|
|
|
if (sender === '') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.incrementUnreadMsgCounter(original_stanza);
|
|
|
|
this.createMessage(stanza, delay, original_stanza);
|
|
|
|
if (sender !== this.get('nick')) {
|
|
|
|
// We only emit an event if it's not our own message
|
|
|
|
_converse.emit('message', {'stanza': original_stanza, 'chatbox': this});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onPresence (pres) {
|
|
|
|
/* Handles all MUC presence stanzas.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (XMLElement) pres: The stanza
|
|
|
|
*/
|
|
|
|
if (pres.getAttribute('type') === 'error') {
|
|
|
|
this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const is_self = pres.querySelector("status[code='110']");
|
|
|
|
if (is_self && pres.getAttribute('type') !== 'unavailable') {
|
|
|
|
this.onOwnPresence(pres);
|
|
|
|
}
|
|
|
|
this.updateOccupantsOnPresence(pres);
|
|
|
|
if (this.get('role') !== 'none' && this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
|
|
|
|
this.save('connection_status', converse.ROOMSTATUS.CONNECTED);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onOwnPresence (pres) {
|
|
|
|
/* Handles a received presence relating to the current
|
|
|
|
* user.
|
|
|
|
*
|
|
|
|
* For locked rooms (which are by definition "new"), the
|
|
|
|
* room will either be auto-configured or created instantly
|
|
|
|
* (with default config) or a configuration room will be
|
|
|
|
* rendered.
|
|
|
|
*
|
|
|
|
* If the room is not locked, then the room will be
|
|
|
|
* auto-configured only if applicable and if the current
|
|
|
|
* user is the room's owner.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (XMLElement) pres: The stanza
|
|
|
|
*/
|
|
|
|
this.saveAffiliationAndRole(pres);
|
|
|
|
|
|
|
|
const locked_room = pres.querySelector("status[code='201']");
|
|
|
|
if (locked_room) {
|
|
|
|
if (this.get('auto_configure')) {
|
|
|
|
this.autoConfigureChatRoom().then(this.getRoomFeatures.bind(this));
|
|
|
|
} else if (_converse.muc_instant_rooms) {
|
|
|
|
// Accept default configuration
|
|
|
|
this.saveConfiguration().then(this.getRoomFeatures.bind(this));
|
|
|
|
} else {
|
|
|
|
this.trigger('configurationNeeded');
|
|
|
|
return; // We haven't yet entered the room, so bail here.
|
|
|
|
}
|
|
|
|
} else if (!this.get('features_fetched')) {
|
|
|
|
// The features for this room weren't fetched.
|
|
|
|
// That must mean it's a new room without locking
|
|
|
|
// (in which case Prosody doesn't send a 201 status),
|
|
|
|
// otherwise the features would have been fetched in
|
|
|
|
// the "initialize" method already.
|
|
|
|
if (this.get('affiliation') === 'owner' && this.get('auto_configure')) {
|
|
|
|
this.autoConfigureChatRoom().then(this.getRoomFeatures.bind(this));
|
|
|
|
} else {
|
|
|
|
this.getRoomFeatures();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.save('connection_status', converse.ROOMSTATUS.ENTERED);
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
isUserMentioned (message) {
|
2017-05-23 18:36:40 +02:00
|
|
|
/* Returns a boolean to indicate whether the current user
|
|
|
|
* was mentioned in a message.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (String): The text message
|
|
|
|
*/
|
2017-07-10 17:46:22 +02:00
|
|
|
return (new RegExp(`\\b${this.get('nick')}\\b`)).test(message);
|
2017-05-23 18:36:40 +02:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
incrementUnreadMsgCounter (stanza) {
|
2017-05-23 18:36:40 +02:00
|
|
|
/* Given a newly received message, update the unread counter if
|
|
|
|
* necessary.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (XMLElement): The <messsage> stanza
|
|
|
|
*/
|
2017-06-12 20:30:58 +02:00
|
|
|
const body = stanza.querySelector('body');
|
2017-05-23 18:36:40 +02:00
|
|
|
if (_.isNull(body)) {
|
|
|
|
return; // The message has no text
|
|
|
|
}
|
2017-12-06 14:59:01 +01:00
|
|
|
if (u.isNewMessage(stanza) && this.newMessageWillBeHidden()) {
|
2018-03-29 10:16:41 +02:00
|
|
|
const settings = {'num_unread_general': this.get('num_unread_general') + 1};
|
2017-05-24 08:40:09 +02:00
|
|
|
if (this.isUserMentioned(body.textContent)) {
|
2018-03-29 10:16:41 +02:00
|
|
|
settings.num_unread = this.get('num_unread') + 1;
|
2017-05-24 08:40:09 +02:00
|
|
|
_converse.incrementMsgCounter();
|
|
|
|
}
|
2018-03-29 10:16:41 +02:00
|
|
|
this.save(settings);
|
2017-05-23 18:36:40 +02:00
|
|
|
}
|
|
|
|
},
|
2017-05-24 08:40:09 +02:00
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
clearUnreadMsgCounter() {
|
2017-12-06 14:59:01 +01:00
|
|
|
u.safeSave(this, {
|
2017-06-19 11:08:57 +02:00
|
|
|
'num_unread': 0,
|
|
|
|
'num_unread_general': 0
|
|
|
|
});
|
2017-05-24 08:40:09 +02:00
|
|
|
}
|
2017-05-23 18:36:40 +02:00
|
|
|
});
|
|
|
|
|
2016-02-16 08:46:47 +01:00
|
|
|
|
2018-02-21 22:29:21 +01:00
|
|
|
_converse.ChatRoomOccupant = Backbone.Model.extend({
|
|
|
|
initialize (attributes) {
|
|
|
|
this.set(_.extend({
|
|
|
|
'id': _converse.connection.getUniqueId(),
|
|
|
|
}, attributes));
|
|
|
|
}
|
|
|
|
});
|
2017-12-14 14:07:39 +01:00
|
|
|
|
2017-12-07 07:05:37 +01:00
|
|
|
|
2018-02-21 22:29:21 +01:00
|
|
|
_converse.ChatRoomOccupants = Backbone.Collection.extend({
|
|
|
|
model: _converse.ChatRoomOccupant,
|
2017-02-18 11:03:26 +01:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
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;
|
2017-02-17 22:17:19 +01:00
|
|
|
}
|
|
|
|
},
|
2018-02-21 22:40:51 +01:00
|
|
|
});
|
2017-02-17 22:17:19 +01:00
|
|
|
|
2017-12-20 11:02:01 +01:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
_converse.RoomsPanelModel = Backbone.Model.extend({
|
|
|
|
defaults: {
|
|
|
|
'muc_domain': '',
|
2017-02-17 22:17:19 +01:00
|
|
|
},
|
2018-02-21 22:40:51 +01:00
|
|
|
});
|
2017-02-17 22:17:19 +01:00
|
|
|
|
2017-02-15 21:30:32 +01:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
_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.
|
|
|
|
*/
|
2018-03-04 06:37:11 +01:00
|
|
|
const x_el = sizzle('x[xmlns="jabber:x:conference"]', message).pop(),
|
2018-02-21 22:40:51 +01:00
|
|
|
from = Strophe.getBareJidFromJid(message.getAttribute('from')),
|
|
|
|
room_jid = x_el.getAttribute('jid'),
|
|
|
|
reason = x_el.getAttribute('reason');
|
2016-10-27 13:30:58 +02:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
let contact = _converse.roster.get(from),
|
|
|
|
result;
|
2016-03-28 13:42:33 +02:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
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)
|
|
|
|
);
|
2017-12-14 18:30:05 +01:00
|
|
|
} else {
|
2018-02-21 22:40:51 +01:00
|
|
|
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)
|
2017-07-10 17:46:22 +02:00
|
|
|
);
|
2016-12-06 16:18:33 +01:00
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
}
|
|
|
|
if (result === true) {
|
|
|
|
const chatroom = _converse.openChatRoom(
|
|
|
|
room_jid, {'password': x_el.getAttribute('password') });
|
2016-12-07 11:34:02 +01:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
if (chatroom.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
|
|
|
|
_converse.chatboxviews.get(room_jid).join();
|
2016-12-06 21:04:08 +01:00
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
}
|
|
|
|
};
|
2016-12-06 21:04:08 +01:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
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);
|
|
|
|
}
|
2016-12-06 16:18:33 +01:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
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);
|
|
|
|
};
|
2016-02-16 08:46:47 +01:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
const createChatRoom = function (jid, attrs) {
|
|
|
|
return getChatRoom(jid, attrs, true);
|
|
|
|
};
|
2016-08-12 14:52:33 +02:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
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) {
|
2017-02-17 22:17:19 +01:00
|
|
|
return;
|
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
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);
|
2016-08-12 16:40:04 +02:00
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
});
|
|
|
|
_converse.emit('roomsAutoJoined');
|
|
|
|
}
|
2016-02-16 08:46:47 +01:00
|
|
|
|
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
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);
|
2018-04-08 19:44:53 +02:00
|
|
|
view.model.registerHandlers();
|
2018-02-21 22:40:51 +01:00
|
|
|
view.join();
|
|
|
|
view.fetchMessages();
|
2016-02-16 08:46:47 +01:00
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
});
|
|
|
|
}
|
2016-02-16 08:46:47 +01:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
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);
|
2016-03-16 10:03:00 +01:00
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
});
|
|
|
|
}
|
2016-03-16 10:03:00 +01:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
/************************ 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 ************************/
|
2016-12-03 17:39:59 +01:00
|
|
|
|
2016-04-05 13:23:16 +02:00
|
|
|
|
2018-02-21 22:40:51 +01:00
|
|
|
/************************ BEGIN API ************************/
|
|
|
|
// We extend the default converse.js API to add methods specific to MUC chat rooms.
|
2016-12-20 10:30:20 +01:00
|
|
|
_.extend(_converse.api, {
|
2016-02-16 08:46:47 +01:00
|
|
|
'rooms': {
|
2017-07-10 17:46:22 +02:00
|
|
|
'close' (jids) {
|
2017-01-26 15:49:02 +01:00
|
|
|
if (_.isUndefined(jids)) {
|
2018-03-11 13:56:53 +01:00
|
|
|
// FIXME: can't access views here
|
2016-12-20 10:30:20 +01:00
|
|
|
_converse.chatboxviews.each(function (view) {
|
2016-06-03 10:22:31 +02:00
|
|
|
if (view.is_chatroom && view.model) {
|
|
|
|
view.close();
|
|
|
|
}
|
|
|
|
});
|
2017-01-26 15:49:02 +01:00
|
|
|
} else if (_.isString(jids)) {
|
2017-06-12 20:30:58 +02:00
|
|
|
const view = _converse.chatboxviews.get(jids);
|
2016-06-03 10:22:31 +02:00
|
|
|
if (view) { view.close(); }
|
|
|
|
} else {
|
2017-01-26 15:49:02 +01:00
|
|
|
_.each(jids, function (jid) {
|
2017-06-12 20:30:58 +02:00
|
|
|
const view = _converse.chatboxviews.get(jid);
|
2016-06-03 10:22:31 +02:00
|
|
|
if (view) { view.close(); }
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
2018-02-21 22:40:51 +01:00
|
|
|
'create' (jids, attrs) {
|
2017-01-26 15:49:02 +01:00
|
|
|
if (_.isString(attrs)) {
|
2016-08-19 17:16:36 +02:00
|
|
|
attrs = {'nick': attrs};
|
2017-01-26 15:49:02 +01:00
|
|
|
} else if (_.isUndefined(attrs)) {
|
2016-08-19 17:16:36 +02:00
|
|
|
attrs = {};
|
|
|
|
}
|
2016-11-30 11:00:12 +01:00
|
|
|
if (_.isUndefined(attrs.maximize)) {
|
|
|
|
attrs.maximize = false;
|
|
|
|
}
|
2016-12-20 10:30:20 +01:00
|
|
|
if (!attrs.nick && _converse.muc_nickname_from_jid) {
|
|
|
|
attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
|
2016-10-13 18:22:37 +02:00
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
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) {
|
2017-01-26 15:49:02 +01:00
|
|
|
if (_.isUndefined(jids)) {
|
2016-02-16 08:46:47 +01:00
|
|
|
throw new TypeError('rooms.open: You need to provide at least one JID');
|
2017-01-26 15:49:02 +01:00
|
|
|
} else if (_.isString(jids)) {
|
2018-02-21 22:40:51 +01:00
|
|
|
return _converse.api.rooms.create(jids, attrs).trigger('show');
|
2016-02-16 08:46:47 +01:00
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
return _.map(jids, (jid) => _converse.api.rooms.create(jid, attrs).trigger('show'));
|
2016-02-16 08:46:47 +01:00
|
|
|
},
|
2017-07-10 17:46:22 +02:00
|
|
|
'get' (jids, attrs, create) {
|
2017-01-26 15:49:02 +01:00
|
|
|
if (_.isString(attrs)) {
|
2016-08-19 17:16:36 +02:00
|
|
|
attrs = {'nick': attrs};
|
2017-01-26 15:49:02 +01:00
|
|
|
} else if (_.isUndefined(attrs)) {
|
2016-08-19 17:16:36 +02:00
|
|
|
attrs = {};
|
|
|
|
}
|
2017-01-26 15:49:02 +01:00
|
|
|
if (_.isUndefined(jids)) {
|
2017-06-12 20:30:58 +02:00
|
|
|
const result = [];
|
2016-12-20 10:30:20 +01:00
|
|
|
_converse.chatboxes.each(function (chatbox) {
|
2018-02-21 22:40:51 +01:00
|
|
|
if (chatbox.get('type') === converse.CHATROOMS_TYPE) {
|
|
|
|
result.push(chatbox);
|
2016-06-24 10:54:39 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
2016-08-19 17:16:36 +02:00
|
|
|
if (!attrs.nick) {
|
2016-12-20 10:30:20 +01:00
|
|
|
attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
|
2016-06-16 17:20:11 +02:00
|
|
|
}
|
2017-01-26 15:49:02 +01:00
|
|
|
if (_.isString(jids)) {
|
2018-03-11 13:56:53 +01:00
|
|
|
return getChatRoom(jids, attrs);
|
2017-11-10 15:53:59 +01:00
|
|
|
}
|
2018-03-11 13:56:53 +01:00
|
|
|
return _.map(jids, _.partial(getChatRoom, _, attrs));
|
2017-11-10 15:53:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2018-02-21 22:40:51 +01:00
|
|
|
/************************ END API ************************/
|
2016-02-16 08:46:47 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}));
|