2019-07-11 10:48:52 +02:00
|
|
|
/**
|
|
|
|
* @module converse-muc
|
2020-01-26 16:21:20 +01:00
|
|
|
* @copyright The Converse.js contributors
|
2019-09-11 15:23:58 +02:00
|
|
|
* @license Mozilla Public License (MPLv2)
|
|
|
|
* @description Implements the non-view logic for XEP-0045 Multi-User Chat
|
2019-07-11 10:48:52 +02:00
|
|
|
*/
|
2019-11-26 11:46:02 +01:00
|
|
|
import "./converse-chat";
|
2018-10-23 03:41:38 +02:00
|
|
|
import "./converse-disco";
|
2019-07-11 12:30:51 +02:00
|
|
|
import "./converse-emoji";
|
2019-09-19 16:54:55 +02:00
|
|
|
import { Collection } from "skeletor.js/src/collection";
|
|
|
|
import { Model } from 'skeletor.js/src/model.js';
|
2020-03-24 12:26:34 +01:00
|
|
|
import { clone, debounce, intersection, invoke, isElement, isObject, isString, pick, uniq, zipObject } from "lodash";
|
2020-04-23 13:22:31 +02:00
|
|
|
import { converse } from "./converse-core";
|
2019-11-06 11:01:34 +01:00
|
|
|
import log from "./log";
|
2019-11-07 09:08:13 +01:00
|
|
|
import muc_utils from "./utils/muc";
|
2020-04-13 15:20:51 +02:00
|
|
|
import st from "./utils/stanza";
|
2018-10-23 03:41:38 +02:00
|
|
|
import u from "./utils/form";
|
|
|
|
|
2020-03-31 22:43:55 +02:00
|
|
|
converse.MUC_TRAFFIC_STATES = ['entered', 'exited'];
|
2020-04-23 13:36:16 +02:00
|
|
|
converse.MUC_ROLE_CHANGES = ['op', 'deop', 'voice', 'mute'];
|
2020-03-31 22:43:55 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
const MUC_ROLE_WEIGHTS = {
|
|
|
|
'moderator': 1,
|
|
|
|
'participant': 2,
|
|
|
|
'visitor': 3,
|
|
|
|
'none': 2,
|
|
|
|
};
|
|
|
|
|
2019-09-19 16:54:55 +02:00
|
|
|
const { Strophe, $iq, $build, $msg, $pres, sizzle } = converse.env;
|
2018-10-23 03:41:38 +02: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");
|
2020-04-13 15:19:21 +02:00
|
|
|
Strophe.addNamespace('MUC_HATS', "xmpp:prosody.im/protocol/hats:1");
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
converse.MUC_NICK_CHANGED_CODE = "303";
|
|
|
|
|
|
|
|
converse.ROOM_FEATURES = [
|
|
|
|
'passwordprotected', 'unsecured', 'hidden',
|
|
|
|
'publicroom', 'membersonly', 'open', 'persistent',
|
|
|
|
'temporary', 'nonanonymous', 'semianonymous',
|
|
|
|
'moderated', 'unmoderated', 'mam_enabled'
|
|
|
|
];
|
|
|
|
|
2018-12-13 09:48:43 +01:00
|
|
|
// No longer used in code, but useful as reference.
|
|
|
|
//
|
|
|
|
// const ROOM_FEATURES_MAP = {
|
|
|
|
// 'passwordprotected': 'unsecured',
|
|
|
|
// 'unsecured': 'passwordprotected',
|
|
|
|
// 'hidden': 'publicroom',
|
|
|
|
// 'publicroom': 'hidden',
|
|
|
|
// 'membersonly': 'open',
|
|
|
|
// 'open': 'membersonly',
|
|
|
|
// 'persistent': 'temporary',
|
|
|
|
// 'temporary': 'persistent',
|
|
|
|
// 'nonanonymous': 'semianonymous',
|
|
|
|
// 'semianonymous': 'nonanonymous',
|
|
|
|
// 'moderated': 'unmoderated',
|
|
|
|
// 'unmoderated': 'moderated'
|
|
|
|
// };
|
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
converse.ROOMSTATUS = {
|
|
|
|
CONNECTED: 0,
|
|
|
|
CONNECTING: 1,
|
|
|
|
NICKNAME_REQUIRED: 2,
|
|
|
|
PASSWORD_REQUIRED: 3,
|
|
|
|
DISCONNECTED: 4,
|
2019-06-11 12:16:27 +02:00
|
|
|
ENTERED: 5,
|
|
|
|
DESTROYED: 6
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
2019-11-01 16:04:55 +01:00
|
|
|
dependencies: ["converse-chatboxes", "converse-chat", "converse-disco", "converse-controlbox"],
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
overrides: {
|
|
|
|
ChatBoxes: {
|
|
|
|
model (attrs, options) {
|
|
|
|
const { _converse } = this.__super__;
|
2019-10-24 14:29:15 +02:00
|
|
|
if (attrs && attrs.type == _converse.CHATROOMS_TYPE) {
|
2018-10-23 03:41:38 +02:00
|
|
|
return new _converse.ChatRoom(attrs, options);
|
|
|
|
} else {
|
|
|
|
return this.__super__.model.apply(this, arguments);
|
|
|
|
}
|
2017-06-23 18:21:50 +02:00
|
|
|
},
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
},
|
2017-06-23 18:21:50 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
initialize () {
|
|
|
|
/* The initialize function gets called as soon as the plugin is
|
|
|
|
* loaded by converse.js's plugin machinery.
|
|
|
|
*/
|
2019-10-16 17:17:29 +02:00
|
|
|
const { _converse } = this;
|
2020-03-31 13:15:57 +02:00
|
|
|
const { api } = _converse;
|
2019-10-16 17:17:29 +02:00
|
|
|
const { __, ___ } = _converse;
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
// Configuration values for this plugin
|
|
|
|
// ====================================
|
|
|
|
// Refer to docs/source/configuration.rst for explanations of these
|
|
|
|
// configuration settings.
|
2020-03-31 13:15:57 +02:00
|
|
|
api.settings.update({
|
2019-05-21 13:05:55 +02:00
|
|
|
'allow_muc': true,
|
|
|
|
'allow_muc_invitations': true,
|
|
|
|
'auto_join_on_invite': false,
|
|
|
|
'auto_join_rooms': [],
|
|
|
|
'auto_register_muc_nickname': false,
|
|
|
|
'locked_muc_domain': false,
|
|
|
|
'muc_domain': undefined,
|
2019-07-26 13:32:21 +02:00
|
|
|
'muc_fetch_members': true,
|
2019-05-21 13:05:55 +02:00
|
|
|
'muc_history_max_stanzas': undefined,
|
|
|
|
'muc_instant_rooms': true,
|
2019-11-28 17:01:08 +01:00
|
|
|
'muc_nickname_from_jid': false,
|
2020-05-05 15:32:15 +02:00
|
|
|
'muc_send_probes': false,
|
2020-03-31 22:43:55 +02:00
|
|
|
'muc_show_join_leave': true,
|
2019-11-28 17:01:08 +01:00
|
|
|
'muc_show_logs_before_join': false
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
2020-03-31 13:15:57 +02:00
|
|
|
api.promises.add(['roomsAutoJoined']);
|
2018-10-23 03:41:38 +02:00
|
|
|
|
2020-03-31 13:15:57 +02:00
|
|
|
if (api.settings.get('locked_muc_domain') && !isString(api.settings.get('muc_domain'))) {
|
2019-05-21 13:05:55 +02:00
|
|
|
throw new Error("Config Error: it makes no sense to set locked_muc_domain "+
|
|
|
|
"to true when muc_domain is not set");
|
|
|
|
}
|
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
|
2019-06-11 12:16:27 +02:00
|
|
|
/* https://xmpp.org/extensions/xep-0045.html
|
|
|
|
* ----------------------------------------
|
|
|
|
* 100 message Entering a groupchat 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 groupchat
|
|
|
|
* 102 message Configuration change Inform occupants that groupchat now shows unavailable members
|
|
|
|
* 103 message Configuration change Inform occupants that groupchat now does not show unavailable members
|
|
|
|
* 104 message Configuration change Inform occupants that a non-privacy-related groupchat configuration change has occurred
|
|
|
|
* 110 presence Any groupchat presence Inform user that presence refers to one of its own groupchat occupants
|
|
|
|
* 170 message or initial presence Configuration change Inform occupants that groupchat logging is now enabled
|
|
|
|
* 171 message Configuration change Inform occupants that groupchat logging is now disabled
|
|
|
|
* 172 message Configuration change Inform occupants that the groupchat is now non-anonymous
|
|
|
|
* 173 message Configuration change Inform occupants that the groupchat is now semi-anonymous
|
|
|
|
* 174 message Configuration change Inform occupants that the groupchat is now fully-anonymous
|
|
|
|
* 201 presence Entering a groupchat Inform user that a new groupchat has been created
|
|
|
|
* 210 presence Entering a groupchat Inform user that the service has assigned or modified the occupant's roomnick
|
|
|
|
* 301 presence Removal from groupchat Inform user that he or she has been banned from the groupchat
|
|
|
|
* 303 presence Exiting a groupchat Inform all occupants of new groupchat nickname
|
|
|
|
* 307 presence Removal from groupchat Inform user that he or she has been kicked from the groupchat
|
|
|
|
* 321 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of an affiliation change
|
|
|
|
* 322 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member
|
|
|
|
* 332 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of a system shutdown
|
|
|
|
*/
|
|
|
|
_converse.muc = {
|
|
|
|
info_messages: {
|
|
|
|
100: __('This groupchat is not anonymous'),
|
|
|
|
102: __('This groupchat now shows unavailable members'),
|
|
|
|
103: __('This groupchat does not show unavailable members'),
|
|
|
|
104: __('The groupchat configuration has changed'),
|
2019-09-11 14:17:23 +02:00
|
|
|
170: __('Groupchat logging is now enabled'),
|
|
|
|
171: __('Groupchat logging is now disabled'),
|
2019-06-11 12:16:27 +02:00
|
|
|
172: __('This groupchat is now no longer anonymous'),
|
|
|
|
173: __('This groupchat is now semi-anonymous'),
|
|
|
|
174: __('This groupchat is now fully-anonymous'),
|
|
|
|
201: __('A new groupchat has been created')
|
|
|
|
},
|
|
|
|
|
|
|
|
new_nickname_messages: {
|
|
|
|
// XXX: Note the triple underscore function and not double underscore.
|
|
|
|
210: ___('Your nickname has been automatically set to %1$s'),
|
|
|
|
303: ___('Your nickname has been changed to %1$s')
|
|
|
|
},
|
|
|
|
|
|
|
|
disconnect_messages: {
|
|
|
|
301: __('You have been banned from this groupchat'),
|
|
|
|
307: __('You have been kicked from this groupchat'),
|
|
|
|
321: __("You have been removed from this groupchat because of an affiliation change"),
|
|
|
|
322: __("You have been removed from this groupchat because the groupchat has changed to members-only and you're not a member"),
|
|
|
|
332: __("You have been removed from this groupchat because the service hosting it is being shut down")
|
|
|
|
},
|
|
|
|
|
2020-02-14 13:19:12 +01:00
|
|
|
action_info_codes: ['301', '303', '307', '321', '322']
|
2019-06-11 12:16:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-10-24 18:28:28 +02:00
|
|
|
async function openRoom (jid) {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (!u.isValidMUCJID(jid)) {
|
2019-11-06 11:01:34 +01:00
|
|
|
return log.warn(`invalid jid "${jid}" provided in url fragment`);
|
2016-02-28 20:24:06 +01:00
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
await api.waitUntil('roomsAutoJoined');
|
2018-10-23 03:41:38 +02:00
|
|
|
if (_converse.allow_bookmarks) {
|
2020-03-31 13:15:57 +02:00
|
|
|
await api.waitUntil('bookmarksInitialized');
|
2017-10-31 22:08:06 +01:00
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
api.rooms.open(jid);
|
2018-10-23 03:41:38 +02: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
|
|
|
|
2019-03-26 11:31:11 +01:00
|
|
|
_converse.getDefaultMUCNickname = function () {
|
2019-03-26 13:29:33 +01:00
|
|
|
// XXX: if anything changes here, update the docs for the
|
|
|
|
// locked_muc_nickname setting.
|
2019-03-26 11:31:11 +01:00
|
|
|
if (!_converse.xmppstatus) {
|
2019-03-26 13:29:33 +01:00
|
|
|
throw new Error(
|
|
|
|
"Can't call _converse.getDefaultMUCNickname before the statusInitialized has been fired.");
|
2019-03-26 11:31:11 +01:00
|
|
|
}
|
2019-09-04 19:11:26 +02:00
|
|
|
const nick = _converse.xmppstatus.getNickname();
|
2019-03-26 11:31:11 +01:00
|
|
|
if (nick) {
|
|
|
|
return nick;
|
2020-03-31 13:15:57 +02:00
|
|
|
} else if (api.settings.get('muc_nickname_from_jid')) {
|
2019-03-26 11:31:11 +01:00
|
|
|
return Strophe.unescapeNode(Strophe.getNodeFromJid(_converse.bare_jid));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-04 18:26:33 +02:00
|
|
|
async function openChatRoom (jid, settings) {
|
2018-10-23 03:41:38 +02:00
|
|
|
/* Opens a groupchat, making sure that certain attributes
|
|
|
|
* are correct, for example that the "type" is set to
|
|
|
|
* "chatroom".
|
|
|
|
*/
|
|
|
|
settings.type = _converse.CHATROOMS_TYPE;
|
|
|
|
settings.id = jid;
|
2020-03-31 13:15:57 +02:00
|
|
|
const chatbox = await api.rooms.get(jid, settings, true);
|
2019-04-16 16:36:58 +02:00
|
|
|
chatbox.maybeShow(true);
|
2018-10-23 03:41:38 +02:00
|
|
|
return chatbox;
|
|
|
|
}
|
2016-02-16 08:46:47 +01:00
|
|
|
|
2019-07-01 10:05:42 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents a MUC message
|
|
|
|
* @class
|
|
|
|
* @namespace _converse.ChatRoomMessage
|
|
|
|
* @memberOf _converse
|
|
|
|
*/
|
|
|
|
_converse.ChatRoomMessage = _converse.Message.extend({
|
|
|
|
|
2019-07-01 10:44:59 +02:00
|
|
|
initialize () {
|
2020-02-27 15:09:54 +01:00
|
|
|
if (!this.checkValidity()) { return; }
|
2019-07-01 10:44:59 +02:00
|
|
|
if (this.get('file')) {
|
|
|
|
this.on('change:put', this.uploadFile, this);
|
|
|
|
}
|
2019-09-24 15:33:41 +02:00
|
|
|
if (!this.setTimerForEphemeralMessage()) {
|
2019-07-01 10:44:59 +02:00
|
|
|
this.setOccupant();
|
|
|
|
}
|
2019-12-18 12:42:40 +01:00
|
|
|
/**
|
|
|
|
* Triggered once a {@link _converse.ChatRoomMessageInitialized} has been created and initialized.
|
|
|
|
* @event _converse#chatRoomMessageInitialized
|
|
|
|
* @type { _converse.ChatRoomMessages}
|
|
|
|
* @example _converse.api.listen.on('chatRoomMessageInitialized', model => { ... });
|
|
|
|
*/
|
2020-03-31 13:15:57 +02:00
|
|
|
api.trigger('chatRoomMessageInitialized', this);
|
2019-07-01 10:44:59 +02:00
|
|
|
},
|
|
|
|
|
2020-03-26 10:35:08 +01:00
|
|
|
/**
|
|
|
|
* Determines whether this messsage may be moderated,
|
|
|
|
* based on configuration settings and server support.
|
|
|
|
* @async
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoomMessages#mayBeModerated
|
|
|
|
* @returns { Boolean }
|
|
|
|
*/
|
|
|
|
mayBeModerated () {
|
2020-03-31 13:15:57 +02:00
|
|
|
return ['all', 'moderator'].includes(api.settings.get('allow_message_retraction')) &&
|
2020-03-26 10:35:08 +01:00
|
|
|
this.collection.chatbox.canModerateMessages();
|
|
|
|
},
|
|
|
|
|
2020-02-27 15:56:23 +01:00
|
|
|
checkValidity () {
|
|
|
|
const result = _converse.Message.prototype.checkValidity.call(this);
|
|
|
|
!result && this.collection.chatbox.debouncedRejoin();
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
|
2019-10-09 16:01:38 +02:00
|
|
|
onOccupantRemoved () {
|
2019-09-06 13:10:37 +02:00
|
|
|
this.stopListening(this.occupant);
|
2019-08-14 09:40:45 +02:00
|
|
|
delete this.occupant;
|
2020-03-24 12:26:34 +01:00
|
|
|
const chatbox = this?.collection?.chatbox;
|
2019-09-06 13:10:37 +02:00
|
|
|
if (!chatbox) {
|
2019-11-06 11:01:34 +01:00
|
|
|
return log.error(`Could not get collection.chatbox for message: ${JSON.stringify(this.toJSON())}`);
|
2019-09-06 13:10:37 +02:00
|
|
|
}
|
|
|
|
this.listenTo(chatbox.occupants, 'add', this.onOccupantAdded);
|
2019-08-14 09:40:45 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
onOccupantAdded (occupant) {
|
|
|
|
if (occupant.get('nick') === Strophe.getResourceFromJid(this.get('from'))) {
|
|
|
|
this.occupant = occupant;
|
2020-05-05 14:38:40 +02:00
|
|
|
this.trigger('occupantAdded');
|
2019-09-06 13:10:37 +02:00
|
|
|
this.listenTo(this.occupant, 'destroy', this.onOccupantRemoved);
|
2020-03-24 12:26:34 +01:00
|
|
|
const chatbox = this?.collection?.chatbox;
|
2019-09-06 13:10:37 +02:00
|
|
|
if (!chatbox) {
|
2019-11-06 11:01:34 +01:00
|
|
|
return log.error(`Could not get collection.chatbox for message: ${JSON.stringify(this.toJSON())}`);
|
2019-09-06 13:10:37 +02:00
|
|
|
}
|
|
|
|
this.stopListening(chatbox.occupants, 'add', this.onOccupantAdded);
|
2019-08-14 09:40:45 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-07-01 10:44:59 +02:00
|
|
|
setOccupant () {
|
2019-08-14 09:40:45 +02:00
|
|
|
if (this.get('type') !== 'groupchat') { return; }
|
2020-03-24 12:26:34 +01:00
|
|
|
const chatbox = this?.collection?.chatbox;
|
2019-09-06 13:10:37 +02:00
|
|
|
if (!chatbox) {
|
2019-11-06 11:01:34 +01:00
|
|
|
return log.error(`Could not get collection.chatbox for message: ${JSON.stringify(this.toJSON())}`);
|
2019-09-06 13:10:37 +02:00
|
|
|
}
|
2019-07-01 10:44:59 +02:00
|
|
|
const nick = Strophe.getResourceFromJid(this.get('from'));
|
2020-05-05 15:32:15 +02:00
|
|
|
this.occupant = chatbox.occupants.findWhere({ nick });
|
|
|
|
|
|
|
|
if (!this.occupant && api.settings.get("muc_send_probes")) {
|
|
|
|
this.occupant = chatbox.occupants.create({ nick, 'type': 'unavailable' });
|
|
|
|
const jid = `${chatbox.get('jid')}/${nick}`;
|
|
|
|
api.user.presence.send('probe', jid);
|
|
|
|
}
|
|
|
|
|
2019-08-14 09:40:45 +02:00
|
|
|
if (this.occupant) {
|
2019-09-06 13:10:37 +02:00
|
|
|
this.listenTo(this.occupant, 'destroy', this.onOccupantRemoved);
|
2019-08-14 09:40:45 +02:00
|
|
|
} else {
|
2019-09-06 13:10:37 +02:00
|
|
|
this.listenTo(chatbox.occupants, 'add', this.onOccupantAdded);
|
2019-08-14 09:40:45 +02:00
|
|
|
}
|
2019-12-18 12:42:40 +01:00
|
|
|
}
|
2019-07-01 10:05:42 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
2019-09-19 16:54:55 +02:00
|
|
|
const MUCSession = Model.extend({
|
2019-12-10 15:40:33 +01:00
|
|
|
defaults () {
|
|
|
|
return {
|
|
|
|
'connection_status': converse.ROOMSTATUS.DISCONNECTED
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2019-07-01 10:05:42 +02:00
|
|
|
/**
|
|
|
|
* Collection which stores MUC messages
|
|
|
|
* @class
|
|
|
|
* @namespace _converse.ChatRoomMessages
|
|
|
|
* @memberOf _converse
|
|
|
|
*/
|
2019-09-19 16:54:55 +02:00
|
|
|
_converse.ChatRoomMessages = Collection.extend({
|
2019-07-01 10:05:42 +02:00
|
|
|
model: _converse.ChatRoomMessage,
|
|
|
|
comparator: 'time'
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2019-03-29 15:47:23 +01:00
|
|
|
/**
|
|
|
|
* Represents an open/ongoing groupchat conversation.
|
|
|
|
* @class
|
|
|
|
* @namespace _converse.ChatRoom
|
|
|
|
* @memberOf _converse
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
_converse.ChatRoom = _converse.ChatBox.extend({
|
2019-07-01 10:05:42 +02:00
|
|
|
messagesCollection: _converse.ChatRoomMessages,
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
defaults () {
|
2019-04-17 11:52:41 +02:00
|
|
|
return {
|
|
|
|
// 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,
|
|
|
|
'bookmarked': false,
|
|
|
|
'chat_state': undefined,
|
2020-03-31 13:15:57 +02:00
|
|
|
'hidden': ['mobile', 'fullscreen'].includes(api.settings.get("view_mode")),
|
2019-04-17 11:52:41 +02:00
|
|
|
'message_type': 'groupchat',
|
|
|
|
'name': '',
|
|
|
|
'num_unread': 0,
|
|
|
|
'roomconfig': {},
|
2019-05-05 19:05:45 +02:00
|
|
|
'time_opened': this.get('time_opened') || (new Date()).getTime(),
|
2020-02-10 11:23:55 +01:00
|
|
|
'time_sent': (new Date(0)).toISOString(),
|
2019-04-17 11:52:41 +02:00
|
|
|
'type': _converse.CHATROOMS_TYPE
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2017-05-24 08:40:09 +02:00
|
|
|
|
2019-12-18 12:42:40 +01:00
|
|
|
async initialize () {
|
2019-11-08 09:49:58 +01:00
|
|
|
this.initialized = u.getResolveablePromise();
|
2020-02-27 15:56:23 +01:00
|
|
|
this.debouncedRejoin = debounce(this.rejoin, 250);
|
2019-04-17 11:52:41 +02:00
|
|
|
this.set('box_id', `box-${btoa(this.get('jid'))}`);
|
2019-05-21 09:56:27 +02:00
|
|
|
this.initMessages();
|
2020-03-31 22:43:55 +02:00
|
|
|
this.initNotifications();
|
2019-12-08 20:42:09 +01:00
|
|
|
this.initOccupants();
|
2020-01-16 13:01:57 +01:00
|
|
|
this.initDiscoModels(); // sendChatState depends on this.features
|
2019-08-14 09:40:45 +02:00
|
|
|
this.registerHandlers();
|
|
|
|
|
2020-01-09 17:01:11 +01:00
|
|
|
this.on('change:chat_state', this.sendChatState, this);
|
|
|
|
await this.restoreSession();
|
|
|
|
this.session.on('change:connection_status', this.onConnectionStatusChanged, this);
|
|
|
|
|
2020-03-31 22:43:55 +02:00
|
|
|
this.listenTo(this.occupants, 'add', this.onOccupantAdded);
|
|
|
|
this.listenTo(this.occupants, 'remove', this.onOccupantRemoved);
|
|
|
|
this.listenTo(this.occupants, 'change:show', this.onOccupantShowChanged);
|
2020-04-13 11:10:25 +02:00
|
|
|
this.listenTo(this.occupants, 'change:affiliation', this.createAffiliationChangeMessage);
|
|
|
|
this.listenTo(this.occupants, 'change:role', this.createRoleChangeMessage);
|
2020-03-31 22:43:55 +02:00
|
|
|
|
2019-12-08 20:42:09 +01:00
|
|
|
const restored = await this.restoreFromCache()
|
|
|
|
if (!restored) {
|
|
|
|
this.join();
|
|
|
|
}
|
2019-12-18 12:42:40 +01:00
|
|
|
/**
|
|
|
|
* Triggered once a {@link _converse.ChatRoom} has been created and initialized.
|
|
|
|
* @event _converse#chatRoomInitialized
|
|
|
|
* @type { _converse.ChatRoom }
|
|
|
|
* @example _converse.api.listen.on('chatRoomInitialized', model => { ... });
|
|
|
|
*/
|
2020-03-31 13:15:57 +02:00
|
|
|
await api.trigger('chatRoomInitialized', this, {'Synchronous': true});
|
2019-11-08 09:49:58 +01:00
|
|
|
this.initialized.resolve();
|
2019-05-19 22:11:26 +02:00
|
|
|
},
|
|
|
|
|
2019-12-08 20:42:09 +01:00
|
|
|
/**
|
|
|
|
* Checks whether we're still joined and if so, restores the MUC state from cache.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#restoreFromCache
|
|
|
|
* @returns { Boolean } Returns `true` if we're still joined, otherwise returns `false`.
|
|
|
|
*/
|
|
|
|
async restoreFromCache () {
|
2019-12-11 10:06:03 +01:00
|
|
|
if (this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED && (await this.isJoined())) {
|
2019-08-10 18:44:52 +02:00
|
|
|
// We've restored the room from cache and we're still joined.
|
2019-11-29 17:04:50 +01:00
|
|
|
await new Promise(resolve => this.features.fetch({'success': resolve, 'error': resolve}));
|
2019-12-08 20:42:09 +01:00
|
|
|
await this.fetchOccupants();
|
2019-11-29 17:04:50 +01:00
|
|
|
await this.fetchMessages();
|
2019-12-08 20:42:09 +01:00
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
await this.clearCache();
|
|
|
|
return false;
|
2019-05-21 12:04:55 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-12-08 20:42:09 +01:00
|
|
|
/**
|
|
|
|
* Join the MUC
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#join
|
|
|
|
* @param { String } nick - The user's nickname
|
|
|
|
* @param { String } [password] - Optional password, if required by the groupchat.
|
|
|
|
*/
|
|
|
|
async join (nick, password) {
|
2019-12-10 15:40:33 +01:00
|
|
|
if (this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
|
2019-12-08 20:42:09 +01:00
|
|
|
// We have restored a groupchat from session storage,
|
|
|
|
// so we don't send out a presence stanza again.
|
|
|
|
return this;
|
|
|
|
}
|
2020-01-16 13:01:57 +01:00
|
|
|
await this.refreshDiscoInfo();
|
2019-12-08 20:42:09 +01:00
|
|
|
nick = await this.getAndPersistNickname(nick);
|
|
|
|
if (!nick) {
|
2019-12-10 15:40:33 +01:00
|
|
|
u.safeSave(this.session, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
|
2020-03-31 13:15:57 +02:00
|
|
|
if (api.settings.get('muc_show_logs_before_join')) {
|
2019-12-08 20:42:09 +01:00
|
|
|
await this.fetchMessages();
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
const stanza = $pres({
|
|
|
|
'from': _converse.connection.jid,
|
|
|
|
'to': this.getRoomJIDAndNick()
|
|
|
|
}).c("x", {'xmlns': Strophe.NS.MUC})
|
2020-03-31 13:15:57 +02:00
|
|
|
.c("history", {'maxstanzas': this.features.get('mam_enabled') ? 0 : api.settings.get('muc_history_max_stanzas')}).up();
|
2019-12-08 20:42:09 +01:00
|
|
|
|
|
|
|
if (password) {
|
|
|
|
stanza.cnode(Strophe.xmlElement("password", [], password));
|
|
|
|
}
|
2019-12-10 15:40:33 +01:00
|
|
|
this.session.save('connection_status', converse.ROOMSTATUS.CONNECTING);
|
2020-03-31 13:15:57 +02:00
|
|
|
api.send(stanza);
|
2019-12-08 20:42:09 +01:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2020-04-14 12:14:19 +02:00
|
|
|
async fetchMessages () {
|
|
|
|
await _converse.ChatBox.prototype.fetchMessages.call(this);
|
|
|
|
const queued_messages = this.message_queue.map(m => this.queueMessage(m));
|
|
|
|
this.message_queue = [];
|
|
|
|
return Promise.all(queued_messages);
|
|
|
|
},
|
|
|
|
|
2019-12-08 20:42:09 +01:00
|
|
|
async clearCache () {
|
2019-12-10 15:40:33 +01:00
|
|
|
this.session.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
|
2019-12-08 20:42:09 +01:00
|
|
|
if (this.occupants.length) {
|
|
|
|
// Remove non-members when reconnecting
|
|
|
|
this.occupants.filter(o => !o.isMember()).forEach(o => o.destroy());
|
|
|
|
} else {
|
|
|
|
// Looks like we haven't restored occupants from cache, so we clear it.
|
2019-09-19 16:54:55 +02:00
|
|
|
this.occupants.clearStore();
|
2019-12-08 20:42:09 +01:00
|
|
|
}
|
|
|
|
if (_converse.clear_messages_on_reconnection) {
|
|
|
|
await this.clearMessages();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-03-31 22:43:55 +02:00
|
|
|
onOccupantAdded (occupant) {
|
|
|
|
if (api.settings.get('muc_show_join_leave') &&
|
|
|
|
this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED &&
|
|
|
|
occupant.get('show') === 'online') {
|
|
|
|
this.updateNotifications(occupant.get('nick'), 'entered');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onOccupantRemoved (occupant) {
|
|
|
|
if (api.settings.get('muc_show_join_leave') &&
|
|
|
|
this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED &&
|
|
|
|
occupant.get('show') === 'online') {
|
|
|
|
this.updateNotifications(occupant.get('nick'), 'exited');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onOccupantShowChanged (occupant) {
|
|
|
|
if (occupant.get('states').includes('303') || !api.settings.get('muc_show_join_leave')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (occupant.get('show') === 'offline') {
|
|
|
|
this.updateNotifications(occupant.get('nick'), 'exited');
|
|
|
|
} else if (occupant.get('show') === 'online') {
|
|
|
|
this.updateNotifications(occupant.get('nick'), 'entered');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-12-08 20:42:09 +01:00
|
|
|
/**
|
|
|
|
* Clear stale cache and re-join a MUC we've been in before.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#rejoin
|
|
|
|
*/
|
|
|
|
rejoin () {
|
|
|
|
this.clearCache();
|
|
|
|
return this.join();
|
|
|
|
},
|
|
|
|
|
2020-02-19 16:46:56 +01:00
|
|
|
initMessages () {
|
|
|
|
this.message_queue = [];
|
2020-04-14 12:14:19 +02:00
|
|
|
_converse.ChatBox.prototype.initMessages.call(this);
|
2020-02-19 16:46:56 +01:00
|
|
|
},
|
|
|
|
|
2019-05-19 22:11:26 +02:00
|
|
|
async onConnectionStatusChanged () {
|
2019-12-10 15:40:33 +01:00
|
|
|
if (this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
|
2020-02-20 17:11:07 +01:00
|
|
|
await this.occupants.fetchMembers();
|
2019-11-29 15:01:41 +01:00
|
|
|
await this.fetchMessages();
|
2019-07-29 15:41:03 +02:00
|
|
|
/**
|
2019-08-14 09:40:45 +02:00
|
|
|
* Triggered when the user has entered a new MUC
|
2019-07-29 15:41:03 +02:00
|
|
|
* @event _converse#enteredNewRoom
|
|
|
|
* @type { _converse.ChatRoom}
|
|
|
|
* @example _converse.api.listen.on('enteredNewRoom', model => { ... });
|
|
|
|
*/
|
2020-03-31 13:15:57 +02:00
|
|
|
api.trigger('enteredNewRoom', this);
|
2019-07-29 15:41:03 +02:00
|
|
|
|
2020-03-31 13:15:57 +02:00
|
|
|
if (api.settings.get('auto_register_muc_nickname') &&
|
|
|
|
await api.disco.supports(Strophe.NS.MUC_REGISTER, this.get('jid'))) {
|
2019-05-21 11:06:21 +02:00
|
|
|
this.registerNickname()
|
|
|
|
}
|
2019-05-19 22:11:26 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-05-28 14:00:33 +02:00
|
|
|
async onReconnection () {
|
2019-05-22 14:03:13 +02:00
|
|
|
this.registerHandlers();
|
2019-12-08 20:42:09 +01:00
|
|
|
await this.rejoin();
|
2019-05-22 18:03:04 +02:00
|
|
|
this.announceReconnection();
|
2019-05-22 14:03:13 +02:00
|
|
|
},
|
|
|
|
|
2019-12-11 10:06:26 +01:00
|
|
|
restoreSession () {
|
|
|
|
const id = `muc.session-${_converse.bare_jid}-${this.get('jid')}`;
|
|
|
|
this.session = new MUCSession({id});
|
|
|
|
this.session.browserStorage = _converse.createStore(id, "session");
|
|
|
|
return new Promise(r => this.session.fetch({'success': r, 'error': r}));
|
|
|
|
},
|
|
|
|
|
2020-01-16 13:01:57 +01:00
|
|
|
initDiscoModels () {
|
|
|
|
let id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
|
2019-09-19 16:54:55 +02:00
|
|
|
this.features = new Model(
|
2019-11-01 16:04:55 +01:00
|
|
|
Object.assign({id}, zipObject(converse.ROOM_FEATURES, converse.ROOM_FEATURES.map(() => false)))
|
2019-01-10 12:14:45 +01:00
|
|
|
);
|
2019-10-11 20:29:12 +02:00
|
|
|
this.features.browserStorage = _converse.createStore(id, "session");
|
2020-01-16 13:01:57 +01:00
|
|
|
|
|
|
|
id = `converse.muc-config-{_converse.bare_jid}-${this.get('jid')}`;
|
2019-09-19 16:54:55 +02:00
|
|
|
this.config = new Model();
|
2020-01-16 13:01:57 +01:00
|
|
|
this.config.browserStorage = _converse.createStore(id, "session");
|
2019-05-19 22:11:26 +02:00
|
|
|
},
|
2019-01-10 12:14:45 +01:00
|
|
|
|
2019-05-19 22:11:26 +02:00
|
|
|
initOccupants () {
|
2018-10-23 03:41:38 +02:00
|
|
|
this.occupants = new _converse.ChatRoomOccupants();
|
2019-10-11 20:29:12 +02:00
|
|
|
const id = `converse.occupants-${_converse.bare_jid}${this.get('jid')}`;
|
|
|
|
this.occupants.browserStorage = _converse.createStore(id, 'session');
|
2019-12-08 20:42:09 +01:00
|
|
|
this.occupants.chatroom = this;
|
|
|
|
},
|
|
|
|
|
|
|
|
fetchOccupants () {
|
2019-05-14 20:38:25 +02:00
|
|
|
this.occupants.fetched = new Promise(resolve => {
|
2019-05-15 14:30:43 +02:00
|
|
|
this.occupants.fetch({
|
2019-05-14 20:38:25 +02:00
|
|
|
'add': true,
|
|
|
|
'silent': true,
|
|
|
|
'success': resolve,
|
2019-05-15 14:30:43 +02:00
|
|
|
'error': resolve
|
2019-05-14 20:38:25 +02:00
|
|
|
});
|
|
|
|
});
|
2019-08-14 09:40:45 +02:00
|
|
|
return this.occupants.fetched;
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-04-08 19:44:53 +02:00
|
|
|
|
2019-12-06 13:09:09 +01:00
|
|
|
handleAffiliationChangedMessage (stanza) {
|
|
|
|
const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop();
|
|
|
|
if (item) {
|
|
|
|
const from = stanza.getAttribute("from");
|
|
|
|
const type = stanza.getAttribute("type");
|
|
|
|
const affiliation = item.getAttribute('affiliation');
|
|
|
|
const jid = item.getAttribute('jid');
|
|
|
|
const data = {
|
|
|
|
from, type, affiliation,
|
|
|
|
'nick': Strophe.getNodeFromJid(jid),
|
|
|
|
'states': [],
|
|
|
|
'show': type == 'unavailable' ? 'offline' : 'online',
|
|
|
|
'role': item.getAttribute('role'),
|
|
|
|
'jid': Strophe.getBareJidFromJid(jid),
|
|
|
|
'resource': Strophe.getResourceFromJid(jid)
|
|
|
|
}
|
|
|
|
const occupant = this.occupants.findOccupant({'jid': data.jid});
|
|
|
|
if (occupant) {
|
|
|
|
occupant.save(data);
|
|
|
|
} else {
|
|
|
|
this.occupants.create(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-04-24 17:33:32 +02:00
|
|
|
async handleErrormessageStanza (stanza) {
|
|
|
|
if (await this.shouldShowErrorMessage(stanza)) {
|
2020-05-07 13:32:15 +02:00
|
|
|
const attrs = await st.parseMUCMessage(stanza, this, _converse);
|
|
|
|
const message = attrs.msgid && this.messages.findWhere({'msgid': attrs.msgid});
|
|
|
|
if (message) {
|
|
|
|
message.save({'error': attrs.error});
|
|
|
|
} else {
|
|
|
|
this.createMessage(attrs);
|
|
|
|
}
|
2020-04-24 17:33:32 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
async handleMessageStanza (stanza) {
|
|
|
|
if (st.isArchived(stanza)) {
|
|
|
|
// MAM messages are handled in converse-mam.
|
|
|
|
// We shouldn't get MAM messages here because
|
|
|
|
// they shouldn't have a `type` attribute.
|
|
|
|
return log.warn(`Received a MAM message with type "groupchat"`);
|
|
|
|
}
|
|
|
|
api.trigger('message', {'stanza': stanza});
|
|
|
|
this.createInfoMessages(stanza);
|
|
|
|
this.fetchFeaturesIfConfigurationChanged(stanza);
|
|
|
|
const attrs = await st.parseMUCMessage(stanza, this, _converse);
|
|
|
|
return attrs && this.queueMessage(attrs);
|
|
|
|
},
|
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
registerHandlers () {
|
2019-08-14 09:40:45 +02:00
|
|
|
// Register presence and message handlers for this groupchat
|
2018-10-23 03:41:38 +02:00
|
|
|
const room_jid = this.get('jid');
|
|
|
|
this.removeHandlers();
|
2019-12-06 13:09:09 +01:00
|
|
|
this.presence_handler = _converse.connection.addHandler(
|
|
|
|
stanza => (this.onPresence(stanza) || true),
|
2018-10-23 03:41:38 +02:00
|
|
|
null, 'presence', null, null, room_jid,
|
|
|
|
{'ignoreNamespaceFragment': true, 'matchBareFromJid': true}
|
|
|
|
);
|
2019-12-06 13:09:09 +01:00
|
|
|
|
2020-04-24 17:33:32 +02:00
|
|
|
this.message_handler = _converse.connection.addHandler(
|
|
|
|
stanza => (!!this.handleMessageStanza(stanza) || true),
|
|
|
|
null, 'message', 'groupchat', null, room_jid,
|
2018-10-23 03:41:38 +02:00
|
|
|
{'matchBareFromJid': true}
|
|
|
|
);
|
2019-11-29 12:21:38 +01:00
|
|
|
|
2019-12-06 13:09:09 +01:00
|
|
|
this.affiliation_message_handler = _converse.connection.addHandler(
|
|
|
|
stanza => (this.handleAffiliationChangedMessage(stanza) || true),
|
|
|
|
Strophe.NS.MUC_USER, 'message', null, null, room_jid
|
|
|
|
);
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-04-08 19:44:53 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
removeHandlers () {
|
2019-09-24 15:33:41 +02:00
|
|
|
// Remove the presence and message handlers that were
|
|
|
|
// registered for this groupchat.
|
2018-10-23 03:41:38 +02:00
|
|
|
if (this.message_handler) {
|
2019-12-06 13:09:09 +01:00
|
|
|
_converse.connection && _converse.connection.deleteHandler(this.message_handler);
|
2018-10-23 03:41:38 +02:00
|
|
|
delete this.message_handler;
|
|
|
|
}
|
|
|
|
if (this.presence_handler) {
|
2019-12-06 13:09:09 +01:00
|
|
|
_converse.connection && _converse.connection.deleteHandler(this.presence_handler);
|
2018-10-23 03:41:38 +02:00
|
|
|
delete this.presence_handler;
|
|
|
|
}
|
2019-12-06 13:09:09 +01:00
|
|
|
if (this.affiliation_message_handler) {
|
|
|
|
_converse.connection && _converse.connection.deleteHandler(this.affiliation_message_handler);
|
|
|
|
delete this.affiliation_message_handler;
|
2019-11-29 12:21:38 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
return this;
|
|
|
|
},
|
2018-04-08 19:44:53 +02:00
|
|
|
|
2020-02-10 11:23:55 +01:00
|
|
|
invitesAllowed () {
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.settings.get('allow_muc_invitations') &&
|
2020-02-10 11:23:55 +01:00
|
|
|
(this.features.get('open') ||
|
|
|
|
this.getOwnAffiliation() === "owner"
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
getDisplayName () {
|
2019-05-29 12:57:54 +02:00
|
|
|
const name = this.get('name');
|
|
|
|
if (name) {
|
|
|
|
return name;
|
2020-03-31 13:15:57 +02:00
|
|
|
} else if (api.settings.get('locked_muc_domain') === 'hidden') {
|
2019-05-29 12:57:54 +02:00
|
|
|
return Strophe.getNodeFromJid(this.get('jid'));
|
|
|
|
} else {
|
|
|
|
return this.get('jid');
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-09-06 17:09:32 +02:00
|
|
|
|
2019-09-24 15:33:41 +02:00
|
|
|
/**
|
|
|
|
* Sends a message stanza to the XMPP server and expects a reflection
|
|
|
|
* or error message within a specific timeout period.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#sendTimedMessage
|
|
|
|
* @param { _converse.Message|XMLElement } message
|
|
|
|
* @returns { Promise<XMLElement>|Promise<_converse.TimeoutError> } Returns a promise
|
|
|
|
* which resolves with the reflected message stanza or rejects
|
|
|
|
* with an error stanza or with a {@link _converse.TimeoutError}.
|
|
|
|
*/
|
|
|
|
sendTimedMessage (el) {
|
|
|
|
if (typeof(el.tree) === "function") {
|
|
|
|
el = el.tree();
|
|
|
|
}
|
|
|
|
let id = el.getAttribute('id');
|
|
|
|
if (!id) { // inject id if not found
|
|
|
|
id = this.getUniqueId("sendIQ");
|
|
|
|
el.setAttribute("id", id);
|
|
|
|
}
|
|
|
|
const promise = u.getResolveablePromise();
|
|
|
|
const timeoutHandler = _converse.connection.addTimedHandler(
|
|
|
|
_converse.STANZA_TIMEOUT,
|
|
|
|
() => {
|
|
|
|
_converse.connection.deleteHandler(handler);
|
|
|
|
promise.reject(new _converse.TimeoutError("Timeout Error: No response from server"));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
const handler = _converse.connection.addHandler(stanza => {
|
|
|
|
timeoutHandler && _converse.connection.deleteTimedHandler(timeoutHandler);
|
|
|
|
if (stanza.getAttribute('type') === 'groupchat') {
|
|
|
|
promise.resolve(stanza);
|
|
|
|
} else {
|
|
|
|
promise.reject(stanza);
|
|
|
|
}
|
|
|
|
}, null, 'message', ['error', 'groupchat'], id);
|
2020-03-31 13:15:57 +02:00
|
|
|
api.send(el)
|
2019-09-24 15:33:41 +02:00
|
|
|
return promise;
|
|
|
|
},
|
|
|
|
|
2020-02-18 15:42:55 +01:00
|
|
|
/**
|
2020-02-18 16:06:59 +01:00
|
|
|
* Retract one of your messages in this groupchat
|
2020-02-18 15:42:55 +01:00
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#retractOwnMessage
|
|
|
|
* @param { _converse.Message } message - The message which we're retracting.
|
|
|
|
*/
|
|
|
|
async retractOwnMessage(message) {
|
|
|
|
const editable = message.get('editable');
|
|
|
|
// Optimistic save
|
|
|
|
message.save({
|
|
|
|
'retracted': (new Date()).toISOString(),
|
|
|
|
'retracted_id': message.get('origin_id'),
|
|
|
|
'editable': false
|
|
|
|
});
|
|
|
|
try {
|
|
|
|
await this.sendRetractionMessage(message)
|
|
|
|
} catch (e) {
|
|
|
|
message.save({
|
|
|
|
editable,
|
|
|
|
'retracted': undefined,
|
|
|
|
'retracted_id': undefined,
|
|
|
|
});
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-02-18 16:06:59 +01:00
|
|
|
/**
|
|
|
|
* Retract someone else's message in this groupchat.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#retractOtherMessage
|
|
|
|
* @param { _converse.Message } message - The message which we're retracting.
|
|
|
|
* @param { string } [reason] - The reason for retracting the message.
|
|
|
|
*/
|
|
|
|
async retractOtherMessage (message, reason) {
|
|
|
|
const editable = message.get('editable');
|
|
|
|
// Optimistic save
|
|
|
|
message.save({
|
|
|
|
'moderated': 'retracted',
|
|
|
|
'moderated_by': _converse.bare_jid,
|
|
|
|
'moderated_id': message.get('msgid'),
|
|
|
|
'moderation_reason': reason,
|
|
|
|
'editable': false
|
|
|
|
});
|
|
|
|
const result = await this.sendRetractionIQ(message, reason);
|
|
|
|
if (result === null || u.isErrorStanza(result)) {
|
|
|
|
// Undo the save if something went wrong
|
|
|
|
message.save({
|
|
|
|
editable,
|
|
|
|
'moderated': undefined,
|
|
|
|
'moderated_by': undefined,
|
|
|
|
'moderated_id': undefined,
|
|
|
|
'moderation_reason': undefined,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
|
2019-09-24 15:33:41 +02:00
|
|
|
/**
|
|
|
|
* Sends a message stanza to retract a message in this groupchat.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#sendRetractionMessage
|
|
|
|
* @param { _converse.Message } message - The message which we're retracting.
|
|
|
|
*/
|
|
|
|
sendRetractionMessage (message) {
|
|
|
|
const origin_id = message.get('origin_id');
|
|
|
|
if (!origin_id) {
|
|
|
|
throw new Error("Can't retract message without a XEP-0359 Origin ID");
|
|
|
|
}
|
|
|
|
const msg = $msg({
|
|
|
|
'id': u.getUniqueId(),
|
|
|
|
'to': this.get('jid'),
|
|
|
|
'type': "groupchat"
|
|
|
|
})
|
|
|
|
.c('store', {xmlns: Strophe.NS.HINTS}).up()
|
|
|
|
.c("apply-to", {
|
|
|
|
'id': origin_id,
|
|
|
|
'xmlns': Strophe.NS.FASTEN
|
|
|
|
}).c('retract', {xmlns: Strophe.NS.RETRACT});
|
|
|
|
return this.sendTimedMessage(msg);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends an IQ stanza to the XMPP server to retract a message in this groupchat.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#sendRetractionIQ
|
|
|
|
* @param { _converse.Message } message - The message which we're retracting.
|
|
|
|
* @param { string } [reason] - The reason for retracting the message.
|
|
|
|
*/
|
|
|
|
sendRetractionIQ (message, reason) {
|
|
|
|
const iq = $iq({'to': this.get('jid'), 'type': "set"})
|
|
|
|
.c("apply-to", {
|
|
|
|
'id': message.get(`stanza_id ${this.get('jid')}`),
|
|
|
|
'xmlns': Strophe.NS.FASTEN
|
|
|
|
}).c('moderate', {xmlns: Strophe.NS.MODERATE})
|
|
|
|
.c('retract', {xmlns: Strophe.NS.RETRACT}).up()
|
2020-03-26 10:35:08 +01:00
|
|
|
.c('reason').t(reason || '');
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.sendIQ(iq, null, false);
|
2019-09-24 15:33:41 +02:00
|
|
|
},
|
|
|
|
|
2019-05-26 17:53:32 +02:00
|
|
|
/**
|
|
|
|
* Sends an IQ stanza to the XMPP server to destroy this groupchat. Not
|
|
|
|
* to be confused with the {@link _converse.ChatRoom#destroy}
|
|
|
|
* method, which simply removes the room from the local browser storage cache.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#sendDestroyIQ
|
2019-09-24 15:33:41 +02:00
|
|
|
* @param { string } [reason] - The reason for destroying the groupchat.
|
|
|
|
* @param { string } [new_jid] - The JID of the new groupchat which replaces this one.
|
2019-05-26 17:53:32 +02:00
|
|
|
*/
|
|
|
|
sendDestroyIQ (reason, new_jid) {
|
|
|
|
const destroy = $build("destroy");
|
|
|
|
if (new_jid) {
|
|
|
|
destroy.attrs({'jid': new_jid});
|
|
|
|
}
|
|
|
|
const iq = $iq({
|
|
|
|
'to': this.get('jid'),
|
|
|
|
'type': "set"
|
|
|
|
}).c("query", {'xmlns': Strophe.NS.MUC_OWNER}).cnode(destroy.node);
|
|
|
|
if (reason && reason.length > 0) {
|
|
|
|
iq.c("reason", reason);
|
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.sendIQ(iq);
|
2019-05-26 17:53:32 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Leave the groupchat.
|
2019-03-29 23:47:56 +01:00
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#leave
|
2019-05-26 17:53:32 +02:00
|
|
|
* @param { string } [exit_msg] - Message to indicate your reason for leaving
|
2019-03-29 23:47:56 +01:00
|
|
|
*/
|
2019-10-24 14:29:15 +02:00
|
|
|
async leave (exit_msg) {
|
2019-01-10 12:14:45 +01:00
|
|
|
this.features.destroy();
|
2019-09-19 16:54:55 +02:00
|
|
|
this.occupants.clearStore();
|
2018-11-14 18:10:23 +01:00
|
|
|
if (_converse.disco_entities) {
|
|
|
|
const disco_entity = _converse.disco_entities.get(this.get('jid'));
|
|
|
|
if (disco_entity) {
|
2019-10-24 14:29:15 +02:00
|
|
|
await new Promise((success, error) => disco_entity.destroy({success, error}));
|
2018-11-14 18:10:23 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
if (api.connection.connected()) {
|
2020-04-19 09:23:52 +02:00
|
|
|
api.user.presence.send('unavailable', this.getRoomJIDAndNick(), exit_msg);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-12-10 15:40:33 +01:00
|
|
|
u.safeSave(this.session, {'connection_status': converse.ROOMSTATUS.DISCONNECTED});
|
2018-10-23 03:41:38 +02:00
|
|
|
this.removeHandlers();
|
|
|
|
},
|
2018-04-08 19:44:53 +02:00
|
|
|
|
2019-10-24 14:29:15 +02:00
|
|
|
async close () {
|
2019-12-10 15:40:33 +01:00
|
|
|
// Delete the session model
|
|
|
|
await new Promise(resolve => this.session.destroy({
|
|
|
|
'success': resolve,
|
|
|
|
'error': (m, e) => { log.error(e); resolve() }
|
|
|
|
}));
|
|
|
|
// Delete the features model
|
|
|
|
await new Promise(resolve => this.features.destroy({
|
|
|
|
'success': resolve,
|
|
|
|
'error': (m, e) => { log.error(e); resolve() }
|
|
|
|
}));
|
2019-08-01 10:26:35 +02:00
|
|
|
return _converse.ChatBox.prototype.close.call(this);
|
|
|
|
},
|
|
|
|
|
2020-03-05 13:21:32 +01:00
|
|
|
canModerateMessages () {
|
2019-11-20 18:26:41 +01:00
|
|
|
const self = this.getOwnOccupant();
|
2020-03-31 13:15:57 +02:00
|
|
|
return self && self.isModerator() && api.disco.supports(Strophe.NS.MODERATE, this.get('jid'));
|
2019-11-20 18:26:41 +01:00
|
|
|
},
|
|
|
|
|
2020-02-21 12:30:37 +01:00
|
|
|
/**
|
|
|
|
* Return an array of unique nicknames based on all occupants and messages in this MUC.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#getAllKnownNicknames
|
|
|
|
* @returns { String[] }
|
|
|
|
*/
|
|
|
|
getAllKnownNicknames () {
|
|
|
|
return [...new Set([
|
|
|
|
...this.occupants.map(o => o.get('nick')),
|
|
|
|
...this.messages.map(m => m.get('nick'))
|
|
|
|
])].filter(n => n);
|
|
|
|
},
|
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
getReferenceForMention (mention, index) {
|
2020-02-21 12:30:37 +01:00
|
|
|
const nicknames = this.getAllKnownNicknames();
|
|
|
|
const longest_match = u.getLongestSubstring(mention, nicknames);
|
2018-10-23 03:41:38 +02:00
|
|
|
if (!longest_match) {
|
|
|
|
return null;
|
|
|
|
}
|
2019-07-04 08:14:44 +02:00
|
|
|
if ((mention[longest_match.length] || '').match(/[A-Za-zäëïöüâêîôûáéíóúàèìòùÄËÏÖÜÂÊÎÔÛÁÉÍÓÚÀÈÌÒÙ0-9]/i)) {
|
2018-10-23 03:41:38 +02:00
|
|
|
// avoid false positives, i.e. mentions that have
|
|
|
|
// further alphabetical characters than our longest
|
|
|
|
// match.
|
|
|
|
return null;
|
|
|
|
}
|
2020-02-21 12:30:37 +01:00
|
|
|
|
|
|
|
let uri;
|
2018-10-23 03:41:38 +02:00
|
|
|
const occupant = this.occupants.findOccupant({'nick': longest_match}) ||
|
2020-02-21 12:30:37 +01:00
|
|
|
u.isValidJID(longest_match) && this.occupants.findOccupant({'jid': longest_match});
|
|
|
|
|
|
|
|
if (occupant) {
|
|
|
|
uri = occupant.get('jid') || `${this.get('jid')}/${occupant.get('nick')}`;
|
|
|
|
} else if (nicknames.includes(longest_match)) {
|
|
|
|
// TODO: show a warning to the user that the person is not currently in the chat
|
|
|
|
uri = `${this.get('jid')}/${longest_match}`;
|
|
|
|
} else {
|
|
|
|
return;
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
const obj = {
|
|
|
|
'begin': index,
|
|
|
|
'end': index + longest_match.length,
|
|
|
|
'value': longest_match,
|
2020-02-21 12:30:37 +01:00
|
|
|
'type': 'mention',
|
|
|
|
'uri': encodeURI(`xmpp:${uri}`)
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
|
|
|
return obj;
|
|
|
|
},
|
2018-08-15 17:22:24 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
extractReference (text, index) {
|
|
|
|
for (let i=index; i<text.length; i++) {
|
2019-03-04 20:47:06 +01:00
|
|
|
if (text[i] === '@' && (i === 0 || text[i - 1] === ' ')) {
|
2018-10-23 03:41:38 +02:00
|
|
|
const match = text.slice(i+1),
|
|
|
|
ref = this.getReferenceForMention(match, i);
|
|
|
|
if (ref) {
|
|
|
|
return [text.slice(0, i) + match, ref, i]
|
2018-08-15 17:22:24 +02:00
|
|
|
}
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
},
|
2018-04-24 11:08:26 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
parseTextForReferences (text) {
|
|
|
|
const refs = [];
|
|
|
|
let index = 0;
|
|
|
|
while (index < (text || '').length) {
|
|
|
|
const result = this.extractReference(text, index);
|
|
|
|
if (result) {
|
|
|
|
text = result[0]; // @ gets filtered out
|
|
|
|
refs.push(result[1]);
|
|
|
|
index = result[2];
|
2018-04-08 19:44:53 +02:00
|
|
|
} else {
|
2018-10-23 03:41:38 +02:00
|
|
|
break;
|
2018-04-08 19:44:53 +02:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
return [text, refs];
|
|
|
|
},
|
2018-09-02 15:07:14 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
getOutgoingMessageAttributes (text, spoiler_hint) {
|
|
|
|
const is_spoiler = this.get('composing_spoiler');
|
|
|
|
var references;
|
|
|
|
[text, references] = this.parseTextForReferences(text);
|
2019-10-11 15:32:38 +02:00
|
|
|
const origin_id = u.getUniqueId();
|
2020-05-07 13:32:15 +02:00
|
|
|
const body = text ? u.httpToGeoUri(u.shortnameToUnicode(text), _converse) : undefined;
|
2019-07-01 10:44:59 +02:00
|
|
|
return {
|
2020-05-07 13:32:15 +02:00
|
|
|
body,
|
2020-02-21 12:30:37 +01:00
|
|
|
is_spoiler,
|
|
|
|
origin_id,
|
|
|
|
references,
|
2019-05-20 17:15:12 +02:00
|
|
|
'id': origin_id,
|
2019-03-26 21:58:49 +01:00
|
|
|
'msgid': origin_id,
|
2018-10-23 03:41:38 +02:00
|
|
|
'from': `${this.get('jid')}/${this.get('nick')}`,
|
|
|
|
'fullname': this.get('nick'),
|
2019-11-20 14:14:20 +01:00
|
|
|
'is_only_emojis': text ? u.isOnlyEmojis(text) : false,
|
2020-05-07 13:32:15 +02:00
|
|
|
'message': body,
|
2018-10-23 03:41:38 +02:00
|
|
|
'nick': this.get('nick'),
|
|
|
|
'sender': 'me',
|
|
|
|
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
|
|
|
|
'type': 'groupchat'
|
2019-07-01 10:44:59 +02:00
|
|
|
};
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-04-22 04:20:14 +02:00
|
|
|
|
2019-06-11 11:23:42 +02:00
|
|
|
/**
|
2020-02-21 12:30:37 +01:00
|
|
|
* Utility method to construct the JID for the current user as occupant of the groupchat.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#getRoomJIDAndNick
|
2019-06-11 11:23:42 +02:00
|
|
|
* @returns {string} - The groupchat JID with the user's nickname added at the end.
|
|
|
|
* @example groupchat@conference.example.org/nickname
|
|
|
|
*/
|
|
|
|
getRoomJIDAndNick () {
|
|
|
|
const nick = this.get('nick');
|
|
|
|
const jid = Strophe.getBareJidFromJid(this.get('jid'));
|
2018-10-23 03:41:38 +02:00
|
|
|
return jid + (nick !== null ? `/${nick}` : "");
|
|
|
|
},
|
2018-03-29 11:56:24 +02:00
|
|
|
|
2019-06-11 11:23:42 +02:00
|
|
|
/**
|
2019-07-10 09:47:13 +02:00
|
|
|
* Sends a message with the current XEP-0085 chat state of the user
|
|
|
|
* as taken from the `chat_state` attribute of the {@link _converse.ChatRoom}.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#sendChatState
|
2019-06-11 11:23:42 +02:00
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
sendChatState () {
|
2019-07-10 09:47:13 +02:00
|
|
|
if (!_converse.send_chat_state_notifications ||
|
|
|
|
!this.get('chat_state') ||
|
2019-12-10 15:40:33 +01:00
|
|
|
this.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED ||
|
2019-07-10 09:47:13 +02:00
|
|
|
this.features.get('moderated') && this.getOwnRole() === 'visitor') {
|
2018-10-23 03:41:38 +02:00
|
|
|
return;
|
|
|
|
}
|
2019-08-02 12:05:11 +02:00
|
|
|
const allowed = _converse.send_chat_state_notifications;
|
|
|
|
if (Array.isArray(allowed) && !allowed.includes(this.get('chat_state'))) {
|
|
|
|
return;
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
const chat_state = this.get('chat_state');
|
|
|
|
if (chat_state === _converse.GONE) {
|
|
|
|
// <gone/> is not applicable within MUC context
|
|
|
|
return;
|
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
api.send(
|
2018-10-23 03:41:38 +02:00
|
|
|
$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-09-12 12:32:50 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Send a direct invitation as per XEP-0249
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#directInvite
|
|
|
|
* @param { String } recipient - JID of the person being invited
|
2019-05-26 17:53:32 +02:00
|
|
|
* @param { String } [reason] - Reason for the invitation
|
2019-03-29 23:47:56 +01:00
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
directInvite (recipient, reason) {
|
2019-01-21 17:32:44 +01:00
|
|
|
if (this.features.get('membersonly')) {
|
2018-10-23 03:41:38 +02:00
|
|
|
// When inviting to a members-only groupchat, we first add
|
|
|
|
// the person to the member list by giving them an
|
2019-08-08 15:12:18 +02:00
|
|
|
// affiliation of 'member' otherwise they won't be able to join.
|
|
|
|
this.updateMemberLists([{'jid': recipient, 'affiliation': 'member', 'reason': reason}]);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
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({
|
2018-10-23 16:06:43 +02:00
|
|
|
'from': _converse.connection.jid,
|
|
|
|
'to': recipient,
|
2019-10-11 15:32:38 +02:00
|
|
|
'id': u.getUniqueId()
|
2018-10-23 03:41:38 +02:00
|
|
|
}).c('x', attrs);
|
2020-03-31 13:15:57 +02:00
|
|
|
api.send(invitation);
|
2019-03-29 15:47:23 +01:00
|
|
|
/**
|
|
|
|
* 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
|
2020-01-16 13:01:57 +01:00
|
|
|
* @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
|
2019-03-29 15:47:23 +01:00
|
|
|
* @example _converse.api.listen.on('chatBoxMaximized', view => { ... });
|
|
|
|
*/
|
2020-03-31 13:15:57 +02:00
|
|
|
api.trigger('roomInviteSent', {
|
2018-10-23 03:41:38 +02:00
|
|
|
'room': this,
|
|
|
|
'recipient': recipient,
|
|
|
|
'reason': reason
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2020-01-16 13:01:57 +01:00
|
|
|
/**
|
|
|
|
* 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 () {
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.disco.refresh(this.get('jid'))
|
2020-01-16 13:01:57 +01:00
|
|
|
.then(() => this.getDiscoInfo())
|
|
|
|
.catch(e => log.error(e));
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
|
|
|
|
2020-01-16 13:01:57 +01:00
|
|
|
/**
|
|
|
|
* 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 () {
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.disco.getIdentity('conference', 'text', this.get('jid'))
|
2020-01-16 13:01:57 +01:00
|
|
|
.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 () {
|
2020-03-31 13:15:57 +02:00
|
|
|
const fields = await api.disco.getFields(this.get('jid'));
|
2020-01-16 13:01:57 +01:00
|
|
|
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');
|
2019-10-24 14:29:15 +02:00
|
|
|
}
|
2020-01-16 13:01:57 +01:00
|
|
|
return config;
|
|
|
|
}, {});
|
|
|
|
this.config.save(config);
|
|
|
|
},
|
2018-10-23 03:41:38 +02:00
|
|
|
|
2020-01-16 13:01:57 +01:00
|
|
|
/**
|
|
|
|
* 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 () {
|
2020-03-31 13:15:57 +02:00
|
|
|
const features = await api.disco.getFeatures(this.get('jid'));
|
2019-04-29 09:07:15 +02:00
|
|
|
const attrs = Object.assign(
|
2019-11-01 16:04:55 +01:00
|
|
|
zipObject(converse.ROOM_FEATURES, converse.ROOM_FEATURES.map(() => false)),
|
2019-05-05 19:05:45 +02:00
|
|
|
{'fetched': (new Date()).toISOString()}
|
2019-01-10 13:09:14 +01:00
|
|
|
);
|
2018-10-23 03:41:38 +02:00
|
|
|
features.each(feature => {
|
|
|
|
const fieldname = feature.get('var');
|
|
|
|
if (!fieldname.startsWith('muc_')) {
|
|
|
|
if (fieldname === Strophe.NS.MAM) {
|
|
|
|
attrs.mam_enabled = true;
|
2018-03-29 10:16:41 +02:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
attrs[fieldname.replace('muc_', '')] = true;
|
|
|
|
});
|
2019-01-10 12:14:45 +01:00
|
|
|
this.features.save(attrs);
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-03-29 10:16:41 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Send IQ stanzas to the server to set an affiliation for
|
|
|
|
* the provided JIDs.
|
|
|
|
* See: https://xmpp.org/extensions/xep-0045.html#modifymember
|
|
|
|
*
|
|
|
|
* 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://issues.prosody.im/345
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#setAffiliation
|
|
|
|
* @param { string } affiliation - The affiliation
|
|
|
|
* @param { object } members - A map of jids, affiliations and
|
|
|
|
* optionally reasons. Only those entries with the
|
|
|
|
* same affiliation as being currently set will be considered.
|
2019-07-26 13:32:21 +02:00
|
|
|
* @returns { Promise } A promise which resolves and fails depending on the XMPP server response.
|
2019-03-29 23:47:56 +01:00
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
setAffiliation (affiliation, members) {
|
2019-07-29 10:19:05 +02:00
|
|
|
members = members.filter(m => m.affiliation === undefined || m.affiliation === affiliation);
|
2019-07-26 13:32:21 +02:00
|
|
|
return Promise.all(members.map(m => this.sendAffiliationIQ(affiliation, m)));
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-03-29 11:56:24 +02:00
|
|
|
|
2019-05-03 18:01:07 +02:00
|
|
|
/**
|
|
|
|
* Given a <field> element, return a copy with a <value> child if
|
|
|
|
* we can find a value for it in this rooms config.
|
|
|
|
* @private
|
2019-05-21 11:06:21 +02:00
|
|
|
* @method _converse.ChatRoom#addFieldValue
|
2019-05-03 18:01:07 +02:00
|
|
|
* @returns { Element }
|
|
|
|
*/
|
|
|
|
addFieldValue (field) {
|
|
|
|
const type = field.getAttribute('type');
|
2019-05-03 18:03:58 +02:00
|
|
|
if (type === 'fixed') {
|
|
|
|
return field;
|
|
|
|
}
|
2019-05-03 18:01:07 +02:00
|
|
|
const fieldname = field.getAttribute('var').replace('muc#roomconfig_', '');
|
|
|
|
const config = this.get('roomconfig');
|
|
|
|
if (fieldname in config) {
|
2019-05-04 06:15:39 +02:00
|
|
|
let values;
|
2019-05-03 18:01:07 +02:00
|
|
|
switch (type) {
|
|
|
|
case 'boolean':
|
2019-05-04 06:15:39 +02:00
|
|
|
values = [config[fieldname] ? 1 : 0];
|
2019-05-03 18:01:07 +02:00
|
|
|
break;
|
|
|
|
case 'list-multi':
|
2019-05-04 06:15:39 +02:00
|
|
|
values = config[fieldname];
|
2019-05-03 18:01:07 +02:00
|
|
|
break;
|
|
|
|
default:
|
2019-05-04 06:15:39 +02:00
|
|
|
values= [config[fieldname]];
|
2019-05-03 18:01:07 +02:00
|
|
|
}
|
2019-05-04 06:15:39 +02:00
|
|
|
field.innerHTML = values.map(v => $build('value').t(v)).join('');
|
2019-05-03 18:01:07 +02:00
|
|
|
}
|
|
|
|
return field;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Automatically configure the groupchat based on this model's
|
|
|
|
* 'roomconfig' data.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#autoConfigureChatRoom
|
2019-07-11 10:53:56 +02:00
|
|
|
* @returns { Promise<XMLElement> }
|
2019-05-03 18:01:07 +02:00
|
|
|
* Returns a promise which resolves once a response IQ has
|
|
|
|
* been received.
|
|
|
|
*/
|
2019-07-11 10:53:56 +02:00
|
|
|
async autoConfigureChatRoom () {
|
|
|
|
const stanza = await this.fetchRoomConfiguration();
|
|
|
|
const fields = sizzle('field', stanza);
|
|
|
|
const configArray = fields.map(f => this.addFieldValue(f))
|
|
|
|
if (configArray.length) {
|
|
|
|
return this.sendConfiguration(configArray);
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-04-08 19:44:53 +02:00
|
|
|
|
2019-07-11 10:53:56 +02:00
|
|
|
/**
|
|
|
|
* Send an IQ stanza to fetch the groupchat configuration data.
|
|
|
|
* Returns a promise which resolves once the response IQ
|
|
|
|
* has been received.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#fetchRoomConfiguration
|
|
|
|
* @returns { Promise<XMLElement> }
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
fetchRoomConfiguration () {
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.sendIQ(
|
2018-10-25 07:32:44 +02:00
|
|
|
$iq({'to': this.get('jid'), 'type': "get"})
|
|
|
|
.c("query", {xmlns: Strophe.NS.MUC_OWNER})
|
|
|
|
);
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-04-08 19:44:53 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
2019-07-11 10:53:56 +02:00
|
|
|
* Sends an IQ stanza with the groupchat configuration.
|
2019-03-29 23:47:56 +01:00
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#sendConfiguration
|
|
|
|
* @param { Array } config - The groupchat configuration
|
2019-07-11 10:53:56 +02:00
|
|
|
* @returns { Promise<XMLElement> } - A promise which resolves with
|
|
|
|
* the `result` stanza received from the XMPP server.
|
2019-03-29 23:47:56 +01:00
|
|
|
*/
|
2019-07-11 10:53:56 +02:00
|
|
|
sendConfiguration (config=[]) {
|
2018-10-23 03:41:38 +02:00
|
|
|
const iq = $iq({to: this.get('jid'), type: "set"})
|
|
|
|
.c("query", {xmlns: Strophe.NS.MUC_OWNER})
|
|
|
|
.c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
|
2019-05-14 11:38:41 +02:00
|
|
|
config.forEach(node => iq.cnode(node).up());
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.sendIQ(iq);
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-04-08 19:44:53 +02:00
|
|
|
|
2019-07-10 09:47:13 +02:00
|
|
|
/**
|
|
|
|
* Returns the `role` which the current user has in this MUC
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#getOwnRole
|
|
|
|
* @returns { ('none'|'visitor'|'participant'|'moderator') }
|
|
|
|
*/
|
|
|
|
getOwnRole () {
|
2020-03-24 12:26:34 +01:00
|
|
|
return this.getOwnOccupant()?.attributes?.role;
|
2019-07-10 09:47:13 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the `affiliation` which the current user has in this MUC
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#getOwnAffiliation
|
|
|
|
* @returns { ('none'|'outcast'|'member'|'admin'|'owner') }
|
|
|
|
*/
|
|
|
|
getOwnAffiliation () {
|
2020-03-24 12:26:34 +01:00
|
|
|
return this.getOwnOccupant()?.attributes?.affiliation;
|
2019-07-10 09:47:13 +02:00
|
|
|
},
|
2019-07-10 09:47:13 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
2019-07-10 09:47:13 +02:00
|
|
|
* Get the {@link _converse.ChatRoomOccupant} instance which
|
|
|
|
* represents the current user.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#getOwnOccupant
|
|
|
|
* @returns { _converse.ChatRoomOccupant }
|
|
|
|
*/
|
|
|
|
getOwnOccupant () {
|
2019-07-10 09:47:13 +02:00
|
|
|
return this.occupants.findWhere({'jid': _converse.bare_jid});
|
2019-07-10 09:47:13 +02:00
|
|
|
},
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Send an IQ stanza specifying an affiliation change.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#
|
|
|
|
* @param { String } affiliation: affiliation
|
|
|
|
* (could also be stored on the member object).
|
|
|
|
* @param { Object } member: Map containing the member's jid and
|
|
|
|
* optionally a reason and affiliation.
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
sendAffiliationIQ (affiliation, member) {
|
2018-10-25 07:32:44 +02:00
|
|
|
const iq = $iq({to: this.get('jid'), type: "set"})
|
|
|
|
.c("query", {xmlns: Strophe.NS.MUC_ADMIN})
|
|
|
|
.c("item", {
|
|
|
|
'affiliation': member.affiliation || affiliation,
|
|
|
|
'nick': member.nick,
|
|
|
|
'jid': member.jid
|
|
|
|
});
|
2019-07-29 10:19:05 +02:00
|
|
|
if (member.reason !== undefined) {
|
2018-10-25 07:32:44 +02:00
|
|
|
iq.c("reason", member.reason);
|
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.sendIQ(iq);
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-03-29 11:56:24 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
2019-07-26 13:32:21 +02:00
|
|
|
* Send IQ stanzas to the server to modify affiliations for users in this groupchat.
|
|
|
|
*
|
2019-03-29 23:47:56 +01:00
|
|
|
* See: https://xmpp.org/extensions/xep-0045.html#modifymember
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#setAffiliations
|
2019-07-26 13:32:21 +02:00
|
|
|
* @param { Object[] } members
|
|
|
|
* @param { string } members[].jid - The JID of the user whose affiliation will change
|
|
|
|
* @param { Array } members[].affiliation - The new affiliation for this user
|
|
|
|
* @param { string } [members[].reason] - An optional reason for the affiliation change
|
|
|
|
* @returns { Promise }
|
2019-03-29 23:47:56 +01:00
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
setAffiliations (members) {
|
2019-11-01 16:04:55 +01:00
|
|
|
const affiliations = uniq(members.map(m => m.affiliation));
|
2019-07-26 13:32:21 +02:00
|
|
|
return Promise.all(affiliations.map(a => this.setAffiliation(a, members)));
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-03-29 11:56:24 +02:00
|
|
|
|
2019-05-26 16:39:17 +02:00
|
|
|
/**
|
|
|
|
* Send an IQ stanza to modify an occupant's role
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#setRole
|
|
|
|
* @param { _converse.ChatRoomOccupant } occupant
|
|
|
|
* @param { String } role
|
|
|
|
* @param { String } reason
|
|
|
|
* @param { function } onSuccess - callback for a succesful response
|
|
|
|
* @param { function } onError - callback for an error response
|
|
|
|
*/
|
|
|
|
setRole (occupant, role, reason, onSuccess, onError) {
|
|
|
|
const item = $build("item", {
|
|
|
|
'nick': occupant.get('nick'),
|
|
|
|
role
|
|
|
|
});
|
|
|
|
const iq = $iq({
|
|
|
|
'to': this.get('jid'),
|
|
|
|
'type': 'set'
|
|
|
|
}).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node);
|
|
|
|
if (reason !== null) {
|
|
|
|
iq.c("reason", reason);
|
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.sendIQ(iq).then(onSuccess).catch(onError);
|
2019-05-26 16:39:17 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @private
|
2019-05-26 17:53:32 +02:00
|
|
|
* @method _converse.ChatRoom#getOccupant
|
|
|
|
* @param { String } nick_or_jid - The nickname or JID of the occupant to be returned
|
2019-05-26 16:39:17 +02:00
|
|
|
* @returns { _converse.ChatRoomOccupant }
|
|
|
|
*/
|
2019-05-26 17:53:32 +02:00
|
|
|
getOccupant (nick_or_jid) {
|
|
|
|
return (u.isValidJID(nick_or_jid) &&
|
|
|
|
this.occupants.findWhere({'jid': nick_or_jid})) ||
|
|
|
|
this.occupants.findWhere({'nick': nick_or_jid});
|
2019-05-26 16:39:17 +02:00
|
|
|
},
|
|
|
|
|
2020-03-23 17:55:11 +01:00
|
|
|
/**
|
|
|
|
* Return an array of occupant models that have the required role
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#getOccupantsWithRole
|
|
|
|
* @param { String } role
|
|
|
|
* @returns { _converse.ChatRoomOccupant[] }
|
|
|
|
*/
|
|
|
|
getOccupantsWithRole (role) {
|
|
|
|
return this.getOccupantsSortedBy('nick')
|
|
|
|
.filter(o => o.get('role') === role)
|
|
|
|
.map(item => {
|
|
|
|
return {
|
|
|
|
'jid': item.get('jid'),
|
|
|
|
'nick': item.get('nick'),
|
|
|
|
'role': item.get('role')
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an array of occupant models that have the required affiliation
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#getOccupantsWithAffiliation
|
|
|
|
* @param { String } affiliation
|
|
|
|
* @returns { _converse.ChatRoomOccupant[] }
|
|
|
|
*/
|
|
|
|
getOccupantsWithAffiliation (affiliation) {
|
|
|
|
return this.getOccupantsSortedBy('nick')
|
|
|
|
.filter(o => o.get('affiliation') === affiliation)
|
|
|
|
.map(item => {
|
|
|
|
return {
|
|
|
|
'jid': item.get('jid'),
|
|
|
|
'nick': item.get('nick'),
|
|
|
|
'affiliation': item.get('affiliation')
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an array of occupant models, sorted according to the passed-in attribute.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#getOccupantsSortedBy
|
|
|
|
* @param { String } attr - The attribute to sort the returned array by
|
|
|
|
* @returns { _converse.ChatRoomOccupant[] }
|
|
|
|
*/
|
|
|
|
getOccupantsSortedBy (attr) {
|
|
|
|
return Array.from(this.occupants.models)
|
|
|
|
.sort((a, b) => a.get(attr) < b.get(attr) ? -1 : (a.get(attr) > b.get(attr) ? 1 : 0));
|
|
|
|
},
|
|
|
|
|
2019-07-26 13:32:21 +02:00
|
|
|
/**
|
2019-08-08 15:12:18 +02:00
|
|
|
* Sends an IQ stanza to the server, asking it for the relevant affiliation list .
|
|
|
|
* Returns an array of {@link MemberListItem} objects, representing occupants
|
|
|
|
* that have the given affiliation.
|
|
|
|
* See: https://xmpp.org/extensions/xep-0045.html#modifymember
|
2019-07-26 13:32:21 +02:00
|
|
|
* @private
|
2019-08-08 15:12:18 +02:00
|
|
|
* @method _converse.ChatRoom#getAffiliationList
|
|
|
|
* @param { ("admin"|"owner"|"member") } affiliation
|
|
|
|
* @returns { Promise<MemberListItem[]> }
|
2019-07-26 13:32:21 +02:00
|
|
|
*/
|
2019-08-08 15:12:18 +02:00
|
|
|
async getAffiliationList (affiliation) {
|
|
|
|
const iq = $iq({to: this.get('jid'), type: "get"})
|
|
|
|
.c("query", {xmlns: Strophe.NS.MUC_ADMIN})
|
|
|
|
.c("item", {'affiliation': affiliation});
|
2020-03-31 13:15:57 +02:00
|
|
|
const result = await api.sendIQ(iq, null, false);
|
2019-08-10 13:18:56 +02:00
|
|
|
if (result === null) {
|
|
|
|
const err_msg = `Error: timeout while fetching ${affiliation} list for MUC ${this.get('jid')}`;
|
|
|
|
const err = new Error(err_msg);
|
2019-11-06 11:01:34 +01:00
|
|
|
log.warn(err_msg);
|
|
|
|
log.warn(result);
|
2019-08-10 13:18:56 +02:00
|
|
|
return err;
|
|
|
|
}
|
|
|
|
if (u.isErrorStanza(result)) {
|
2019-07-04 14:12:12 +02:00
|
|
|
const err_msg = `Error: not allowed to fetch ${affiliation} list for MUC ${this.get('jid')}`;
|
|
|
|
const err = new Error(err_msg);
|
2019-11-06 11:01:34 +01:00
|
|
|
log.warn(err_msg);
|
|
|
|
log.warn(result);
|
2019-07-04 14:12:12 +02:00
|
|
|
return err;
|
2019-08-08 15:12:18 +02:00
|
|
|
}
|
2020-03-23 17:55:11 +01:00
|
|
|
return muc_utils.parseMemberListIQ(result)
|
|
|
|
.filter(p => p)
|
|
|
|
.sort((a, b) => a.nick < b.nick ? -1 : (a.nick > b.nick ? 1 : 0))
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-03-29 11:56:24 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#updateMemberLists
|
|
|
|
* @param { object } members - Map of member jids and affiliations.
|
2019-07-11 10:53:56 +02:00
|
|
|
* @returns { Promise }
|
2019-03-29 23:47:56 +01:00
|
|
|
* 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.
|
|
|
|
*/
|
2019-08-08 15:12:18 +02:00
|
|
|
async updateMemberLists (members) {
|
|
|
|
const all_affiliations = ['member', 'admin', 'owner'];
|
|
|
|
const aff_lists = await Promise.all(all_affiliations.map(a => this.getAffiliationList(a)));
|
2019-07-04 14:12:12 +02:00
|
|
|
const old_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc: [...val, ...acc]), []);
|
2019-11-07 09:08:13 +01:00
|
|
|
await this.setAffiliations(muc_utils.computeAffiliationsDelta(true, false, members, old_members));
|
2020-02-20 17:11:07 +01:00
|
|
|
await this.occupants.fetchMembers();
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-03-29 11:56:24 +02:00
|
|
|
|
2019-05-21 11:07:06 +02:00
|
|
|
/**
|
|
|
|
* Given a nick name, save it to the model state, otherwise, look
|
|
|
|
* for a server-side reserved nickname or default configured
|
|
|
|
* nickname and if found, persist that to the model state.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#getAndPersistNickname
|
2019-07-11 10:53:56 +02:00
|
|
|
* @returns { Promise<string> } A promise which resolves with the nickname
|
2019-05-21 11:07:06 +02:00
|
|
|
*/
|
|
|
|
async getAndPersistNickname (nick) {
|
2019-05-23 16:03:09 +02:00
|
|
|
nick = nick ||
|
|
|
|
this.get('nick') ||
|
|
|
|
await this.getReservedNick() ||
|
|
|
|
_converse.getDefaultMUCNickname();
|
|
|
|
|
|
|
|
if (nick) {
|
2019-12-08 20:42:09 +01:00
|
|
|
this.save({nick}, {'silent': true});
|
2019-05-20 10:06:37 +02:00
|
|
|
}
|
|
|
|
return nick;
|
|
|
|
},
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Use service-discovery to ask the XMPP server whether
|
|
|
|
* this user has a reserved nickname for this groupchat.
|
|
|
|
* If so, we'll use that, otherwise we render the nickname form.
|
|
|
|
* @private
|
2019-05-20 10:06:37 +02:00
|
|
|
* @method _converse.ChatRoom#getReservedNick
|
2019-07-11 10:53:56 +02:00
|
|
|
* @returns { Promise<string> } A promise which resolves with the reserved nick or null
|
2019-03-29 23:47:56 +01:00
|
|
|
*/
|
2019-05-20 10:06:37 +02:00
|
|
|
async getReservedNick () {
|
2019-09-04 21:21:16 +02:00
|
|
|
const stanza = $iq({
|
|
|
|
'to': this.get('jid'),
|
|
|
|
'from': _converse.connection.jid,
|
|
|
|
'type': "get"
|
|
|
|
}).c("query", {
|
|
|
|
'xmlns': Strophe.NS.DISCO_INFO,
|
|
|
|
'node': 'x-roomuser-item'
|
|
|
|
})
|
2020-03-31 13:15:57 +02:00
|
|
|
const result = await api.sendIQ(stanza, null, false);
|
2019-09-04 21:21:16 +02:00
|
|
|
if (u.isErrorObject(result)) {
|
|
|
|
throw result;
|
2019-05-23 16:03:09 +02:00
|
|
|
}
|
2019-09-04 21:21:16 +02:00
|
|
|
const identity_el = result.querySelector('query[node="x-roomuser-item"] identity');
|
2019-05-20 10:06:37 +02:00
|
|
|
return identity_el ? identity_el.getAttribute('name') : null;
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-09-12 07:20:57 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
async registerNickname () {
|
|
|
|
// See https://xmpp.org/extensions/xep-0045.html#register
|
2019-09-04 21:21:16 +02:00
|
|
|
const nick = this.get('nick');
|
|
|
|
const jid = this.get('jid');
|
2018-10-23 03:41:38 +02:00
|
|
|
let iq, err_msg;
|
|
|
|
try {
|
2020-03-31 13:15:57 +02:00
|
|
|
iq = await api.sendIQ(
|
2018-03-29 10:16:41 +02:00
|
|
|
$iq({
|
2018-10-23 03:41:38 +02:00
|
|
|
'to': jid,
|
2018-03-29 10:16:41 +02:00
|
|
|
'from': _converse.connection.jid,
|
2018-10-23 03:41:38 +02:00
|
|
|
'type': 'get'
|
|
|
|
}).c('query', {'xmlns': Strophe.NS.MUC_REGISTER})
|
|
|
|
);
|
|
|
|
} catch (e) {
|
2019-06-11 12:16:27 +02:00
|
|
|
if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
|
2018-10-23 03:41:38 +02:00
|
|
|
err_msg = __("You're not allowed to register yourself in this groupchat.");
|
2019-06-11 12:16:27 +02:00
|
|
|
} else if (sizzle(`registration-required[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
|
2018-10-23 03:41:38 +02:00
|
|
|
err_msg = __("You're not allowed to register in this groupchat because it's members-only.");
|
2018-09-12 12:33:28 +02:00
|
|
|
}
|
2019-11-06 11:01:34 +01:00
|
|
|
log.error(e);
|
2018-10-23 03:41:38 +02:00
|
|
|
return err_msg;
|
|
|
|
}
|
|
|
|
const required_fields = sizzle('field required', iq).map(f => f.parentElement);
|
|
|
|
if (required_fields.length > 1 && required_fields[0].getAttribute('var') !== 'muc#register_roomnick') {
|
2019-11-06 11:01:34 +01:00
|
|
|
return log.error(`Can't register the user register in the groupchat ${jid} due to the required fields`);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
try {
|
2020-03-31 13:15:57 +02:00
|
|
|
await api.sendIQ($iq({
|
2018-10-23 03:41:38 +02:00
|
|
|
'to': jid,
|
|
|
|
'from': _converse.connection.jid,
|
|
|
|
'type': 'set'
|
|
|
|
}).c('query', {'xmlns': Strophe.NS.MUC_REGISTER})
|
|
|
|
.c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'})
|
|
|
|
.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#register').up().up()
|
|
|
|
.c('field', {'var': 'muc#register_roomnick'}).c('value').t(nick)
|
|
|
|
);
|
|
|
|
} catch (e) {
|
2019-06-11 12:16:27 +02:00
|
|
|
if (sizzle(`service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
|
2018-10-23 03:41:38 +02:00
|
|
|
err_msg = __("Can't register your nickname in this groupchat, it doesn't support registration.");
|
2019-06-11 12:16:27 +02:00
|
|
|
} else if (sizzle(`bad-request[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
|
2018-10-23 03:41:38 +02:00
|
|
|
err_msg = __("Can't register your nickname in this groupchat, invalid data form supplied.");
|
2018-09-04 11:39:27 +02:00
|
|
|
}
|
2019-11-06 11:01:34 +01:00
|
|
|
log.error(err_msg);
|
|
|
|
log.error(e);
|
2018-10-23 03:41:38 +02:00
|
|
|
return err_msg;
|
|
|
|
}
|
|
|
|
},
|
2018-09-04 11:39:27 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
2019-10-24 14:29:15 +02:00
|
|
|
* Given a presence stanza, update the occupant model based on its contents.
|
2019-03-29 23:47:56 +01:00
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#updateOccupantsOnPresence
|
|
|
|
* @param { XMLElement } pres - The presence stanza
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
updateOccupantsOnPresence (pres) {
|
2020-04-13 15:29:40 +02:00
|
|
|
const data = st.parseMUCPresence(pres);
|
2018-10-23 03:41:38 +02:00
|
|
|
if (data.type === 'error' || (!data.jid && !data.nick)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
const occupant = this.occupants.findOccupant(data);
|
|
|
|
if (data.type === 'unavailable' && occupant) {
|
2019-09-07 22:00:28 +02:00
|
|
|
if (!data.states.includes(converse.MUC_NICK_CHANGED_CODE) && !occupant.isMember()) {
|
2018-10-23 03:41:38 +02:00
|
|
|
// We only destroy the occupant if this is not a nickname change operation.
|
|
|
|
// and if they're not on the member lists.
|
|
|
|
// Before destroying we set the new data, so
|
|
|
|
// that we can show the disconnection message.
|
|
|
|
occupant.set(data);
|
|
|
|
occupant.destroy();
|
|
|
|
return;
|
2018-04-08 19:44:53 +02:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-04-10 23:56:06 +02:00
|
|
|
const jid = data.jid || '';
|
2019-04-29 09:07:15 +02:00
|
|
|
const attributes = Object.assign(data, {
|
2020-03-24 12:26:34 +01:00
|
|
|
'jid': Strophe.getBareJidFromJid(jid) || occupant?.attributes?.jid,
|
|
|
|
'resource': Strophe.getResourceFromJid(jid) || occupant?.attributes?.resource
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
if (occupant) {
|
|
|
|
occupant.save(attributes);
|
|
|
|
} else {
|
|
|
|
this.occupants.create(attributes);
|
|
|
|
}
|
|
|
|
},
|
2018-04-08 19:44:53 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
fetchFeaturesIfConfigurationChanged (stanza) {
|
2019-06-20 12:04:05 +02:00
|
|
|
// 104: configuration change
|
|
|
|
// 170: logging enabled
|
|
|
|
// 171: logging disabled
|
|
|
|
// 172: room no longer anonymous
|
|
|
|
// 173: room now semi-anonymous
|
|
|
|
// 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) {
|
2020-01-16 13:01:57 +01:00
|
|
|
this.refreshDiscoInfo();
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
},
|
2018-07-16 00:49:00 +02:00
|
|
|
|
2019-09-24 15:33:41 +02:00
|
|
|
/**
|
|
|
|
* Given two JIDs, which can be either user JIDs or MUC occupant JIDs,
|
|
|
|
* determine whether they belong to the same user.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#isSameUser
|
|
|
|
* @param { String } jid1
|
|
|
|
* @param { String } jid2
|
|
|
|
* @returns { Boolean }
|
|
|
|
*/
|
|
|
|
isSameUser (jid1, jid2) {
|
|
|
|
const bare_jid1 = Strophe.getBareJidFromJid(jid1);
|
|
|
|
const bare_jid2 = Strophe.getBareJidFromJid(jid2);
|
|
|
|
const resource1 = Strophe.getResourceFromJid(jid1);
|
|
|
|
const resource2 = Strophe.getResourceFromJid(jid2);
|
|
|
|
if (u.isSameBareJID(jid1, jid2)) {
|
|
|
|
if (bare_jid1 === this.get('jid')) {
|
|
|
|
// MUC JIDs
|
|
|
|
return resource1 === resource2;
|
|
|
|
} else {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const occupant1 = (bare_jid1 === this.get('jid')) ?
|
|
|
|
this.occupants.findOccupant({'nick': resource1}) :
|
|
|
|
this.occupants.findOccupant({'jid': bare_jid1});
|
|
|
|
|
|
|
|
const occupant2 = (bare_jid2 === this.get('jid')) ?
|
|
|
|
this.occupants.findOccupant({'nick': resource2}) :
|
|
|
|
this.occupants.findOccupant({'jid': bare_jid2});
|
|
|
|
return occupant1 === occupant2;
|
|
|
|
}
|
|
|
|
},
|
2019-02-13 08:47:38 +01:00
|
|
|
|
2020-04-08 14:31:18 +02:00
|
|
|
async isSubjectHidden () {
|
|
|
|
const jids = await api.user.settings.get('mucs_with_hidden_subject', [])
|
|
|
|
return jids.includes(this.get('jid'));
|
|
|
|
},
|
|
|
|
|
|
|
|
async toggleSubjectHiddenState () {
|
|
|
|
const muc_jid = this.get('jid');
|
|
|
|
const jids = await api.user.settings.get('mucs_with_hidden_subject', []);
|
|
|
|
if (jids.includes(this.get('jid'))) {
|
|
|
|
api.user.settings.set('mucs_with_hidden_subject', jids.filter(jid => jid !== muc_jid));
|
|
|
|
} else {
|
|
|
|
api.user.settings.set('mucs_with_hidden_subject', [...jids, muc_jid]);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
2020-04-07 11:30:43 +02:00
|
|
|
* Handle a possible subject change and return `true` if so.
|
2019-03-29 23:47:56 +01:00
|
|
|
* @private
|
2020-04-07 11:30:43 +02:00
|
|
|
* @method _converse.ChatRoom#handleSubjectChange
|
|
|
|
* @param { object } attrs - Attributes representing a received
|
2020-04-24 17:33:32 +02:00
|
|
|
* message, as returned by {@link st.parseMUCMessage}
|
2019-03-29 23:47:56 +01:00
|
|
|
*/
|
2020-04-08 14:31:18 +02:00
|
|
|
async handleSubjectChange (attrs) {
|
2020-01-14 10:06:01 +01:00
|
|
|
if (isString(attrs.subject) && !attrs.thread && !attrs.message) {
|
2019-02-13 08:47:38 +01:00
|
|
|
// https://xmpp.org/extensions/xep-0045.html#subject-mod
|
|
|
|
// -----------------------------------------------------
|
|
|
|
// The subject is changed by sending a message of type "groupchat" to the <room@service>,
|
|
|
|
// where the <message/> MUST contain a <subject/> element that specifies the new subject but
|
|
|
|
// MUST NOT contain a <body/> element (or a <thread/> element).
|
2020-04-07 11:30:43 +02:00
|
|
|
const subject = attrs.subject;
|
|
|
|
const author = attrs.nick;
|
2020-04-08 14:31:18 +02:00
|
|
|
u.safeSave(this, {'subject': {author, 'text': attrs.subject || ''}});
|
2020-04-10 20:47:07 +02:00
|
|
|
if (!attrs.is_delayed && author) {
|
2020-04-07 11:30:43 +02:00
|
|
|
const message = subject ? __('Topic set by %1$s', author) : __('Topic cleared by %1$s', author);
|
2020-04-08 13:08:15 +02:00
|
|
|
const prev_msg = this.messages.last();
|
|
|
|
if (prev_msg?.get('nick') !== attrs.nick ||
|
|
|
|
prev_msg?.get('type') !== 'info' ||
|
|
|
|
prev_msg?.get('message') !== message) {
|
2020-04-08 14:31:18 +02:00
|
|
|
this.createMessage({message, 'nick': attrs.nick, 'type': 'info'});
|
|
|
|
}
|
|
|
|
if (await this.isSubjectHidden()) {
|
|
|
|
this.toggleSubjectHiddenState();
|
2020-04-08 13:08:15 +02:00
|
|
|
}
|
2020-04-07 11:30:43 +02:00
|
|
|
}
|
2019-02-13 08:47:38 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
2019-02-12 19:10:46 +01:00
|
|
|
},
|
|
|
|
|
2019-05-26 17:53:32 +02:00
|
|
|
/**
|
|
|
|
* Set the subject for this {@link _converse.ChatRoom}
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#setSubject
|
|
|
|
* @param { String } value
|
|
|
|
*/
|
|
|
|
setSubject(value='') {
|
2020-03-31 13:15:57 +02:00
|
|
|
api.send(
|
2019-05-26 17:53:32 +02:00
|
|
|
$msg({
|
|
|
|
to: this.get('jid'),
|
|
|
|
from: _converse.connection.jid,
|
|
|
|
type: "groupchat"
|
|
|
|
}).c("subject", {xmlns: "jabber:client"}).t(value).tree()
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Is this a chat state notification that can be ignored,
|
|
|
|
* because it's old or because it's from us.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#ignorableCSN
|
|
|
|
* @param { Object } attrs - The message attributes
|
|
|
|
*/
|
2019-02-13 15:22:54 +01:00
|
|
|
ignorableCSN (attrs) {
|
2020-04-24 17:33:32 +02:00
|
|
|
return attrs.chat_state && !attrs.body && (attrs.is_delayed || this.isOwnMessage(attrs));
|
2019-02-13 15:22:54 +01:00
|
|
|
},
|
|
|
|
|
2019-06-06 16:02:03 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether the message is from ourselves by checking
|
|
|
|
* the `from` attribute. Doesn't check the `type` attribute.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#isOwnMessage
|
|
|
|
* @param { Object|XMLElement|_converse.Message } msg
|
|
|
|
* @returns { boolean }
|
|
|
|
*/
|
|
|
|
isOwnMessage (msg) {
|
|
|
|
let from;
|
2019-11-01 16:04:55 +01:00
|
|
|
if (isElement(msg)) {
|
2019-06-06 16:02:03 +02:00
|
|
|
from = msg.getAttribute('from');
|
|
|
|
} else if (msg instanceof _converse.Message) {
|
|
|
|
from = msg.get('from');
|
|
|
|
} else {
|
|
|
|
from = msg.from;
|
|
|
|
}
|
|
|
|
return Strophe.getResourceFromJid(from) == this.get('nick');
|
|
|
|
},
|
|
|
|
|
|
|
|
|
2020-04-24 17:33:32 +02:00
|
|
|
getUpdatedMessageAttributes (message, attrs) {
|
2019-03-07 17:29:20 +01:00
|
|
|
// Overridden in converse-muc and converse-mam
|
2020-04-24 17:33:32 +02:00
|
|
|
const new_attrs = _converse.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, attrs);
|
|
|
|
if (this.isOwnMessage(attrs)) {
|
|
|
|
const stanza_id_keys = Object.keys(attrs).filter(k => k.startsWith('stanza_id'));
|
|
|
|
Object.assign(new_attrs, pick(attrs, stanza_id_keys));
|
2019-03-07 15:44:58 +01:00
|
|
|
if (!message.get('received')) {
|
2020-04-24 17:33:32 +02:00
|
|
|
new_attrs.received = (new Date()).toISOString();
|
2019-03-07 15:44:58 +01:00
|
|
|
}
|
|
|
|
}
|
2020-04-24 17:33:32 +02:00
|
|
|
return new_attrs;
|
2019-03-07 15:44:58 +01:00
|
|
|
},
|
|
|
|
|
2019-06-03 11:36:55 +02:00
|
|
|
/**
|
|
|
|
* Send a MUC-0410 MUC Self-Ping stanza to room to determine
|
|
|
|
* whether we're still joined.
|
|
|
|
* @async
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#isJoined
|
|
|
|
* @returns {Promise<boolean>}
|
|
|
|
*/
|
|
|
|
async isJoined () {
|
|
|
|
const ping = $iq({
|
|
|
|
'to': `${this.get('jid')}/${this.get('nick')}`,
|
|
|
|
'type': "get"
|
|
|
|
}).c("ping", {'xmlns': Strophe.NS.PING});
|
|
|
|
try {
|
2020-03-31 13:15:57 +02:00
|
|
|
await api.sendIQ(ping);
|
2019-06-03 11:36:55 +02:00
|
|
|
} catch (e) {
|
2019-12-11 10:06:03 +01:00
|
|
|
if (e === null) {
|
|
|
|
log.error(`Timeout error while checking whether we're joined to MUC: ${this.get('jid')}`);
|
|
|
|
} else {
|
|
|
|
log.error(`Apparently we're no longer connected to MUC: ${this.get('jid')}`);
|
|
|
|
log.error(e);
|
2019-06-03 11:36:55 +02:00
|
|
|
}
|
2019-12-11 10:06:03 +01:00
|
|
|
return false;
|
2019-06-03 11:36:55 +02:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether we're still joined and re-join if not
|
|
|
|
* @async
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#rejoinIfNecessary
|
|
|
|
*/
|
|
|
|
async rejoinIfNecessary () {
|
2019-12-08 20:42:09 +01:00
|
|
|
if (! await this.isJoined()) {
|
|
|
|
this.rejoin();
|
2019-06-03 11:36:55 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#shouldShowErrorMessage
|
|
|
|
* @returns {Promise<boolean>}
|
|
|
|
*/
|
|
|
|
async shouldShowErrorMessage (stanza) {
|
|
|
|
if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
|
|
|
|
if (await this.rejoinIfNecessary()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2019-06-17 10:50:34 +02:00
|
|
|
return _converse.ChatBox.prototype.shouldShowErrorMessage.call(this, stanza);
|
2019-06-03 11:36:55 +02:00
|
|
|
},
|
|
|
|
|
2019-09-24 15:33:41 +02:00
|
|
|
/**
|
|
|
|
* Looks whether we already have a moderation message for this
|
|
|
|
* incoming message. If so, it's considered "dangling" because
|
|
|
|
* it probably hasn't been applied to anything yet, given that
|
|
|
|
* the relevant message is only coming in now.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#findDanglingModeration
|
|
|
|
* @param { object } attrs - Attributes representing a received
|
2020-04-24 17:33:32 +02:00
|
|
|
* message, as returned by {@link st.parseMUCMessage}
|
2019-09-24 15:33:41 +02:00
|
|
|
* @returns { _converse.ChatRoomMessage }
|
|
|
|
*/
|
|
|
|
findDanglingModeration (attrs) {
|
|
|
|
if (!this.messages.length) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
// Only look for dangling moderation if there are newer
|
|
|
|
// messages than this one, since moderation come after.
|
|
|
|
if (this.messages.last().get('time') > attrs.time) {
|
|
|
|
// Search from latest backwards
|
|
|
|
const messages = Array.from(this.messages.models);
|
|
|
|
const stanza_id = attrs[`stanza_id ${this.get('jid')}`];
|
|
|
|
if (!stanza_id) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
messages.reverse();
|
|
|
|
return messages.find(
|
|
|
|
({attributes}) =>
|
2020-02-22 22:58:51 +01:00
|
|
|
attributes.moderated === 'retracted' &&
|
2019-09-24 15:33:41 +02:00
|
|
|
attributes.moderated_id === stanza_id &&
|
|
|
|
attributes.moderated_by
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles message moderation based on the passed in attributes.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#handleModeration
|
|
|
|
* @param { object } attrs - Attributes representing a received
|
2020-04-24 17:33:32 +02:00
|
|
|
* message, as returned by {@link st.parseMUCMessage}
|
2019-09-24 15:33:41 +02:00
|
|
|
* @returns { Boolean } Returns `true` or `false` depending on
|
|
|
|
* whether a message was moderated or not.
|
|
|
|
*/
|
2020-03-02 16:59:18 +01:00
|
|
|
async handleModeration (attrs) {
|
2019-09-24 15:33:41 +02:00
|
|
|
const MODERATION_ATTRIBUTES = [
|
2019-11-26 12:42:03 +01:00
|
|
|
'editable',
|
2019-09-24 15:33:41 +02:00
|
|
|
'moderated',
|
|
|
|
'moderated_by',
|
|
|
|
'moderated_id',
|
|
|
|
'moderation_reason'
|
|
|
|
];
|
|
|
|
if (attrs.moderated === 'retracted') {
|
|
|
|
const query = {};
|
|
|
|
const key = `stanza_id ${this.get('jid')}`;
|
|
|
|
query[key] = attrs.moderated_id;
|
|
|
|
const message = this.messages.findWhere(query);
|
|
|
|
if (!message) {
|
|
|
|
attrs['dangling_moderation'] = true;
|
2020-03-02 16:59:18 +01:00
|
|
|
await this.createMessage(attrs);
|
2019-09-24 15:33:41 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
message.save(pick(attrs, MODERATION_ATTRIBUTES));
|
|
|
|
return true;
|
2019-04-10 22:03:00 +02:00
|
|
|
} else {
|
2019-09-24 15:33:41 +02:00
|
|
|
// Check if we have dangling moderation message
|
|
|
|
const message = this.findDanglingModeration(attrs);
|
|
|
|
if (message) {
|
|
|
|
const moderation_attrs = pick(message.attributes, MODERATION_ATTRIBUTES);
|
|
|
|
const new_attrs = Object.assign({'dangling_moderation': false}, attrs, moderation_attrs);
|
|
|
|
delete new_attrs['id']; // Delete id, otherwise a new cache entry gets created
|
|
|
|
message.save(new_attrs);
|
|
|
|
return true;
|
|
|
|
}
|
2019-04-10 22:03:00 +02:00
|
|
|
}
|
2019-09-24 15:33:41 +02:00
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
2020-03-02 16:59:18 +01:00
|
|
|
* Queue an incoming message stanza meant for this {@link _converse.Chatroom} for processing.
|
|
|
|
* @async
|
2019-03-29 23:47:56 +01:00
|
|
|
* @private
|
2020-03-02 16:59:18 +01:00
|
|
|
* @method _converse.ChatRoom#queueMessage
|
2019-03-29 23:47:56 +01:00
|
|
|
* @param { XMLElement } stanza - The message stanza.
|
|
|
|
*/
|
2020-03-02 16:59:18 +01:00
|
|
|
queueMessage (stanza) {
|
2020-04-14 12:14:19 +02:00
|
|
|
if (this.messages?.fetched) {
|
2020-03-02 16:59:18 +01:00
|
|
|
this.msg_chain = (this.msg_chain || this.messages.fetched);
|
|
|
|
this.msg_chain = this.msg_chain.then(() => this.onMessage(stanza));
|
|
|
|
return this.msg_chain;
|
|
|
|
} else {
|
2020-02-19 16:46:56 +01:00
|
|
|
this.message_queue.push(stanza);
|
2020-03-02 16:59:18 +01:00
|
|
|
return Promise.resolve();
|
2020-02-19 16:46:56 +01:00
|
|
|
}
|
2020-03-02 16:59:18 +01:00
|
|
|
},
|
2020-02-19 16:46:56 +01:00
|
|
|
|
2020-04-07 22:04:25 +02:00
|
|
|
/**
|
|
|
|
* @param {String} actor - The nickname of the actor that caused the notification
|
|
|
|
* @param {String|Array<String>} states - The state or states representing the type of notificcation
|
|
|
|
*/
|
|
|
|
removeNotification (actor, states) {
|
2020-03-31 22:43:55 +02:00
|
|
|
const actors_per_state = this.notifications.toJSON();
|
2020-04-07 22:04:25 +02:00
|
|
|
states = Array.isArray(states) ? states : [states];
|
|
|
|
states.forEach(state => {
|
2020-04-07 22:15:36 +02:00
|
|
|
const existing_actors = Array.from(actors_per_state[state] || []);
|
2020-04-07 22:04:25 +02:00
|
|
|
if (existing_actors.includes(actor)) {
|
|
|
|
const idx = existing_actors.indexOf(actor);
|
|
|
|
existing_actors.splice(idx, 1);
|
|
|
|
this.notifications.set(state, Array.from(existing_actors));
|
|
|
|
}
|
|
|
|
});
|
2020-03-18 19:32:03 +01:00
|
|
|
},
|
|
|
|
|
2020-03-31 22:43:55 +02:00
|
|
|
/**
|
|
|
|
* Update the notifications model by adding the passed in nickname
|
|
|
|
* to the array of nicknames that all match a particular state.
|
2020-04-07 22:04:25 +02:00
|
|
|
*
|
|
|
|
* Removes the nickname from any other states it might be associated with.
|
|
|
|
*
|
2020-03-31 22:43:55 +02:00
|
|
|
* The state can be a XEP-0085 Chat State or a XEP-0045 join/leave
|
|
|
|
* state.
|
|
|
|
* @param {String} actor - The nickname of the actor that causes the notification
|
|
|
|
* @param {String} state - The state representing the type of notificcation
|
|
|
|
*/
|
|
|
|
updateNotifications (actor, state) {
|
|
|
|
const actors_per_state = this.notifications.toJSON();
|
2020-03-18 19:32:03 +01:00
|
|
|
const existing_actors = actors_per_state[state] || [];
|
|
|
|
if (existing_actors.includes(actor)) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-31 22:43:55 +02:00
|
|
|
const reducer = (out, s) => {
|
2020-03-18 19:32:03 +01:00
|
|
|
if (s === state) {
|
|
|
|
out[s] = [...existing_actors, actor];
|
|
|
|
} else {
|
|
|
|
out[s] = (actors_per_state[s] || []).filter(a => a !== actor);
|
|
|
|
}
|
|
|
|
return out;
|
2020-03-31 22:43:55 +02:00
|
|
|
};
|
|
|
|
const actors_per_chat_state = converse.CHAT_STATES.reduce(reducer, {});
|
|
|
|
const actors_per_traffic_state = converse.MUC_TRAFFIC_STATES.reduce(reducer, {});
|
2020-04-23 13:36:16 +02:00
|
|
|
const actors_per_role_change = converse.MUC_ROLE_CHANGES.reduce(reducer, {});
|
|
|
|
this.notifications.set(Object.assign(
|
|
|
|
actors_per_chat_state,
|
|
|
|
actors_per_traffic_state,
|
|
|
|
actors_per_role_change
|
|
|
|
));
|
2020-03-31 22:43:55 +02:00
|
|
|
window.setTimeout(() => this.removeNotification(actor, state), 10000);
|
2020-03-18 19:32:03 +01:00
|
|
|
},
|
|
|
|
|
2020-03-02 16:59:18 +01:00
|
|
|
/**
|
|
|
|
* Handler for all MUC messages sent to this groupchat. This method
|
|
|
|
* shouldn't be called directly, instead {@link _converse.ChatRoom#queueMessage}
|
|
|
|
* should be called.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#onMessage
|
2020-04-24 17:33:32 +02:00
|
|
|
* @param { MessageAttributes } attrs - The message attributes
|
2020-03-02 16:59:18 +01:00
|
|
|
*/
|
2020-04-24 17:33:32 +02:00
|
|
|
async onMessage (attrs) {
|
|
|
|
if (u.isErrorObject(attrs)) {
|
|
|
|
attrs.stanza && log.error(attrs.stanza);
|
|
|
|
return log.error(attrs.message);
|
2019-09-13 08:57:49 +02:00
|
|
|
}
|
2020-04-24 17:33:32 +02:00
|
|
|
// TODO: move to OMEMO
|
|
|
|
attrs = attrs.encrypted ? await this.decrypt(attrs) : attrs;
|
2020-02-07 12:15:23 +01:00
|
|
|
const message = this.getDuplicateMessage(attrs);
|
2019-03-07 15:44:58 +01:00
|
|
|
if (message) {
|
2020-04-24 17:33:32 +02:00
|
|
|
return this.updateMessage(message, attrs);
|
|
|
|
} else if (attrs.is_receipt_request || attrs.is_marker || this.ignorableCSN(attrs)) {
|
|
|
|
return;
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2020-03-02 16:59:18 +01:00
|
|
|
if (await this.handleRetraction(attrs) ||
|
|
|
|
await this.handleModeration(attrs) ||
|
2020-04-08 14:31:18 +02:00
|
|
|
await this.handleSubjectChange(attrs)) {
|
2020-04-24 17:33:32 +02:00
|
|
|
return this.removeNotification(attrs.nick, ['composing', 'paused']);
|
2019-09-24 15:33:41 +02:00
|
|
|
}
|
2019-09-29 11:40:53 +02:00
|
|
|
this.setEditable(attrs, attrs.time);
|
2019-09-24 15:33:41 +02:00
|
|
|
|
2020-03-18 19:32:03 +01:00
|
|
|
if (attrs['chat_state']) {
|
2020-03-31 22:43:55 +02:00
|
|
|
this.updateNotifications(attrs.nick, attrs.chat_state);
|
2020-03-23 20:54:27 +01:00
|
|
|
}
|
|
|
|
if (u.shouldCreateGroupchatMessage(attrs)) {
|
2020-03-02 16:59:18 +01:00
|
|
|
const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);
|
2020-04-07 22:04:25 +02:00
|
|
|
this.removeNotification(attrs.nick, ['composing', 'paused']);
|
2019-02-13 15:22:54 +01:00
|
|
|
this.incrementUnreadMsgCounter(msg);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
},
|
2018-09-02 15:07:14 +02:00
|
|
|
|
2019-06-11 10:34:44 +02:00
|
|
|
handleModifyError(pres) {
|
2020-03-24 12:26:34 +01:00
|
|
|
const text = pres.querySelector('error text')?.textContent;
|
2019-06-11 10:34:44 +02:00
|
|
|
if (text) {
|
2019-12-10 15:40:33 +01:00
|
|
|
if (this.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
|
2019-06-12 07:52:57 +02:00
|
|
|
this.setDisconnectionMessage(text);
|
|
|
|
} else {
|
|
|
|
const attrs = {
|
|
|
|
'type': 'error',
|
2019-08-12 20:16:34 +02:00
|
|
|
'message': text,
|
2019-09-24 15:33:41 +02:00
|
|
|
'is_ephemeral': true
|
2019-06-12 07:52:57 +02:00
|
|
|
}
|
2020-03-02 16:59:18 +01:00
|
|
|
this.createMessage(attrs);
|
2019-06-11 10:34:44 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-06-11 12:16:27 +02:00
|
|
|
handleDisconnection (stanza) {
|
2019-09-07 22:00:28 +02:00
|
|
|
const is_self = stanza.querySelector("status[code='110']") !== null;
|
2019-06-11 12:16:27 +02:00
|
|
|
const x = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza).pop();
|
|
|
|
if (!x) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const codes = sizzle('status', x).map(s => s.getAttribute('code'));
|
2019-11-01 16:04:55 +01:00
|
|
|
const disconnection_codes = intersection(codes, Object.keys(_converse.muc.disconnect_messages));
|
2019-06-11 12:16:27 +02:00
|
|
|
const disconnected = is_self && disconnection_codes.length > 0;
|
|
|
|
if (!disconnected) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// By using querySelector we assume here there is
|
|
|
|
// one <item> per <x xmlns='http://jabber.org/protocol/muc#user'>
|
|
|
|
// element. This appears to be a safe assumption, since
|
|
|
|
// each <x/> element pertains to a single user.
|
|
|
|
const item = x.querySelector('item');
|
2020-03-24 12:26:34 +01:00
|
|
|
const reason = item ? item.querySelector('reason')?.textContent : undefined;
|
2019-11-01 16:04:55 +01:00
|
|
|
const actor = item ? invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
|
2019-06-11 12:16:27 +02:00
|
|
|
const message = _converse.muc.disconnect_messages[disconnection_codes[0]];
|
|
|
|
this.setDisconnectionMessage(message, reason, actor);
|
|
|
|
},
|
|
|
|
|
|
|
|
|
2020-02-14 13:19:12 +01:00
|
|
|
getActionInfoMessage (code, nick, actor) {
|
|
|
|
if (code === '301') {
|
|
|
|
return actor ? __("%1$s has been banned by %2$s", nick, actor) : __("%1$s has been banned", nick);
|
|
|
|
} else if (code === '303') {
|
2020-04-27 14:57:45 +02:00
|
|
|
return ___("%1$s's nickname has changed", nick);
|
2020-02-14 13:19:12 +01:00
|
|
|
} else if (code === '307') {
|
|
|
|
return actor ? __("%1$s has been kicked out by %2$s", nick, actor) : __("%1$s has been kicked out", nick);
|
|
|
|
} else if (code === '321') {
|
|
|
|
return __("%1$s has been removed because of an affiliation change");
|
|
|
|
} else if (code === '322') {
|
|
|
|
return ___("%1$s has been removed for not being a member");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-04-13 11:10:25 +02:00
|
|
|
createAffiliationChangeMessage (occupant) {
|
|
|
|
const previous_affiliation = occupant._previousAttributes.affiliation;
|
2020-05-07 10:03:28 +02:00
|
|
|
|
|
|
|
if (!previous_affiliation) {
|
|
|
|
// If no previous affiliation was set, then we don't
|
|
|
|
// interpret this as an affiliation change.
|
|
|
|
// For example, if muc_send_probes is true, then occupants
|
|
|
|
// are created based on incoming messages, in which case
|
|
|
|
// we don't yet know the affiliation
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-13 11:10:25 +02:00
|
|
|
const current_affiliation = occupant.get('affiliation');
|
|
|
|
if (previous_affiliation === 'admin') {
|
|
|
|
this.createMessage({
|
|
|
|
'type': 'info',
|
|
|
|
'message': __("%1$s is no longer an admin of this groupchat", occupant.get('nick'))
|
|
|
|
});
|
|
|
|
} else if (previous_affiliation === 'owner') {
|
|
|
|
this.createMessage({
|
|
|
|
'type': 'info',
|
|
|
|
'message': __("%1$s is no longer an owner of this groupchat", occupant.get('nick'))
|
|
|
|
});
|
|
|
|
} else if (previous_affiliation === 'outcast') {
|
|
|
|
this.createMessage({
|
|
|
|
'type': 'info',
|
|
|
|
'message': __("%1$s is no longer banned from this groupchat", occupant.get('nick'))
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (current_affiliation === 'none' && previous_affiliation === 'member') {
|
|
|
|
this.createMessage({
|
|
|
|
'type': 'info',
|
|
|
|
'message': __("%1$s is no longer a member of this groupchat", occupant.get('nick'))
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (current_affiliation === 'member') {
|
|
|
|
this.createMessage({
|
|
|
|
'type': 'info',
|
|
|
|
'message': __("%1$s is now a member of this groupchat", occupant.get('nick'))
|
|
|
|
});
|
|
|
|
} else if (current_affiliation === 'admin' || current_affiliation == 'owner') {
|
|
|
|
// For example: AppleJack is now an (admin|owner) of this groupchat
|
|
|
|
this.createMessage({
|
|
|
|
'type': 'info',
|
|
|
|
'message': __(
|
|
|
|
'%1$s is now an %2$s of this groupchat',
|
|
|
|
occupant.get('nick'),
|
|
|
|
current_affiliation
|
|
|
|
)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
createRoleChangeMessage (occupant, changed) {
|
|
|
|
if (changed === "none" || occupant.changed.affiliation) {
|
|
|
|
// We don't inform of role changes if they accompany affiliation changes.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const previous_role = occupant._previousAttributes.role;
|
|
|
|
if (previous_role === 'moderator') {
|
2020-04-23 13:36:16 +02:00
|
|
|
this.updateNotifications(occupant.get('nick'), 'deop');
|
|
|
|
} else if (previous_role === 'visitor') {
|
|
|
|
this.updateNotifications(occupant.get('nick'), 'voice');
|
2020-04-13 11:10:25 +02:00
|
|
|
}
|
|
|
|
if (occupant.get('role') === 'visitor') {
|
2020-04-23 13:36:16 +02:00
|
|
|
this.updateNotifications(occupant.get('nick'), 'mute');
|
|
|
|
} else if (occupant.get('role') === 'moderator') {
|
2020-04-13 11:10:25 +02:00
|
|
|
if (!['owner', 'admin'].includes(occupant.get('affiliation'))) {
|
|
|
|
// Oly show this message if the user isn't already
|
|
|
|
// an admin or owner, otherwise this isn't new information.
|
2020-04-23 13:36:16 +02:00
|
|
|
this.updateNotifications(occupant.get('nick'), 'op');
|
2020-04-13 11:10:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2020-02-14 13:19:12 +01:00
|
|
|
|
2019-06-11 12:16:27 +02:00
|
|
|
/**
|
2020-04-07 11:30:43 +02:00
|
|
|
* Create info messages based on a received presence or message stanza
|
2019-06-11 12:16:27 +02:00
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#createInfoMessages
|
2020-04-07 11:30:43 +02:00
|
|
|
* @param { XMLElement } stanza
|
2019-06-11 12:16:27 +02:00
|
|
|
*/
|
|
|
|
createInfoMessages (stanza) {
|
2019-08-14 08:51:06 +02:00
|
|
|
const is_self = stanza.querySelector("status[code='110']") !== null;
|
2019-06-11 12:16:27 +02:00
|
|
|
const x = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza).pop();
|
|
|
|
if (!x) {
|
|
|
|
return;
|
|
|
|
}
|
2020-04-13 11:10:25 +02:00
|
|
|
sizzle('status', x).map(s => s.getAttribute('code')).forEach(code => {
|
2020-02-14 13:19:12 +01:00
|
|
|
const data = {
|
2020-04-13 11:10:25 +02:00
|
|
|
'type': 'info',
|
2020-02-14 13:19:12 +01:00
|
|
|
};
|
2019-06-11 12:16:27 +02:00
|
|
|
if (code === '110' || (code === '100' && !is_self)) {
|
|
|
|
return;
|
|
|
|
} else if (code in _converse.muc.info_messages) {
|
2020-02-14 13:19:12 +01:00
|
|
|
data.message = _converse.muc.info_messages[code];
|
|
|
|
} else if (!is_self && _converse.muc.action_info_codes.includes(code)) {
|
2019-06-11 12:16:27 +02:00
|
|
|
const nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
|
|
|
|
const item = x.querySelector('item');
|
2020-02-14 13:19:12 +01:00
|
|
|
data.actor = item ? invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
|
2020-03-24 12:26:34 +01:00
|
|
|
data.reason = item ? item.querySelector('reason')?.textContent : undefined;
|
2020-02-14 13:19:12 +01:00
|
|
|
data.message = this.getActionInfoMessage(code, nick, data.actor);
|
2019-06-11 12:16:27 +02:00
|
|
|
} else if (is_self && (code in _converse.muc.new_nickname_messages)) {
|
|
|
|
let nick;
|
|
|
|
if (is_self && code === "210") {
|
|
|
|
nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
|
|
|
|
} else if (is_self && code === "303") {
|
|
|
|
nick = stanza.querySelector('x item').getAttribute('nick');
|
|
|
|
}
|
|
|
|
this.save('nick', nick);
|
2020-02-14 13:19:12 +01:00
|
|
|
data.message = __(_converse.muc.new_nickname_messages[code], nick);
|
2019-06-11 12:16:27 +02:00
|
|
|
}
|
2020-02-14 13:19:12 +01:00
|
|
|
if (data.message) {
|
|
|
|
if (code === "201" && this.messages.findWhere(data)) {
|
2019-11-15 18:15:41 +01:00
|
|
|
return;
|
|
|
|
} else if (code in _converse.muc.info_messages &&
|
|
|
|
this.messages.length &&
|
2020-02-14 13:19:12 +01:00
|
|
|
this.messages.pop().get('message') === data.message) {
|
2019-11-15 18:15:41 +01:00
|
|
|
// XXX: very naive duplication checking
|
|
|
|
return;
|
|
|
|
}
|
2020-03-02 16:59:18 +01:00
|
|
|
this.createMessage(data);
|
2019-06-11 12:16:27 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setDisconnectionMessage (message, reason, actor) {
|
|
|
|
this.save({
|
|
|
|
'disconnection_message': message,
|
|
|
|
'disconnection_reason': reason,
|
|
|
|
'disconnection_actor': actor
|
|
|
|
});
|
2019-12-10 15:40:33 +01:00
|
|
|
this.session.save({'connection_status': converse.ROOMSTATUS.DISCONNECTED});
|
2019-06-11 12:16:27 +02:00
|
|
|
},
|
|
|
|
|
2019-06-11 10:34:44 +02:00
|
|
|
|
2019-06-11 12:16:27 +02:00
|
|
|
onNicknameClash (presence) {
|
2020-03-31 13:15:57 +02:00
|
|
|
if (api.settings.get('muc_nickname_from_jid')) {
|
2019-06-11 12:16:27 +02:00
|
|
|
const nick = presence.getAttribute('from').split('/')[1];
|
|
|
|
if (nick === _converse.getDefaultMUCNickname()) {
|
|
|
|
this.join(nick + '-2');
|
|
|
|
} else {
|
|
|
|
const del= nick.lastIndexOf("-");
|
|
|
|
const num = nick.substring(del+1, nick.length);
|
|
|
|
this.join(nick.substring(0, del+1) + String(Number(num)+1));
|
|
|
|
}
|
2019-05-28 13:59:53 +02:00
|
|
|
} else {
|
2019-06-11 12:16:27 +02:00
|
|
|
this.save({
|
|
|
|
'nickname_validation_message': __(
|
|
|
|
"The nickname you chose is reserved or "+
|
2019-12-10 15:40:33 +01:00
|
|
|
"currently in use, please choose a different one."
|
|
|
|
)
|
2019-06-11 12:16:27 +02:00
|
|
|
});
|
2019-12-10 15:40:33 +01:00
|
|
|
this.session.save({'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
|
2019-06-11 12:16:27 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parses a <presence> stanza with type "error" and sets the proper
|
|
|
|
* `connection_status` value for this {@link _converse.ChatRoom} as
|
|
|
|
* well as any additional output that can be shown to the user.
|
|
|
|
* @private
|
|
|
|
* @param { XMLElement } stanza - The presence stanza
|
|
|
|
*/
|
|
|
|
onErrorPresence (stanza) {
|
|
|
|
const error = stanza.querySelector('error');
|
|
|
|
const error_type = error.getAttribute('type');
|
2020-03-24 12:26:34 +01:00
|
|
|
const reason = sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop()?.textContent;
|
2019-06-11 12:16:27 +02:00
|
|
|
|
|
|
|
if (error_type === 'modify') {
|
|
|
|
this.handleModifyError(stanza);
|
|
|
|
} else if (error_type === 'auth') {
|
2019-06-16 12:14:29 +02:00
|
|
|
if (sizzle(`not-authorized[xmlns="${Strophe.NS.STANZAS}"]`, error).length) {
|
2019-12-10 15:40:33 +01:00
|
|
|
this.save({'password_validation_message': reason || __("Password incorrect")});
|
|
|
|
this.session.save({'connection_status': converse.ROOMSTATUS.PASSWORD_REQUIRED});
|
2019-06-16 12:14:29 +02:00
|
|
|
}
|
2019-06-11 12:16:27 +02:00
|
|
|
if (error.querySelector('registration-required')) {
|
2019-06-16 12:14:29 +02:00
|
|
|
const message = __('You are not on the member list of this groupchat.');
|
|
|
|
this.setDisconnectionMessage(message, reason);
|
2019-06-11 12:16:27 +02:00
|
|
|
} else if (error.querySelector('forbidden')) {
|
2019-06-16 12:14:29 +02:00
|
|
|
const message = __('You have been banned from this groupchat.');
|
|
|
|
this.setDisconnectionMessage(message, reason);
|
2019-06-11 12:16:27 +02:00
|
|
|
}
|
|
|
|
} else if (error_type === 'cancel') {
|
|
|
|
if (error.querySelector('not-allowed')) {
|
2019-06-16 12:14:29 +02:00
|
|
|
const message = __('You are not allowed to create new groupchats.');
|
|
|
|
this.setDisconnectionMessage(message, reason);
|
2019-06-11 12:16:27 +02:00
|
|
|
} else if (error.querySelector('not-acceptable')) {
|
2019-06-16 12:14:29 +02:00
|
|
|
const message = __("Your nickname doesn't conform to this groupchat's policies.");
|
|
|
|
this.setDisconnectionMessage(message, reason);
|
2019-06-11 12:16:27 +02:00
|
|
|
} else if (sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).length) {
|
2020-03-24 12:26:34 +01:00
|
|
|
const moved_jid = sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).pop()?.textContent
|
2019-06-11 12:16:27 +02:00
|
|
|
.replace(/^xmpp:/, '')
|
|
|
|
.replace(/\?join$/, '');
|
2019-12-10 15:40:33 +01:00
|
|
|
this.save({ moved_jid, 'destroyed_reason': reason});
|
|
|
|
this.session.save({'connection_status': converse.ROOMSTATUS.DESTROYED});
|
2019-06-11 12:16:27 +02:00
|
|
|
} else if (error.querySelector('conflict')) {
|
|
|
|
this.onNicknameClash(stanza);
|
|
|
|
} else if (error.querySelector('item-not-found')) {
|
2019-06-16 12:14:29 +02:00
|
|
|
const message = __("This groupchat does not (yet) exist.");
|
|
|
|
this.setDisconnectionMessage(message, reason);
|
2019-06-11 12:16:27 +02:00
|
|
|
} else if (error.querySelector('service-unavailable')) {
|
2019-06-16 12:14:29 +02:00
|
|
|
const message = __("This groupchat has reached its maximum number of participants.");
|
|
|
|
this.setDisconnectionMessage(message, reason);
|
2019-06-11 12:16:27 +02:00
|
|
|
} else if (error.querySelector('remote-server-not-found')) {
|
|
|
|
const message = __("Remote server not found");
|
2019-06-16 12:14:29 +02:00
|
|
|
const feedback = reason ? __('The explanation given is: "%1$s".', reason) : undefined;
|
|
|
|
this.setDisconnectionMessage(message, feedback);
|
2019-06-11 12:16:27 +02:00
|
|
|
}
|
2019-05-28 13:59:53 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-06-11 12:16:27 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Handles all MUC presence stanzas.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#onPresence
|
2019-05-28 13:59:53 +02:00
|
|
|
* @param { XMLElement } stanza
|
2019-03-29 23:47:56 +01:00
|
|
|
*/
|
2019-05-28 13:59:53 +02:00
|
|
|
onPresence (stanza) {
|
|
|
|
if (stanza.getAttribute('type') === 'error') {
|
|
|
|
return this.onErrorPresence(stanza);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-07-10 09:47:13 +02:00
|
|
|
this.createInfoMessages(stanza);
|
2019-05-28 13:59:53 +02:00
|
|
|
if (stanza.querySelector("status[code='110']")) {
|
|
|
|
this.onOwnPresence(stanza);
|
2019-07-10 09:47:13 +02:00
|
|
|
if (this.getOwnRole() !== 'none' &&
|
2019-12-10 15:40:33 +01:00
|
|
|
this.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
|
|
|
|
this.session.save('connection_status', converse.ROOMSTATUS.CONNECTED);
|
2019-07-10 09:47:13 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.updateOccupantsOnPresence(stanza);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
},
|
2018-04-08 19:44:53 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Handles a received presence relating to the current user.
|
|
|
|
*
|
|
|
|
* For locked groupchats (which are by definition "new"), the
|
|
|
|
* groupchat will either be auto-configured or created instantly
|
|
|
|
* (with default config) or a configuration groupchat will be
|
|
|
|
* rendered.
|
|
|
|
*
|
|
|
|
* If the groupchat is not locked, then the groupchat will be
|
|
|
|
* auto-configured only if applicable and if the current
|
|
|
|
* user is the groupchat's owner.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#onOwnPresence
|
|
|
|
* @param { XMLElement } pres - The stanza
|
|
|
|
*/
|
2019-05-28 13:59:53 +02:00
|
|
|
onOwnPresence (stanza) {
|
2019-07-10 09:47:13 +02:00
|
|
|
if (stanza.getAttribute('type') !== 'unavailable') {
|
2019-12-10 15:40:33 +01:00
|
|
|
this.session.save('connection_status', converse.ROOMSTATUS.ENTERED);
|
2019-07-10 09:47:13 +02:00
|
|
|
}
|
|
|
|
this.updateOccupantsOnPresence(stanza);
|
2019-05-28 13:59:53 +02:00
|
|
|
|
|
|
|
if (stanza.getAttribute('type') === 'unavailable') {
|
2019-06-11 12:16:27 +02:00
|
|
|
this.handleDisconnection(stanza);
|
2019-05-28 13:59:53 +02:00
|
|
|
} else {
|
|
|
|
const locked_room = stanza.querySelector("status[code='201']");
|
|
|
|
if (locked_room) {
|
|
|
|
if (this.get('auto_configure')) {
|
2020-01-16 13:01:57 +01:00
|
|
|
this.autoConfigureChatRoom().then(() => this.refreshDiscoInfo());
|
2020-03-31 13:15:57 +02:00
|
|
|
} else if (api.settings.get('muc_instant_rooms')) {
|
2019-05-28 13:59:53 +02:00
|
|
|
// Accept default configuration
|
2020-01-16 13:01:57 +01:00
|
|
|
this.sendConfiguration().then(() => this.refreshDiscoInfo());
|
2019-05-28 13:59:53 +02:00
|
|
|
} else {
|
|
|
|
/**
|
|
|
|
* Triggered when a new room has been created which first needs to be configured
|
|
|
|
* and when `auto_configure` is set to `false`.
|
|
|
|
* Used by `_converse.ChatRoomView` in order to know when to render the
|
|
|
|
* configuration form for a new room.
|
|
|
|
* @event _converse.ChatRoom#configurationNeeded
|
|
|
|
* @example _converse.api.listen.on('configurationNeeded', () => { ... });
|
|
|
|
*/
|
|
|
|
this.trigger('configurationNeeded');
|
|
|
|
return; // We haven't yet entered the groupchat, so bail here.
|
|
|
|
}
|
|
|
|
} else if (!this.features.get('fetched')) {
|
|
|
|
// The features for this groupchat weren't fetched.
|
|
|
|
// That must mean it's a new groupchat without locking
|
|
|
|
// (in which case Prosody doesn't send a 201 status),
|
|
|
|
// otherwise the features would have been fetched in
|
|
|
|
// the "initialize" method already.
|
2019-07-10 09:47:13 +02:00
|
|
|
if (this.getOwnAffiliation() === 'owner' && this.get('auto_configure')) {
|
2020-01-16 13:01:57 +01:00
|
|
|
this.autoConfigureChatRoom().then(() => this.refreshDiscoInfo());
|
2019-05-28 13:59:53 +02:00
|
|
|
} else {
|
2020-01-16 13:01:57 +01:00
|
|
|
this.getDiscoInfo();
|
2019-05-28 13:59:53 +02:00
|
|
|
}
|
2017-05-23 18:36:40 +02:00
|
|
|
}
|
2017-05-24 08:40:09 +02:00
|
|
|
}
|
2019-12-10 15:40:33 +01:00
|
|
|
this.session.save({'connection_status': converse.ROOMSTATUS.ENTERED});
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2017-05-23 18:36:40 +02:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* Returns a boolean to indicate whether the current user
|
|
|
|
* was mentioned in a message.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#isUserMentioned
|
|
|
|
* @param { String } - The text message
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
isUserMentioned (message) {
|
|
|
|
const nick = this.get('nick');
|
|
|
|
if (message.get('references').length) {
|
|
|
|
const mentions = message.get('references').filter(ref => (ref.type === 'mention')).map(ref => ref.value);
|
2019-09-07 22:00:28 +02:00
|
|
|
return mentions.includes(nick);
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
|
|
|
return (new RegExp(`\\b${nick}\\b`)).test(message.get('message'));
|
|
|
|
}
|
|
|
|
},
|
2016-02-16 08:46:47 +01:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/* Given a newly received message, update the unread counter if necessary.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#incrementUnreadMsgCounter
|
|
|
|
* @param { XMLElement } - The <messsage> stanza
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
incrementUnreadMsgCounter (message) {
|
|
|
|
if (!message) { return; }
|
|
|
|
const body = message.get('message');
|
2019-08-05 01:39:57 +02:00
|
|
|
if (!body) { return; }
|
2018-10-23 03:41:38 +02:00
|
|
|
if (u.isNewMessage(message) && this.isHidden()) {
|
2020-04-28 10:35:19 +02:00
|
|
|
this.setFirstUnreadMsgId(message);
|
2018-10-23 03:41:38 +02:00
|
|
|
const settings = {'num_unread_general': this.get('num_unread_general') + 1};
|
|
|
|
if (this.isUserMentioned(message)) {
|
|
|
|
settings.num_unread = this.get('num_unread') + 1;
|
|
|
|
_converse.incrementMsgCounter();
|
|
|
|
}
|
|
|
|
this.save(settings);
|
|
|
|
}
|
|
|
|
},
|
2018-05-09 12:28:34 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
clearUnreadMsgCounter() {
|
|
|
|
u.safeSave(this, {
|
|
|
|
'num_unread': 0,
|
|
|
|
'num_unread_general': 0
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2018-05-09 12:28:34 +02:00
|
|
|
|
2018-05-03 14:50:27 +02:00
|
|
|
|
2019-07-10 09:47:13 +02:00
|
|
|
/**
|
2020-05-06 13:53:13 +02:00
|
|
|
* Represents a participant in a MUC
|
2019-07-10 09:47:13 +02:00
|
|
|
* @class
|
|
|
|
* @namespace _converse.ChatRoomOccupant
|
|
|
|
* @memberOf _converse
|
|
|
|
*/
|
2019-09-19 16:54:55 +02:00
|
|
|
_converse.ChatRoomOccupant = Model.extend({
|
2018-05-03 14:50:27 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
defaults: {
|
2020-04-13 15:19:21 +02:00
|
|
|
'hats': [],
|
2018-12-17 13:08:17 +01:00
|
|
|
'show': 'offline',
|
|
|
|
'states': []
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-05-25 15:55:34 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
initialize (attributes) {
|
2019-10-11 15:32:38 +02:00
|
|
|
this.set(Object.assign({'id': u.getUniqueId()}, attributes));
|
2018-10-23 03:41:38 +02:00
|
|
|
this.on('change:image_hash', this.onAvatarChanged, this);
|
|
|
|
},
|
2018-05-24 09:09:26 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
onAvatarChanged () {
|
|
|
|
const hash = this.get('image_hash');
|
|
|
|
const vcards = [];
|
|
|
|
if (this.get('jid')) {
|
|
|
|
vcards.push(_converse.vcards.findWhere({'jid': this.get('jid')}));
|
2018-02-21 22:29:21 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
vcards.push(_converse.vcards.findWhere({'jid': this.get('from')}));
|
2017-12-07 07:05:37 +01:00
|
|
|
|
2019-09-07 22:00:28 +02:00
|
|
|
vcards.filter(v => v).forEach(vcard => {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (hash && vcard.get('image_hash') !== hash) {
|
2020-03-31 13:15:57 +02:00
|
|
|
api.vcard.update(vcard, true);
|
2017-02-17 22:17:19 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
},
|
2018-05-09 12:28:34 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
getDisplayName () {
|
|
|
|
return this.get('nick') || this.get('jid');
|
|
|
|
},
|
2018-05-09 12:28:34 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
isMember () {
|
2019-07-08 13:45:50 +02:00
|
|
|
return ['admin', 'owner', 'member'].includes(this.get('affiliation'));
|
|
|
|
},
|
|
|
|
|
|
|
|
isModerator () {
|
|
|
|
return ['admin', 'owner'].includes(this.get('affiliation')) || this.get('role') === 'moderator';
|
2018-12-13 15:36:37 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
isSelf () {
|
|
|
|
return this.get('states').includes('110');
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
});
|
2017-02-17 22:17:19 +01:00
|
|
|
|
2017-12-20 11:02:01 +01:00
|
|
|
|
2020-05-06 13:53:13 +02:00
|
|
|
/**
|
|
|
|
* A list of {@link _converse.ChatRoomOccupant} instances, representing participants in a MUC.
|
|
|
|
* @class
|
|
|
|
* @namespace _converse.ChatRoomOccupants
|
|
|
|
* @memberOf _converse
|
|
|
|
*/
|
2019-09-19 16:54:55 +02:00
|
|
|
_converse.ChatRoomOccupants = Collection.extend({
|
2018-10-23 03:41:38 +02:00
|
|
|
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.getDisplayName().toLowerCase();
|
|
|
|
const nick2 = occupant2.getDisplayName().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
|
|
|
|
2020-02-21 16:20:30 +01:00
|
|
|
getAutoFetchedAffiliationLists () {
|
2020-03-31 13:15:57 +02:00
|
|
|
const affs = api.settings.get('muc_fetch_members');
|
2020-02-21 16:20:30 +01:00
|
|
|
return Array.isArray(affs) ? affs : (affs ? ['member', 'admin', 'owner'] : []);
|
|
|
|
},
|
|
|
|
|
|
|
|
async fetchMembers () {
|
|
|
|
const affiliations = this.getAutoFetchedAffiliationLists();
|
|
|
|
if (affiliations.length === 0) {
|
2020-02-20 17:11:07 +01:00
|
|
|
return;
|
|
|
|
}
|
2020-02-21 16:20:30 +01:00
|
|
|
const aff_lists = await Promise.all(affiliations.map(a => this.chatroom.getAffiliationList(a)));
|
2019-07-04 14:12:12 +02:00
|
|
|
const new_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
|
2020-02-21 16:20:30 +01:00
|
|
|
const known_affiliations = affiliations.filter(a => !u.isErrorObject(aff_lists[affiliations.indexOf(a)]));
|
2019-07-29 10:19:05 +02:00
|
|
|
const new_jids = new_members.map(m => m.jid).filter(m => m !== undefined);
|
|
|
|
const new_nicks = new_members.map(m => !m.jid && m.nick || undefined).filter(m => m !== undefined);
|
2019-07-08 12:33:07 +02:00
|
|
|
const removed_members = this.filter(m => {
|
2019-08-08 15:12:18 +02:00
|
|
|
return known_affiliations.includes(m.get('affiliation')) &&
|
2019-07-08 12:33:07 +02:00
|
|
|
!new_nicks.includes(m.get('nick')) &&
|
|
|
|
!new_jids.includes(m.get('jid'));
|
|
|
|
});
|
2018-10-24 18:28:28 +02:00
|
|
|
|
2019-05-14 11:38:41 +02:00
|
|
|
removed_members.forEach(occupant => {
|
2018-10-24 18:28:28 +02:00
|
|
|
if (occupant.get('jid') === _converse.bare_jid) { return; }
|
|
|
|
if (occupant.get('show') === 'offline') {
|
|
|
|
occupant.destroy();
|
2019-07-03 14:54:50 +02:00
|
|
|
} else {
|
|
|
|
occupant.save('affiliation', null);
|
2018-10-24 18:28:28 +02:00
|
|
|
}
|
|
|
|
});
|
2019-05-14 11:38:41 +02:00
|
|
|
new_members.forEach(attrs => {
|
2019-08-08 15:12:18 +02:00
|
|
|
const occupant = attrs.jid ?
|
|
|
|
this.findOccupant({'jid': attrs.jid}) :
|
|
|
|
this.findOccupant({'nick': attrs.nick});
|
2018-10-24 18:28:28 +02:00
|
|
|
if (occupant) {
|
|
|
|
occupant.save(attrs);
|
|
|
|
} else {
|
|
|
|
this.create(attrs);
|
|
|
|
}
|
|
|
|
});
|
2019-10-24 14:29:15 +02:00
|
|
|
/**
|
|
|
|
* Triggered once the member lists for this MUC have been fetched and processed.
|
|
|
|
* @event _converse#membersFetched
|
|
|
|
* @example _converse.api.listen.on('membersFetched', () => { ... });
|
|
|
|
*/
|
2020-03-31 13:15:57 +02:00
|
|
|
api.trigger('membersFetched');
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2017-02-15 21:30:32 +01:00
|
|
|
|
2020-05-06 13:53:13 +02:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoomOccupants#findOccupant
|
|
|
|
* @param { Object } data
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
findOccupant (data) {
|
|
|
|
const jid = Strophe.getBareJidFromJid(data.jid);
|
2020-05-06 13:53:13 +02:00
|
|
|
return (jid && this.findWhere({ jid })) || this.findWhere({'nick': data.nick});
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
});
|
2016-12-07 11:34:02 +01:00
|
|
|
|
2016-12-06 21:04:08 +01:00
|
|
|
|
2019-09-19 16:54:55 +02:00
|
|
|
_converse.RoomsPanelModel = Model.extend({
|
2019-03-26 11:31:11 +01:00
|
|
|
defaults: function () {
|
|
|
|
return {
|
2020-03-31 13:15:57 +02:00
|
|
|
'muc_domain': api.settings.get('muc_domain'),
|
2019-03-26 11:31:11 +01:00
|
|
|
'nick': _converse.getDefaultMUCNickname()
|
|
|
|
}
|
2019-04-05 12:49:24 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
setDomain (jid) {
|
2020-03-31 13:15:57 +02:00
|
|
|
if (!api.settings.get('locked_muc_domain')) {
|
2019-04-05 12:49:24 +02:00
|
|
|
this.save('muc_domain', Strophe.getDomainFromJid(jid));
|
|
|
|
}
|
2019-03-26 11:31:11 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
2016-12-06 16:18:33 +01:00
|
|
|
|
2016-02-16 08:46:47 +01:00
|
|
|
|
2019-03-29 23:47:56 +01:00
|
|
|
/**
|
|
|
|
* A direct MUC invitation to join a groupchat has been received
|
|
|
|
* See XEP-0249: Direct MUC invitations.
|
|
|
|
* @private
|
|
|
|
* @method _converse.ChatRoom#onDirectMUCInvitation
|
|
|
|
* @param { XMLElement } message - The message stanza containing the invitation.
|
|
|
|
*/
|
2019-10-04 18:26:33 +02:00
|
|
|
_converse.onDirectMUCInvitation = async function (message) {
|
2018-10-23 03:41:38 +02:00
|
|
|
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');
|
|
|
|
|
2020-01-16 13:48:46 +01:00
|
|
|
let result;
|
2020-03-31 13:15:57 +02:00
|
|
|
if (api.settings.get('auto_join_on_invite')) {
|
2018-10-23 03:41:38 +02:00
|
|
|
result = true;
|
|
|
|
} else {
|
|
|
|
// Invite request might come from someone not your roster list
|
2020-01-16 13:48:46 +01:00
|
|
|
let contact = _converse.roster.get(from);
|
|
|
|
contact = contact ? contact.getDisplayName(): from;
|
2018-10-23 03:41:38 +02:00
|
|
|
if (!reason) {
|
|
|
|
result = confirm(
|
|
|
|
__("%1$s has invited you to join a groupchat: %2$s", contact, room_jid)
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
result = confirm(
|
|
|
|
__('%1$s has invited you to join a groupchat: %2$s, and left the following reason: "%3$s"',
|
|
|
|
contact, room_jid, reason)
|
|
|
|
);
|
2018-10-13 20:09:01 +02:00
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
if (result === true) {
|
2019-10-04 18:26:33 +02:00
|
|
|
const chatroom = await openChatRoom(room_jid, {'password': x_el.getAttribute('password') });
|
2019-12-10 15:40:33 +01:00
|
|
|
if (chatroom.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
|
2019-12-08 20:42:09 +01:00
|
|
|
_converse.chatboxes.get(room_jid).rejoin();
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2018-02-21 22:40:51 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
2016-03-16 10:03:00 +01:00
|
|
|
|
2020-03-31 13:15:57 +02:00
|
|
|
if (api.settings.get('allow_muc_invitations')) {
|
2018-10-23 03:41:38 +02:00
|
|
|
const registerDirectInvitationHandler = function () {
|
|
|
|
_converse.connection.addHandler(
|
|
|
|
(message) => {
|
|
|
|
_converse.onDirectMUCInvitation(message);
|
|
|
|
return true;
|
|
|
|
}, 'jabber:x:conference', 'message');
|
|
|
|
};
|
2020-03-31 13:15:57 +02:00
|
|
|
api.listen.on('connected', registerDirectInvitationHandler);
|
|
|
|
api.listen.on('reconnected', registerDirectInvitationHandler);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2018-09-04 11:39:27 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
const createChatRoom = function (jid, attrs) {
|
|
|
|
if (jid.startsWith('xmpp:') && jid.endsWith('?join')) {
|
|
|
|
jid = jid.replace(/^xmpp:/, '').replace(/\?join$/, '');
|
2018-09-04 11:39:27 +02:00
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
return api.rooms.get(jid, attrs, true);
|
2018-10-23 03:41:38 +02:00
|
|
|
};
|
|
|
|
|
2019-07-26 19:29:58 +02:00
|
|
|
/**
|
|
|
|
* Automatically join groupchats, based on the
|
|
|
|
* "auto_join_rooms" configuration setting, which is an array
|
|
|
|
* of strings (groupchat JIDs) or objects (with groupchat JID and other
|
|
|
|
* settings).
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
function autoJoinRooms () {
|
2020-03-31 13:15:57 +02:00
|
|
|
api.settings.get('auto_join_rooms').forEach(groupchat => {
|
2019-11-01 16:04:55 +01:00
|
|
|
if (isString(groupchat)) {
|
2019-07-26 19:29:58 +02:00
|
|
|
if (_converse.chatboxes.where({'jid': groupchat}).length) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
api.rooms.open(groupchat);
|
2019-11-01 16:04:55 +01:00
|
|
|
} else if (isObject(groupchat)) {
|
2020-03-31 13:15:57 +02:00
|
|
|
api.rooms.open(groupchat.jid, clone(groupchat));
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
2019-11-06 11:01:34 +01:00
|
|
|
log.error('Invalid groupchat criteria specified for "auto_join_rooms"');
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
});
|
2019-03-29 15:47:23 +01:00
|
|
|
/**
|
|
|
|
* Triggered once any rooms that have been configured to be automatically joined,
|
|
|
|
* specified via the _`auto_join_rooms` setting, have been entered.
|
|
|
|
* @event _converse#roomsAutoJoined
|
|
|
|
* @example _converse.api.listen.on('roomsAutoJoined', () => { ... });
|
|
|
|
* @example _converse.api.waitUntil('roomsAutoJoined').then(() => { ... });
|
|
|
|
*/
|
2020-03-31 13:15:57 +02:00
|
|
|
api.trigger('roomsAutoJoined');
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2018-09-04 11:39:27 +02:00
|
|
|
|
2019-12-05 22:17:27 +01:00
|
|
|
async function onWindowStateChanged (data) {
|
2020-03-31 13:15:57 +02:00
|
|
|
if (data.state === 'visible' && api.connection.connected()) {
|
|
|
|
const rooms = await api.rooms.get();
|
2019-12-05 22:17:27 +01:00
|
|
|
rooms.forEach(room => room.rejoinIfNecessary());
|
|
|
|
}
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
/************************ BEGIN Event Handlers ************************/
|
2020-03-31 13:15:57 +02:00
|
|
|
api.listen.on('beforeTearDown', () => {
|
2019-06-01 15:37:18 +02:00
|
|
|
const groupchats = _converse.chatboxes.where({'type': _converse.CHATROOMS_TYPE});
|
2019-12-10 15:40:33 +01:00
|
|
|
groupchats.forEach(muc => u.safeSave(muc.session, {'connection_status': converse.ROOMSTATUS.DISCONNECTED}));
|
2019-06-01 15:37:18 +02:00
|
|
|
});
|
|
|
|
|
2020-03-31 13:15:57 +02:00
|
|
|
api.listen.on('windowStateChanged', onWindowStateChanged);
|
2019-06-01 15:37:18 +02:00
|
|
|
|
2020-03-31 13:15:57 +02:00
|
|
|
api.listen.on('addClientFeatures', () => {
|
|
|
|
if (api.settings.get('allow_muc')) {
|
|
|
|
api.disco.own.features.add(Strophe.NS.MUC);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
if (api.settings.get('allow_muc_invitations')) {
|
|
|
|
api.disco.own.features.add('jabber:x:conference'); // Invites
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
});
|
2020-03-31 13:15:57 +02:00
|
|
|
api.listen.on('chatBoxesFetched', autoJoinRooms);
|
2019-05-20 17:10:34 +02:00
|
|
|
|
|
|
|
|
2020-03-31 13:15:57 +02:00
|
|
|
api.listen.on('beforeResourceBinding', () => {
|
2020-02-18 23:04:30 +01:00
|
|
|
_converse.connection.addHandler(stanza => {
|
|
|
|
const muc_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
|
|
|
|
if (!_converse.chatboxes.get(muc_jid)) {
|
2020-03-31 13:15:57 +02:00
|
|
|
api.waitUntil('chatBoxesFetched')
|
2020-02-19 14:18:52 +01:00
|
|
|
.then(async () => {
|
2020-02-18 23:04:30 +01:00
|
|
|
const muc = _converse.chatboxes.get(muc_jid);
|
2020-02-19 14:18:52 +01:00
|
|
|
if (muc) {
|
|
|
|
await muc.initialized;
|
|
|
|
await muc.messages.fetched
|
|
|
|
muc.message_handler.run(stanza);
|
|
|
|
}
|
2020-02-18 23:04:30 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}, null, 'message', 'groupchat')
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2019-05-20 17:10:34 +02:00
|
|
|
function disconnectChatRooms () {
|
|
|
|
/* When disconnecting, mark all groupchats as
|
|
|
|
* disconnected, so that they will be properly entered again
|
|
|
|
* when fetched from session storage.
|
|
|
|
*/
|
|
|
|
return _converse.chatboxes
|
|
|
|
.filter(m => (m.get('type') === _converse.CHATROOMS_TYPE))
|
2019-12-10 15:40:33 +01:00
|
|
|
.forEach(m => m.session.save({'connection_status': converse.ROOMSTATUS.DISCONNECTED}));
|
2019-05-20 17:10:34 +02:00
|
|
|
}
|
2020-03-31 13:15:57 +02:00
|
|
|
api.listen.on('disconnected', disconnectChatRooms);
|
2018-10-23 03:41:38 +02:00
|
|
|
|
2020-03-31 13:15:57 +02:00
|
|
|
api.listen.on('statusInitialized', () => {
|
2018-10-23 03:41:38 +02:00
|
|
|
window.addEventListener(_converse.unloadevent, () => {
|
2020-03-31 13:15:57 +02:00
|
|
|
const using_websocket = api.connection.isType('websocket');
|
2019-06-27 15:49:19 +02:00
|
|
|
if (using_websocket &&
|
|
|
|
(!_converse.enable_smacks || !_converse.session.get('smacks_stream_id'))) {
|
|
|
|
// For non-SMACKS websocket connections, or non-resumeable
|
|
|
|
// connections, we disconnect all chatrooms when the page unloads.
|
2019-06-04 10:59:59 +02:00
|
|
|
// See issue #1111
|
2018-10-23 03:41:38 +02:00
|
|
|
disconnectChatRooms();
|
2018-02-21 22:40:51 +01:00
|
|
|
}
|
|
|
|
});
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
/************************ END Event Handlers ************************/
|
|
|
|
|
|
|
|
|
|
|
|
/************************ BEGIN API ************************/
|
2019-11-07 09:08:13 +01:00
|
|
|
converse.env.muc_utils = muc_utils;
|
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
// We extend the default converse.js API to add methods specific to MUC groupchats.
|
2020-03-31 13:15:57 +02:00
|
|
|
Object.assign(api, {
|
2018-10-23 03:41:38 +02:00
|
|
|
/**
|
|
|
|
* The "rooms" namespace groups methods relevant to chatrooms
|
|
|
|
* (aka groupchats).
|
|
|
|
*
|
2020-03-31 13:15:57 +02:00
|
|
|
* @namespace api.rooms
|
|
|
|
* @memberOf api
|
2018-10-23 03:41:38 +02:00
|
|
|
*/
|
2019-09-24 15:33:41 +02:00
|
|
|
rooms: {
|
2018-10-23 03:41:38 +02:00
|
|
|
/**
|
|
|
|
* Creates a new MUC chatroom (aka groupchat)
|
|
|
|
*
|
2020-03-31 13:15:57 +02:00
|
|
|
* Similar to {@link api.rooms.open}, but creates
|
2019-03-14 21:04:12 +01:00
|
|
|
* the chatroom in the background (i.e. doesn't cause a view to open).
|
2018-10-23 03:41:38 +02:00
|
|
|
*
|
2020-03-31 13:15:57 +02:00
|
|
|
* @method api.rooms.create
|
2018-10-23 03:41:38 +02:00
|
|
|
* @param {(string[]|string)} jid|jids The JID or array of
|
|
|
|
* JIDs of the chatroom(s) to create
|
|
|
|
* @param {object} [attrs] attrs The room attributes
|
2019-09-19 16:54:55 +02:00
|
|
|
* @returns {Promise} Promise which resolves with the Model representing the chat.
|
2018-10-23 03:41:38 +02:00
|
|
|
*/
|
2019-07-09 13:45:34 +02:00
|
|
|
create (jids, attrs={}) {
|
2019-11-01 16:04:55 +01:00
|
|
|
attrs = isString(attrs) ? {'nick': attrs} : (attrs || {});
|
2020-03-31 13:15:57 +02:00
|
|
|
if (!attrs.nick && api.settings.get('muc_nickname_from_jid')) {
|
2018-10-23 03:41:38 +02:00
|
|
|
attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
|
|
|
|
}
|
2019-07-29 10:19:05 +02:00
|
|
|
if (jids === undefined) {
|
2018-10-23 03:41:38 +02:00
|
|
|
throw new TypeError('rooms.create: You need to provide at least one JID');
|
2019-11-01 16:04:55 +01:00
|
|
|
} else if (isString(jids)) {
|
2018-10-23 03:41:38 +02:00
|
|
|
return createChatRoom(jids, attrs);
|
|
|
|
}
|
2019-09-07 22:00:28 +02:00
|
|
|
return jids.map(jid => createChatRoom(jid, attrs));
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2016-04-05 13:23:16 +02:00
|
|
|
|
2018-09-02 15:07:14 +02:00
|
|
|
/**
|
2018-10-23 03:41:38 +02:00
|
|
|
* Opens a MUC chatroom (aka groupchat)
|
|
|
|
*
|
2020-03-31 13:15:57 +02:00
|
|
|
* Similar to {@link api.chats.open}, but for groupchats.
|
2018-10-23 03:41:38 +02:00
|
|
|
*
|
2020-03-31 13:15:57 +02:00
|
|
|
* @method api.rooms.open
|
2018-10-23 03:41:38 +02:00
|
|
|
* @param {string} jid The room JID or JIDs (if not specified, all
|
|
|
|
* currently open rooms will be returned).
|
|
|
|
* @param {string} attrs A map containing any extra room attributes.
|
|
|
|
* @param {string} [attrs.nick] The current user's nickname for the MUC
|
|
|
|
* @param {boolean} [attrs.auto_configure] A boolean, indicating
|
|
|
|
* whether the room should be configured automatically or not.
|
|
|
|
* If set to `true`, then it makes sense to pass in configuration settings.
|
|
|
|
* @param {object} [attrs.roomconfig] A map of configuration settings to be used when the room gets
|
|
|
|
* configured automatically. Currently it doesn't make sense to specify
|
|
|
|
* `roomconfig` values if `auto_configure` is set to `false`.
|
|
|
|
* For a list of configuration values that can be passed in, refer to these values
|
2019-03-04 17:49:44 +01:00
|
|
|
* in the [XEP-0045 MUC specification](https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner).
|
2018-10-23 03:41:38 +02:00
|
|
|
* The values should be named without the `muc#roomconfig_` prefix.
|
2019-08-06 13:32:52 +02:00
|
|
|
* @param {boolean} [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not.
|
2018-10-23 03:41:38 +02:00
|
|
|
* @param {boolean} [attrs.bring_to_foreground] A boolean indicating whether the room should be
|
|
|
|
* brought to the foreground and therefore replace the currently shown chat.
|
|
|
|
* If there is no chat currently open, then this option is ineffective.
|
2019-04-16 16:36:58 +02:00
|
|
|
* @param {Boolean} [force=false] - By default, a minimized
|
|
|
|
* room won't be maximized (in `overlayed` view mode) and in
|
|
|
|
* `fullscreen` view mode a newly opened room won't replace
|
|
|
|
* another chat already in the foreground.
|
|
|
|
* Set `force` to `true` if you want to force the room to be
|
|
|
|
* maximized or shown.
|
2019-09-19 16:54:55 +02:00
|
|
|
* @returns {Promise} Promise which resolves with the Model representing the chat.
|
2018-10-23 03:41:38 +02:00
|
|
|
*
|
|
|
|
* @example
|
2020-03-31 13:15:57 +02:00
|
|
|
* this.api.rooms.open('group@muc.example.com')
|
2018-09-02 15:07:14 +02:00
|
|
|
*
|
2018-10-23 03:41:38 +02:00
|
|
|
* @example
|
|
|
|
* // To return an array of rooms, provide an array of room JIDs:
|
2020-03-31 13:15:57 +02:00
|
|
|
* api.rooms.open(['group1@muc.example.com', 'group2@muc.example.com'])
|
2018-10-23 03:41:38 +02:00
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* // To setup a custom nickname when joining the room, provide the optional nick argument:
|
2020-03-31 13:15:57 +02:00
|
|
|
* api.rooms.open('group@muc.example.com', {'nick': 'mycustomnick'})
|
2018-10-23 03:41:38 +02:00
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* // For example, opening a room with a specific default configuration:
|
2020-03-31 13:15:57 +02:00
|
|
|
* api.rooms.open(
|
2018-10-23 03:41:38 +02:00
|
|
|
* 'myroom@conference.example.org',
|
|
|
|
* { 'nick': 'coolguy69',
|
|
|
|
* 'auto_configure': true,
|
|
|
|
* 'roomconfig': {
|
|
|
|
* 'changesubject': false,
|
|
|
|
* 'membersonly': true,
|
|
|
|
* 'persistentroom': true,
|
|
|
|
* 'publicroom': true,
|
|
|
|
* 'roomdesc': 'Comfy room for hanging out',
|
|
|
|
* 'whois': 'anyone'
|
|
|
|
* }
|
2019-05-03 11:38:05 +02:00
|
|
|
* }
|
2018-10-23 03:41:38 +02:00
|
|
|
* );
|
2018-09-02 15:07:14 +02:00
|
|
|
*/
|
2019-06-05 14:43:59 +02:00
|
|
|
async open (jids, attrs, force=false) {
|
2020-03-31 13:15:57 +02:00
|
|
|
await api.waitUntil('chatBoxesFetched');
|
2019-07-29 10:19:05 +02:00
|
|
|
if (jids === undefined) {
|
2018-10-23 03:41:38 +02:00
|
|
|
const err_msg = 'rooms.open: You need to provide at least one JID';
|
2019-11-06 11:01:34 +01:00
|
|
|
log.error(err_msg);
|
2018-10-23 03:41:38 +02:00
|
|
|
throw(new TypeError(err_msg));
|
2019-11-01 16:04:55 +01:00
|
|
|
} else if (isString(jids)) {
|
2020-03-31 13:15:57 +02:00
|
|
|
const room = await api.rooms.create(jids, attrs);
|
2019-10-04 18:26:33 +02:00
|
|
|
room && room.maybeShow(force);
|
2019-06-05 14:43:59 +02:00
|
|
|
return room;
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
2020-03-31 13:15:57 +02:00
|
|
|
const rooms = await Promise.all(jids.map(jid => api.rooms.create(jid, attrs)));
|
2019-10-24 14:29:15 +02:00
|
|
|
rooms.forEach(r => r.maybeShow(force));
|
|
|
|
return rooms;
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
},
|
2018-07-30 18:16:32 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
/**
|
2019-10-24 14:29:15 +02:00
|
|
|
* Fetches the object representing a MUC chatroom (aka groupchat)
|
2018-10-23 03:41:38 +02:00
|
|
|
*
|
2020-03-31 13:15:57 +02:00
|
|
|
* @method api.rooms.get
|
2018-10-23 03:41:38 +02:00
|
|
|
* @param {string} [jid] The room JID (if not specified, all rooms will be returned).
|
|
|
|
* @param {object} attrs A map containing any extra room attributes For example, if you want
|
|
|
|
* to specify the nickname, use `{'nick': 'bloodninja'}`. Previously (before
|
|
|
|
* version 1.0.7, the second parameter only accepted the nickname (as a string
|
|
|
|
* value). This is currently still accepted, but then you can't pass in any
|
|
|
|
* other room attributes. If the nickname is not specified then the node part of
|
|
|
|
* the user's JID will be used.
|
|
|
|
* @param {boolean} create A boolean indicating whether the room should be created
|
|
|
|
* if not found (default: `false`)
|
2019-11-01 16:04:55 +01:00
|
|
|
* @returns { Promise<_converse.ChatRoom> }
|
2018-10-23 03:41:38 +02:00
|
|
|
* @example
|
2020-03-31 13:15:57 +02:00
|
|
|
* api.waitUntil('roomsAutoJoined').then(() => {
|
2018-10-23 03:41:38 +02:00
|
|
|
* const create_if_not_found = true;
|
2020-03-31 13:15:57 +02:00
|
|
|
* api.rooms.get(
|
2018-10-23 03:41:38 +02:00
|
|
|
* 'group@muc.example.com',
|
|
|
|
* {'nick': 'dread-pirate-roberts'},
|
|
|
|
* create_if_not_found
|
|
|
|
* )
|
|
|
|
* });
|
|
|
|
*/
|
2019-11-01 16:04:55 +01:00
|
|
|
async get (jids, attrs={}, create=false) {
|
|
|
|
async function _get (jid) {
|
2020-03-31 13:15:57 +02:00
|
|
|
let model = await api.chatboxes.get(jid);
|
2019-11-01 16:04:55 +01:00
|
|
|
if (!model && create) {
|
2020-03-31 13:15:57 +02:00
|
|
|
model = await api.chatboxes.create(jid, attrs, _converse.ChatRoom);
|
2019-11-01 16:04:55 +01:00
|
|
|
} else {
|
|
|
|
model = (model && model.get('type') === _converse.CHATROOMS_TYPE) ? model : null;
|
|
|
|
if (model && Object.keys(attrs).length) {
|
|
|
|
model.save(attrs);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-11-01 16:04:55 +01:00
|
|
|
}
|
|
|
|
return model;
|
2017-11-10 15:53:59 +01:00
|
|
|
}
|
2019-11-01 16:04:55 +01:00
|
|
|
if (jids === undefined) {
|
2020-03-31 13:15:57 +02:00
|
|
|
const chats = await api.chatboxes.get();
|
2019-11-01 16:04:55 +01:00
|
|
|
return chats.filter(c => (c.get('type') === _converse.CHATROOMS_TYPE));
|
|
|
|
} else if (isString(jids)) {
|
|
|
|
return _get(jids);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-11-01 16:04:55 +01:00
|
|
|
return Promise.all(jids.map(jid => _get(jid)));
|
2017-11-10 15:53:59 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
/************************ END API ************************/
|
|
|
|
}
|
|
|
|
});
|
2020-03-02 16:59:18 +01:00
|
|
|
|
|
|
|
|