Move converse-muc into a folder
This commit is contained in:
parent
e8536ebc88
commit
c0fafcec70
@ -7,12 +7,12 @@ import "./plugins/bookmarks.js"; // XEP-0199 XMPP Ping
|
||||
import "./plugins/bosh.js"; // XEP-0206 BOSH
|
||||
import "./plugins/caps.js"; // XEP-0115 Entity Capabilities
|
||||
import "./plugins/carbons.js"; // XEP-0280 Message Carbons
|
||||
import "./plugins/chat/index.js"; // RFC-6121 Instant messaging
|
||||
import "./plugins/chat/index.js"; // RFC-6121 Instant messaging
|
||||
import "./plugins/chatboxes.js";
|
||||
import "./plugins/disco.js"; // XEP-0030 Service discovery
|
||||
import "./plugins/headlines.js"; // Support for headline messages
|
||||
import "./plugins/mam.js"; // XEP-0313 Message Archive Management
|
||||
import "./plugins/muc.js"; // XEP-0045 Multi-user chat
|
||||
import "./plugins/muc/index.js"; // XEP-0045 Multi-user chat
|
||||
import "./plugins/ping.js"; // XEP-0199 XMPP Ping
|
||||
import "./plugins/pubsub.js"; // XEP-0060 Pubsub
|
||||
import "./plugins/roster.js"; // RFC-6121 Contacts Roster
|
||||
|
@ -5,7 +5,7 @@
|
||||
* @copyright 2020, the Converse.js contributors
|
||||
* @license Mozilla Public License (MPLv2)
|
||||
*/
|
||||
import "@converse/headless/plugins/muc";
|
||||
import "@converse/headless/plugins/muc/index.js";
|
||||
import log from "../log.js";
|
||||
import { Collection } from "@converse/skeletor/src/collection";
|
||||
import { Model } from '@converse/skeletor/src/model.js';
|
||||
|
File diff suppressed because it is too large
Load Diff
160
src/headless/plugins/muc/api.js
Normal file
160
src/headless/plugins/muc/api.js
Normal file
@ -0,0 +1,160 @@
|
||||
import log from '../../log';
|
||||
import u from '../../utils/form';
|
||||
import { Strophe } from 'strophe.js/src/strophe';
|
||||
import { _converse, api } from '../../core.js';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* The "rooms" namespace groups methods relevant to chatrooms
|
||||
* (aka groupchats).
|
||||
*
|
||||
* @namespace api.rooms
|
||||
* @memberOf api
|
||||
*/
|
||||
rooms: {
|
||||
/**
|
||||
* Creates a new MUC chatroom (aka groupchat)
|
||||
*
|
||||
* Similar to {@link api.rooms.open}, but creates
|
||||
* the chatroom in the background (i.e. doesn't cause a view to open).
|
||||
*
|
||||
* @method api.rooms.create
|
||||
* @param {(string[]|string)} jid|jids The JID or array of
|
||||
* JIDs of the chatroom(s) to create
|
||||
* @param {object} [attrs] attrs The room attributes
|
||||
* @returns {Promise} Promise which resolves with the Model representing the chat.
|
||||
*/
|
||||
create (jids, attrs = {}) {
|
||||
attrs = typeof attrs === 'string' ? { 'nick': attrs } : attrs || {};
|
||||
if (!attrs.nick && api.settings.get('muc_nickname_from_jid')) {
|
||||
attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
|
||||
}
|
||||
if (jids === undefined) {
|
||||
throw new TypeError('rooms.create: You need to provide at least one JID');
|
||||
} else if (typeof jids === 'string') {
|
||||
return api.rooms.get(u.getJIDFromURI(jids), attrs, true);
|
||||
}
|
||||
return jids.map(jid => api.rooms.get(u.getJIDFromURI(jid), attrs, true));
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a MUC chatroom (aka groupchat)
|
||||
*
|
||||
* Similar to {@link api.chats.open}, but for groupchats.
|
||||
*
|
||||
* @method api.rooms.open
|
||||
* @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
|
||||
* in the [XEP-0045 MUC specification](https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner).
|
||||
* The values should be named without the `muc#roomconfig_` prefix.
|
||||
* @param {boolean} [attrs.minimized] A boolean, indicating whether the room should be opened minimized or not.
|
||||
* @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.
|
||||
* @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.
|
||||
* @returns {Promise} Promise which resolves with the Model representing the chat.
|
||||
*
|
||||
* @example
|
||||
* this.api.rooms.open('group@muc.example.com')
|
||||
*
|
||||
* @example
|
||||
* // To return an array of rooms, provide an array of room JIDs:
|
||||
* api.rooms.open(['group1@muc.example.com', 'group2@muc.example.com'])
|
||||
*
|
||||
* @example
|
||||
* // To setup a custom nickname when joining the room, provide the optional nick argument:
|
||||
* api.rooms.open('group@muc.example.com', {'nick': 'mycustomnick'})
|
||||
*
|
||||
* @example
|
||||
* // For example, opening a room with a specific default configuration:
|
||||
* api.rooms.open(
|
||||
* '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'
|
||||
* }
|
||||
* }
|
||||
* );
|
||||
*/
|
||||
async open (jids, attrs = {}, force = false) {
|
||||
await api.waitUntil('chatBoxesFetched');
|
||||
if (jids === undefined) {
|
||||
const err_msg = 'rooms.open: You need to provide at least one JID';
|
||||
log.error(err_msg);
|
||||
throw new TypeError(err_msg);
|
||||
} else if (typeof jids === 'string') {
|
||||
const room = await api.rooms.get(jids, attrs, true);
|
||||
room && room.maybeShow(force);
|
||||
return room;
|
||||
} else {
|
||||
const rooms = await Promise.all(jids.map(jid => api.rooms.get(jid, attrs, true)));
|
||||
rooms.forEach(r => r.maybeShow(force));
|
||||
return rooms;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the object representing a MUC chatroom (aka groupchat)
|
||||
*
|
||||
* @method api.rooms.get
|
||||
* @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 a nickname and password, use `{'nick': 'bloodninja', 'password': 'secret'}`.
|
||||
* @param {boolean} create A boolean indicating whether the room should be created
|
||||
* if not found (default: `false`)
|
||||
* @returns { Promise<_converse.ChatRoom> }
|
||||
* @example
|
||||
* api.waitUntil('roomsAutoJoined').then(() => {
|
||||
* const create_if_not_found = true;
|
||||
* api.rooms.get(
|
||||
* 'group@muc.example.com',
|
||||
* {'nick': 'dread-pirate-roberts'},
|
||||
* create_if_not_found
|
||||
* )
|
||||
* });
|
||||
*/
|
||||
async get (jids, attrs = {}, create = false) {
|
||||
async function _get (jid) {
|
||||
jid = u.getJIDFromURI(jid);
|
||||
let model = await api.chatboxes.get(jid);
|
||||
if (!model && create) {
|
||||
model = await api.chatboxes.create(jid, attrs, _converse.ChatRoom);
|
||||
} else {
|
||||
model = model && model.get('type') === _converse.CHATROOMS_TYPE ? model : null;
|
||||
if (model && Object.keys(attrs).length) {
|
||||
model.save(attrs);
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
if (jids === undefined) {
|
||||
const chats = await api.chatboxes.get();
|
||||
return chats.filter(c => c.get('type') === _converse.CHATROOMS_TYPE);
|
||||
} else if (typeof jids === 'string') {
|
||||
return _get(jids);
|
||||
}
|
||||
return Promise.all(jids.map(jid => _get(jid)));
|
||||
}
|
||||
}
|
||||
}
|
488
src/headless/plugins/muc/index.js
Normal file
488
src/headless/plugins/muc/index.js
Normal file
@ -0,0 +1,488 @@
|
||||
/**
|
||||
* @module converse-muc
|
||||
* @copyright The Converse.js contributors
|
||||
* @license Mozilla Public License (MPLv2)
|
||||
* @description Implements the non-view logic for XEP-0045 Multi-User Chat
|
||||
*/
|
||||
import '../chat/index.js';
|
||||
import '../disco';
|
||||
import '../emoji/index.js';
|
||||
import ChatRoomMessageMixin from './message.js';
|
||||
import ChatRoomMixin from './muc.js';
|
||||
import ChatRoomOccupant from './occupant.js';
|
||||
import ChatRoomOccupants from './occupants.js';
|
||||
import log from '../../log';
|
||||
import muc_api from './api.js';
|
||||
import muc_utils from '../../utils/muc';
|
||||
import u from '../../utils/form';
|
||||
import { Collection } from '@converse/skeletor/src/collection';
|
||||
import { Model } from '@converse/skeletor/src/model.js';
|
||||
import { _converse, api, converse } from '../../core.js';
|
||||
import { isObject } from 'lodash-es';
|
||||
|
||||
export const ROLES = ['moderator', 'participant', 'visitor'];
|
||||
export const AFFILIATIONS = ['owner', 'admin', 'member', 'outcast', 'none'];
|
||||
|
||||
converse.AFFILIATION_CHANGES = {
|
||||
OWNER: 'owner',
|
||||
ADMIN: 'admin',
|
||||
MEMBER: 'member',
|
||||
EXADMIN: 'exadmin',
|
||||
EXOWNER: 'exowner',
|
||||
EXOUTCAST: 'exoutcast',
|
||||
EXMEMBER: 'exmember'
|
||||
};
|
||||
converse.AFFILIATION_CHANGES_LIST = Object.values(converse.AFFILIATION_CHANGES);
|
||||
converse.MUC_TRAFFIC_STATES = { ENTERED: 'entered', EXITED: 'exited' };
|
||||
converse.MUC_TRAFFIC_STATES_LIST = Object.values(converse.MUC_TRAFFIC_STATES);
|
||||
converse.MUC_ROLE_CHANGES = { OP: 'op', DEOP: 'deop', VOICE: 'voice', MUTE: 'mute' };
|
||||
converse.MUC_ROLE_CHANGES_LIST = Object.values(converse.MUC_ROLE_CHANGES);
|
||||
|
||||
converse.MUC_INFO_CODES = {
|
||||
'visibility_changes': ['100', '102', '103', '172', '173', '174'],
|
||||
'self': ['110'],
|
||||
'non_privacy_changes': ['104', '201'],
|
||||
'muc_logging_changes': ['170', '171'],
|
||||
'nickname_changes': ['210', '303'],
|
||||
'disconnect_messages': ['301', '307', '321', '322', '332', '333'],
|
||||
'affiliation_changes': [...converse.AFFILIATION_CHANGES_LIST],
|
||||
'join_leave_events': [...converse.MUC_TRAFFIC_STATES_LIST],
|
||||
'role_changes': [...converse.MUC_ROLE_CHANGES_LIST]
|
||||
};
|
||||
|
||||
const { Strophe, sizzle } = converse.env;
|
||||
|
||||
// Add Strophe Namespaces
|
||||
Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + '#admin');
|
||||
Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + '#owner');
|
||||
Strophe.addNamespace('MUC_REGISTER', 'jabber:iq:register');
|
||||
Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + '#roomconfig');
|
||||
Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + '#user');
|
||||
Strophe.addNamespace('MUC_HATS', 'xmpp:prosody.im/protocol/hats:1');
|
||||
|
||||
converse.MUC_NICK_CHANGED_CODE = '303';
|
||||
|
||||
converse.ROOM_FEATURES = [
|
||||
'passwordprotected',
|
||||
'unsecured',
|
||||
'hidden',
|
||||
'publicroom',
|
||||
'membersonly',
|
||||
'open',
|
||||
'persistent',
|
||||
'temporary',
|
||||
'nonanonymous',
|
||||
'semianonymous',
|
||||
'moderated',
|
||||
'unmoderated',
|
||||
'mam_enabled'
|
||||
];
|
||||
|
||||
// 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'
|
||||
// };
|
||||
|
||||
converse.ROOMSTATUS = {
|
||||
CONNECTED: 0,
|
||||
CONNECTING: 1,
|
||||
NICKNAME_REQUIRED: 2,
|
||||
PASSWORD_REQUIRED: 3,
|
||||
DISCONNECTED: 4,
|
||||
ENTERED: 5,
|
||||
DESTROYED: 6
|
||||
};
|
||||
|
||||
|
||||
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)
|
||||
.forEach(m => m.session.save({ 'connection_status': converse.ROOMSTATUS.DISCONNECTED }));
|
||||
}
|
||||
|
||||
async function onWindowStateChanged (data) {
|
||||
if (data.state === 'visible' && api.connection.connected()) {
|
||||
const rooms = await api.rooms.get();
|
||||
rooms.forEach(room => room.rejoinIfNecessary());
|
||||
}
|
||||
}
|
||||
|
||||
async function routeToRoom (jid) {
|
||||
if (!u.isValidMUCJID(jid)) {
|
||||
return log.warn(`invalid jid "${jid}" provided in url fragment`);
|
||||
}
|
||||
await api.waitUntil('roomsAutoJoined');
|
||||
if (api.settings.get('allow_bookmarks')) {
|
||||
await api.waitUntil('bookmarksInitialized');
|
||||
}
|
||||
api.rooms.open(jid);
|
||||
}
|
||||
|
||||
/* Opens a groupchat, making sure that certain attributes
|
||||
* are correct, for example that the "type" is set to
|
||||
* "chatroom".
|
||||
*/
|
||||
async function openChatRoom (jid, settings) {
|
||||
settings.type = _converse.CHATROOMS_TYPE;
|
||||
settings.id = jid;
|
||||
const chatbox = await api.rooms.get(jid, settings, true);
|
||||
chatbox.maybeShow(true);
|
||||
return chatbox;
|
||||
}
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
async function autoJoinRooms () {
|
||||
await Promise.all(
|
||||
api.settings.get('auto_join_rooms').map(muc => {
|
||||
if (typeof muc === 'string') {
|
||||
if (_converse.chatboxes.where({ 'jid': muc }).length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return api.rooms.open(muc);
|
||||
} else if (isObject(muc)) {
|
||||
return api.rooms.open(muc.jid, { ...muc });
|
||||
} else {
|
||||
log.error('Invalid muc criteria specified for "auto_join_rooms"');
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
/**
|
||||
* 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(() => { ... });
|
||||
*/
|
||||
api.trigger('roomsAutoJoined');
|
||||
}
|
||||
|
||||
|
||||
converse.plugins.add('converse-muc', {
|
||||
/* Optional dependencies are other plugins which might be
|
||||
* overridden or relied upon, and therefore need to be loaded before
|
||||
* this plugin. They are called "optional" because they might not be
|
||||
* available, in which case any overrides applicable to them will be
|
||||
* ignored.
|
||||
*
|
||||
* It's possible however to make optional dependencies non-optional.
|
||||
* If the setting "strict_plugin_dependencies" is set to true,
|
||||
* an error will be raised if the plugin is not found.
|
||||
*
|
||||
* NB: These plugins need to have already been loaded via require.js.
|
||||
*/
|
||||
dependencies: ['converse-chatboxes', 'converse-chat', 'converse-disco', 'converse-controlbox'],
|
||||
|
||||
overrides: {
|
||||
ChatBoxes: {
|
||||
model (attrs, options) {
|
||||
const { _converse } = this.__super__;
|
||||
if (attrs && attrs.type == _converse.CHATROOMS_TYPE) {
|
||||
return new _converse.ChatRoom(attrs, options);
|
||||
} else {
|
||||
return this.__super__.model.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
initialize () {
|
||||
/* The initialize function gets called as soon as the plugin is
|
||||
* loaded by converse.js's plugin machinery.
|
||||
*/
|
||||
const { __, ___ } = _converse;
|
||||
|
||||
// Configuration values for this plugin
|
||||
// ====================================
|
||||
// Refer to docs/source/configuration.rst for explanations of these
|
||||
// configuration settings.
|
||||
api.settings.extend({
|
||||
'allow_muc': true,
|
||||
'allow_muc_invitations': true,
|
||||
'auto_join_on_invite': false,
|
||||
'auto_join_rooms': [],
|
||||
'auto_register_muc_nickname': false,
|
||||
'hide_muc_participants': false,
|
||||
'locked_muc_domain': false,
|
||||
'muc_domain': undefined,
|
||||
'muc_fetch_members': true,
|
||||
'muc_history_max_stanzas': undefined,
|
||||
'muc_instant_rooms': true,
|
||||
'muc_nickname_from_jid': false,
|
||||
'muc_send_probes': false,
|
||||
'muc_show_info_messages': [
|
||||
...converse.MUC_INFO_CODES.visibility_changes,
|
||||
...converse.MUC_INFO_CODES.self,
|
||||
...converse.MUC_INFO_CODES.non_privacy_changes,
|
||||
...converse.MUC_INFO_CODES.muc_logging_changes,
|
||||
...converse.MUC_INFO_CODES.nickname_changes,
|
||||
...converse.MUC_INFO_CODES.disconnect_messages,
|
||||
...converse.MUC_INFO_CODES.affiliation_changes,
|
||||
...converse.MUC_INFO_CODES.join_leave_events,
|
||||
...converse.MUC_INFO_CODES.role_changes
|
||||
],
|
||||
'muc_show_logs_before_join': false
|
||||
});
|
||||
api.promises.add(['roomsAutoJoined']);
|
||||
|
||||
if (api.settings.get('locked_muc_domain') && typeof api.settings.get('muc_domain') !== 'string') {
|
||||
throw new Error(
|
||||
'Config Error: it makes no sense to set locked_muc_domain ' + 'to true when muc_domain is not set'
|
||||
);
|
||||
}
|
||||
|
||||
converse.env.muc_utils = muc_utils;
|
||||
Object.assign(api, muc_api);
|
||||
|
||||
/* 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'),
|
||||
170: __('Groupchat logging is now enabled'),
|
||||
171: __('Groupchat logging is now disabled'),
|
||||
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'),
|
||||
333: __('You have exited this groupchat due to a technical problem'),
|
||||
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')
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines info message visibility based on
|
||||
* muc_show_info_messages configuration setting
|
||||
* @param {*} code
|
||||
* @memberOf _converse
|
||||
*/
|
||||
_converse.isInfoVisible = function (code) {
|
||||
const info_messages = api.settings.get('muc_show_info_messages');
|
||||
if (info_messages.includes(code)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
_converse.router.route('converse/room?jid=:jid', routeToRoom);
|
||||
|
||||
_converse.ChatRoom = _converse.ChatBox.extend(ChatRoomMixin);
|
||||
_converse.ChatRoomMessage = _converse.Message.extend(ChatRoomMessageMixin);
|
||||
_converse.ChatRoomOccupants = ChatRoomOccupants;
|
||||
_converse.ChatRoomOccupant = ChatRoomOccupant;
|
||||
|
||||
_converse.getDefaultMUCNickname = function () {
|
||||
// XXX: if anything changes here, update the docs for the
|
||||
// locked_muc_nickname setting.
|
||||
if (!_converse.xmppstatus) {
|
||||
throw new Error(
|
||||
"Can't call _converse.getDefaultMUCNickname before the statusInitialized has been fired."
|
||||
);
|
||||
}
|
||||
const nick = _converse.xmppstatus.getNickname();
|
||||
if (nick) {
|
||||
return nick;
|
||||
} else if (api.settings.get('muc_nickname_from_jid')) {
|
||||
return Strophe.unescapeNode(Strophe.getNodeFromJid(_converse.bare_jid));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Collection which stores MUC messages
|
||||
* @class
|
||||
* @namespace _converse.ChatRoomMessages
|
||||
* @memberOf _converse
|
||||
*/
|
||||
_converse.ChatRoomMessages = Collection.extend({
|
||||
model: _converse.ChatRoomMessage,
|
||||
comparator: 'time'
|
||||
});
|
||||
|
||||
|
||||
_converse.RoomsPanelModel = Model.extend({
|
||||
defaults: function () {
|
||||
return {
|
||||
'muc_domain': api.settings.get('muc_domain'),
|
||||
'nick': _converse.getDefaultMUCNickname()
|
||||
};
|
||||
},
|
||||
|
||||
setDomain (jid) {
|
||||
if (!api.settings.get('locked_muc_domain')) {
|
||||
this.save('muc_domain', Strophe.getDomainFromJid(jid));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
_converse.onDirectMUCInvitation = async function (message) {
|
||||
const x_el = sizzle('x[xmlns="jabber:x:conference"]', message).pop(),
|
||||
from = Strophe.getBareJidFromJid(message.getAttribute('from')),
|
||||
room_jid = x_el.getAttribute('jid'),
|
||||
reason = x_el.getAttribute('reason');
|
||||
|
||||
let result;
|
||||
if (api.settings.get('auto_join_on_invite')) {
|
||||
result = true;
|
||||
} else {
|
||||
// Invite request might come from someone not your roster list
|
||||
let contact = _converse.roster.get(from);
|
||||
contact = contact ? contact.getDisplayName() : from;
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (result === true) {
|
||||
const chatroom = await openChatRoom(room_jid, { 'password': x_el.getAttribute('password') });
|
||||
if (chatroom.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
|
||||
_converse.chatboxes.get(room_jid).rejoin();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (api.settings.get('allow_muc_invitations')) {
|
||||
const registerDirectInvitationHandler = function () {
|
||||
_converse.connection.addHandler(
|
||||
message => {
|
||||
_converse.onDirectMUCInvitation(message);
|
||||
return true;
|
||||
},
|
||||
'jabber:x:conference',
|
||||
'message'
|
||||
);
|
||||
};
|
||||
api.listen.on('connected', registerDirectInvitationHandler);
|
||||
api.listen.on('reconnected', registerDirectInvitationHandler);
|
||||
}
|
||||
|
||||
/************************ BEGIN Event Handlers ************************/
|
||||
api.listen.on('beforeTearDown', () => {
|
||||
const groupchats = _converse.chatboxes.where({ 'type': _converse.CHATROOMS_TYPE });
|
||||
groupchats.forEach(muc =>
|
||||
u.safeSave(muc.session, { 'connection_status': converse.ROOMSTATUS.DISCONNECTED })
|
||||
);
|
||||
});
|
||||
|
||||
api.listen.on('windowStateChanged', onWindowStateChanged);
|
||||
|
||||
api.listen.on('addClientFeatures', () => {
|
||||
if (api.settings.get('allow_muc')) {
|
||||
api.disco.own.features.add(Strophe.NS.MUC);
|
||||
}
|
||||
if (api.settings.get('allow_muc_invitations')) {
|
||||
api.disco.own.features.add('jabber:x:conference'); // Invites
|
||||
}
|
||||
});
|
||||
api.listen.on('chatBoxesFetched', autoJoinRooms);
|
||||
|
||||
api.listen.on('beforeResourceBinding', () => {
|
||||
_converse.connection.addHandler(
|
||||
stanza => {
|
||||
const muc_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
|
||||
if (!_converse.chatboxes.get(muc_jid)) {
|
||||
api.waitUntil('chatBoxesFetched').then(async () => {
|
||||
const muc = _converse.chatboxes.get(muc_jid);
|
||||
if (muc) {
|
||||
await muc.initialized;
|
||||
muc.message_handler.run(stanza);
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
null,
|
||||
'message',
|
||||
'groupchat'
|
||||
);
|
||||
});
|
||||
|
||||
api.listen.on('disconnected', disconnectChatRooms);
|
||||
|
||||
api.listen.on('statusInitialized', () => {
|
||||
window.addEventListener(_converse.unloadevent, () => {
|
||||
const using_websocket = api.connection.isType('websocket');
|
||||
if (
|
||||
using_websocket &&
|
||||
(!api.settings.get('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.
|
||||
// See issue #1111
|
||||
disconnectChatRooms();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
100
src/headless/plugins/muc/message.js
Normal file
100
src/headless/plugins/muc/message.js
Normal file
@ -0,0 +1,100 @@
|
||||
import log from '../../log';
|
||||
import { Strophe } from 'strophe.js/src/strophe';
|
||||
import { _converse, api } from '../../core.js';
|
||||
|
||||
/**
|
||||
* Mixing that turns a Message model into a ChatRoomMessage model.
|
||||
* @class
|
||||
* @namespace _converse.ChatRoomMessage
|
||||
* @memberOf _converse
|
||||
*/
|
||||
const ChatRoomMessageMixin = {
|
||||
initialize () {
|
||||
if (!this.checkValidity()) {
|
||||
return;
|
||||
}
|
||||
if (this.get('file')) {
|
||||
this.on('change:put', this.uploadFile, this);
|
||||
}
|
||||
if (!this.setTimerForEphemeralMessage()) {
|
||||
this.setOccupant();
|
||||
}
|
||||
/**
|
||||
* Triggered once a {@link _converse.ChatRoomMessageInitialized} has been created and initialized.
|
||||
* @event _converse#chatRoomMessageInitialized
|
||||
* @type { _converse.ChatRoomMessages}
|
||||
* @example _converse.api.listen.on('chatRoomMessageInitialized', model => { ... });
|
||||
*/
|
||||
api.trigger('chatRoomMessageInitialized', this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines whether this messsage may be moderated,
|
||||
* based on configuration settings and server support.
|
||||
* @async
|
||||
* @private
|
||||
* @method _converse.ChatRoomMessages#mayBeModerated
|
||||
* @returns { Boolean }
|
||||
*/
|
||||
mayBeModerated () {
|
||||
return (
|
||||
['all', 'moderator'].includes(api.settings.get('allow_message_retraction')) &&
|
||||
this.collection.chatbox.canModerateMessages()
|
||||
);
|
||||
},
|
||||
|
||||
checkValidity () {
|
||||
const result = _converse.Message.prototype.checkValidity.call(this);
|
||||
!result && this.collection.chatbox.debouncedRejoin();
|
||||
return result;
|
||||
},
|
||||
|
||||
onOccupantRemoved () {
|
||||
this.stopListening(this.occupant);
|
||||
delete this.occupant;
|
||||
const chatbox = this?.collection?.chatbox;
|
||||
if (!chatbox) {
|
||||
return log.error(`Could not get collection.chatbox for message: ${JSON.stringify(this.toJSON())}`);
|
||||
}
|
||||
this.listenTo(chatbox.occupants, 'add', this.onOccupantAdded);
|
||||
},
|
||||
|
||||
onOccupantAdded (occupant) {
|
||||
if (occupant.get('nick') === Strophe.getResourceFromJid(this.get('from'))) {
|
||||
this.occupant = occupant;
|
||||
this.trigger('occupantAdded');
|
||||
this.listenTo(this.occupant, 'destroy', this.onOccupantRemoved);
|
||||
const chatbox = this?.collection?.chatbox;
|
||||
if (!chatbox) {
|
||||
return log.error(`Could not get collection.chatbox for message: ${JSON.stringify(this.toJSON())}`);
|
||||
}
|
||||
this.stopListening(chatbox.occupants, 'add', this.onOccupantAdded);
|
||||
}
|
||||
},
|
||||
|
||||
setOccupant () {
|
||||
if (this.get('type') !== 'groupchat') {
|
||||
return;
|
||||
}
|
||||
const chatbox = this?.collection?.chatbox;
|
||||
if (!chatbox) {
|
||||
return log.error(`Could not get collection.chatbox for message: ${JSON.stringify(this.toJSON())}`);
|
||||
}
|
||||
const nick = Strophe.getResourceFromJid(this.get('from'));
|
||||
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);
|
||||
}
|
||||
|
||||
if (this.occupant) {
|
||||
this.listenTo(this.occupant, 'destroy', this.onOccupantRemoved);
|
||||
} else {
|
||||
this.listenTo(chatbox.occupants, 'add', this.onOccupantAdded);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default ChatRoomMessageMixin;
|
2223
src/headless/plugins/muc/muc.js
Normal file
2223
src/headless/plugins/muc/muc.js
Normal file
File diff suppressed because it is too large
Load Diff
57
src/headless/plugins/muc/occupant.js
Normal file
57
src/headless/plugins/muc/occupant.js
Normal file
@ -0,0 +1,57 @@
|
||||
import u from '../../utils/form';
|
||||
import { Model } from '@converse/skeletor/src/model.js';
|
||||
import { _converse, api } from '../../core.js';
|
||||
|
||||
/**
|
||||
* Represents a participant in a MUC
|
||||
* @class
|
||||
* @namespace _converse.ChatRoomOccupant
|
||||
* @memberOf _converse
|
||||
*/
|
||||
const ChatRoomOccupant = Model.extend({
|
||||
defaults: {
|
||||
'hats': [],
|
||||
'show': 'offline',
|
||||
'states': []
|
||||
},
|
||||
|
||||
initialize (attributes) {
|
||||
this.set(Object.assign({ 'id': u.getUniqueId() }, attributes));
|
||||
this.on('change:image_hash', this.onAvatarChanged, this);
|
||||
},
|
||||
|
||||
onAvatarChanged () {
|
||||
const hash = this.get('image_hash');
|
||||
const vcards = [];
|
||||
if (this.get('jid')) {
|
||||
vcards.push(_converse.vcards.findWhere({ 'jid': this.get('jid') }));
|
||||
}
|
||||
vcards.push(_converse.vcards.findWhere({ 'jid': this.get('from') }));
|
||||
|
||||
vcards
|
||||
.filter(v => v)
|
||||
.forEach(vcard => {
|
||||
if (hash && vcard.get('image_hash') !== hash) {
|
||||
api.vcard.update(vcard, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getDisplayName () {
|
||||
return this.get('nick') || this.get('jid');
|
||||
},
|
||||
|
||||
isMember () {
|
||||
return ['admin', 'owner', 'member'].includes(this.get('affiliation'));
|
||||
},
|
||||
|
||||
isModerator () {
|
||||
return ['admin', 'owner'].includes(this.get('affiliation')) || this.get('role') === 'moderator';
|
||||
},
|
||||
|
||||
isSelf () {
|
||||
return this.get('states').includes('110');
|
||||
}
|
||||
});
|
||||
|
||||
export default ChatRoomOccupant;
|
112
src/headless/plugins/muc/occupants.js
Normal file
112
src/headless/plugins/muc/occupants.js
Normal file
@ -0,0 +1,112 @@
|
||||
import ChatRoomOccupant from './occupant.js';
|
||||
import u from '../../utils/form';
|
||||
import { Collection } from '@converse/skeletor/src/collection';
|
||||
import { Strophe } from 'strophe.js/src/strophe';
|
||||
import { _converse, api } from '../../core.js';
|
||||
|
||||
const MUC_ROLE_WEIGHTS = {
|
||||
'moderator': 1,
|
||||
'participant': 2,
|
||||
'visitor': 3,
|
||||
'none': 2
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A list of {@link _converse.ChatRoomOccupant} instances, representing participants in a MUC.
|
||||
* @class
|
||||
* @namespace _converse.ChatRoomOccupants
|
||||
* @memberOf _converse
|
||||
*/
|
||||
const ChatRoomOccupants = Collection.extend({
|
||||
model: 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;
|
||||
}
|
||||
},
|
||||
|
||||
getAutoFetchedAffiliationLists () {
|
||||
const affs = api.settings.get('muc_fetch_members');
|
||||
return Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : [];
|
||||
},
|
||||
|
||||
async fetchMembers () {
|
||||
const affiliations = this.getAutoFetchedAffiliationLists();
|
||||
if (affiliations.length === 0) {
|
||||
return;
|
||||
}
|
||||
const aff_lists = await Promise.all(affiliations.map(a => this.chatroom.getAffiliationList(a)));
|
||||
const new_members = aff_lists.reduce((acc, val) => (u.isErrorObject(val) ? acc : [...val, ...acc]), []);
|
||||
const known_affiliations = affiliations.filter(
|
||||
a => !u.isErrorObject(aff_lists[affiliations.indexOf(a)])
|
||||
);
|
||||
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);
|
||||
const removed_members = this.filter(m => {
|
||||
return (
|
||||
known_affiliations.includes(m.get('affiliation')) &&
|
||||
!new_nicks.includes(m.get('nick')) &&
|
||||
!new_jids.includes(m.get('jid'))
|
||||
);
|
||||
});
|
||||
|
||||
removed_members.forEach(occupant => {
|
||||
if (occupant.get('jid') === _converse.bare_jid) {
|
||||
return;
|
||||
}
|
||||
if (occupant.get('show') === 'offline') {
|
||||
occupant.destroy();
|
||||
} else {
|
||||
occupant.save('affiliation', null);
|
||||
}
|
||||
});
|
||||
new_members.forEach(attrs => {
|
||||
const occupant = attrs.jid
|
||||
? this.findOccupant({ 'jid': attrs.jid })
|
||||
: this.findOccupant({ 'nick': attrs.nick });
|
||||
if (occupant) {
|
||||
occupant.save(attrs);
|
||||
} else {
|
||||
this.create(attrs);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Triggered once the member lists for this MUC have been fetched and processed.
|
||||
* @event _converse#membersFetched
|
||||
* @example _converse.api.listen.on('membersFetched', () => { ... });
|
||||
*/
|
||||
api.trigger('membersFetched');
|
||||
},
|
||||
|
||||
/**
|
||||
* @typedef { Object} OccupantData
|
||||
* @property { String } [jid]
|
||||
* @property { String } [nick]
|
||||
*/
|
||||
/**
|
||||
* 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 { OccupantData } data
|
||||
*/
|
||||
findOccupant (data) {
|
||||
const jid = Strophe.getBareJidFromJid(data.jid);
|
||||
return (jid && this.findWhere({ jid })) || this.findWhere({ 'nick': data.nick });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default ChatRoomOccupants;
|
@ -2,7 +2,7 @@ import BootstrapModal from "./base.js";
|
||||
import log from "@converse/headless/log";
|
||||
import sizzle from "sizzle";
|
||||
import tpl_moderator_tools_modal from "./templates/moderator-tools.js";
|
||||
import { AFFILIATIONS, ROLES } from "@converse/headless/plugins/muc.js";
|
||||
import { AFFILIATIONS, ROLES } from "@converse/headless/plugins/muc/index.js";
|
||||
import { __ } from '../i18n';
|
||||
import { api, converse } from "@converse/headless/core";
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
* @copyright 2020, the Converse.js contributors
|
||||
* @license Mozilla Public License (MPLv2)
|
||||
*/
|
||||
import "@converse/headless/plugins/muc";
|
||||
import "@converse/headless/plugins/muc/index.js";
|
||||
import { _converse, api, converse } from "@converse/headless/core";
|
||||
import tpl_bookmarks_list from "../templates/bookmarks_list.js"
|
||||
import tpl_muc_bookmark_form from "../templates/muc_bookmark_form.js";
|
||||
|
@ -6,7 +6,7 @@
|
||||
import "./chatview/index.js";
|
||||
import "./controlbox/index.js";
|
||||
import "./singleton.js";
|
||||
import "@converse/headless/plugins/muc";
|
||||
import "@converse/headless/plugins/muc/index.js";
|
||||
import { api, converse } from "@converse/headless/core";
|
||||
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
* @copyright 2020, the Converse.js contributors
|
||||
* @license Mozilla Public License (MPLv2)
|
||||
*/
|
||||
import "@converse/headless/plugins/muc";
|
||||
import "@converse/headless/plugins/muc/index.js";
|
||||
import RoomDetailsModal from 'modals/muc-details.js';
|
||||
import { _converse, api, converse } from "@converse/headless/core";
|
||||
import tpl_rooms_list from "../templates/rooms_list.js";
|
||||
|
Loading…
Reference in New Issue
Block a user