You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. import { getLogger } from '@jitsi/logger';
  2. import { $msg, Strophe } from 'strophe.js';
  3. import { XMPPEvents } from '../../service/xmpp/XMPPEvents';
  4. const logger = getLogger('modules/xmpp/Lobby');
  5. /**
  6. * The command type for updating a lobby participant's e-mail address.
  7. *
  8. * @type {string}
  9. */
  10. const EMAIL_COMMAND = 'email';
  11. /**
  12. * The Lobby room implementation. Setting a room to members only, joining the lobby room
  13. * approving or denying access to participants from the lobby room.
  14. */
  15. export default class Lobby {
  16. /**
  17. * Constructs lobby room.
  18. *
  19. * @param {ChatRoom} room the main room.
  20. */
  21. constructor(room) {
  22. this.xmpp = room.xmpp;
  23. this.mainRoom = room;
  24. const maybeJoinLobbyRoom = this._maybeJoinLobbyRoom.bind(this);
  25. this.mainRoom.addEventListener(
  26. XMPPEvents.LOCAL_ROLE_CHANGED,
  27. maybeJoinLobbyRoom);
  28. this.mainRoom.addEventListener(
  29. XMPPEvents.MUC_MEMBERS_ONLY_CHANGED,
  30. maybeJoinLobbyRoom);
  31. this.mainRoom.addEventListener(
  32. XMPPEvents.ROOM_CONNECT_MEMBERS_ONLY_ERROR,
  33. jid => {
  34. this.lobbyRoomJid = jid;
  35. });
  36. }
  37. /**
  38. * Whether lobby is supported on backend.
  39. *
  40. * @returns {boolean} whether lobby is supported on backend.
  41. */
  42. isSupported() {
  43. return this.xmpp.lobbySupported;
  44. }
  45. /**
  46. * Enables lobby by setting the main room to be members only and joins the lobby chat room.
  47. *
  48. * @returns {Promise}
  49. */
  50. enable() {
  51. if (!this.isSupported()) {
  52. return Promise.reject(new Error('Lobby not supported!'));
  53. }
  54. // let's wait for the room data form to be populated after XMPPEvents.MUC_JOINED
  55. if (!this.mainRoom.initialDiscoRoomInfoReceived) {
  56. return new Promise((resolve, reject) => {
  57. let unsubscribers = [];
  58. const unsubscribe = () => {
  59. unsubscribers.forEach(remove => remove());
  60. unsubscribers = [];
  61. };
  62. unsubscribers.push(
  63. this.mainRoom.addCancellableListener(XMPPEvents.ROOM_DISCO_INFO_UPDATED, () => {
  64. unsubscribe();
  65. if (this.mainRoom.membersOnlyEnabled) {
  66. resolve();
  67. return;
  68. }
  69. this.mainRoom.setMembersOnly(true, resolve, reject);
  70. }));
  71. // on timeout or failure
  72. unsubscribers.push(this.mainRoom.addCancellableListener(XMPPEvents.ROOM_DISCO_INFO_FAILED, e => {
  73. unsubscribe();
  74. reject(e);
  75. }));
  76. });
  77. }
  78. if (this.mainRoom.membersOnlyEnabled) {
  79. return Promise.resolve();
  80. }
  81. return new Promise((resolve, reject) => {
  82. this.mainRoom.setMembersOnly(true, resolve, reject);
  83. });
  84. }
  85. /**
  86. * Disable lobby by setting the main room to be non members only and levaes the lobby chat room if joined.
  87. *
  88. * @returns {void}
  89. */
  90. disable() {
  91. if (!this.isSupported() || !this.mainRoom.isModerator()
  92. || !this.lobbyRoom || !this.mainRoom.membersOnlyEnabled) {
  93. return;
  94. }
  95. this.mainRoom.setMembersOnly(false);
  96. }
  97. /**
  98. * Broadcast a message to all participants in the lobby room
  99. * @param {Object} message The message to send
  100. *
  101. * @returns {void}
  102. */
  103. sendMessage(message) {
  104. if (this.lobbyRoom) {
  105. this.lobbyRoom.sendMessage(JSON.stringify(message), 'json-message');
  106. }
  107. }
  108. /**
  109. * Sends a private message to a participant in a lobby room.
  110. * @param {string} id The message to send
  111. * @param {Object} message The message to send
  112. *
  113. * @returns {void}
  114. */
  115. sendPrivateMessage(id, message) {
  116. if (this.lobbyRoom) {
  117. this.lobbyRoom.sendPrivateMessage(id, JSON.stringify(message), 'json-message');
  118. }
  119. }
  120. /**
  121. * Gets the local id for a participant in a lobby room.
  122. * This is used for lobby room private chat messages.
  123. *
  124. * @returns {string}
  125. */
  126. getLocalId() {
  127. if (this.lobbyRoom) {
  128. return Strophe.getResourceFromJid(this.lobbyRoom.myroomjid);
  129. }
  130. }
  131. /**
  132. * Adds a message listener to the lobby room.
  133. * @param {Function} listener The listener function,
  134. * called when a new message is received in the lobby room.
  135. *
  136. * @returns {Function} Handler returned to be able to remove it later.
  137. */
  138. addMessageListener(listener) {
  139. if (this.lobbyRoom) {
  140. const handler = (participantId, message) => {
  141. listener(message, Strophe.getResourceFromJid(participantId));
  142. };
  143. this.lobbyRoom.on(XMPPEvents.JSON_MESSAGE_RECEIVED, handler);
  144. return handler;
  145. }
  146. }
  147. /**
  148. * Remove a message handler from the lobby room.
  149. * @param {Function} handler The handler function to remove.
  150. *
  151. * @returns {void}
  152. */
  153. removeMessageHandler(handler) {
  154. if (this.lobbyRoom) {
  155. this.lobbyRoom.off(XMPPEvents.JSON_MESSAGE_RECEIVED, handler);
  156. }
  157. }
  158. /**
  159. * Leaves the lobby room.
  160. *
  161. * @returns {Promise}
  162. */
  163. leave() {
  164. if (this.lobbyRoom) {
  165. return this.lobbyRoom.leave()
  166. .then(() => {
  167. this.lobbyRoom = undefined;
  168. logger.info('Lobby room left!');
  169. })
  170. .catch(() => {}); // eslint-disable-line no-empty-function
  171. }
  172. return Promise.reject(
  173. new Error('The lobby has already been left'));
  174. }
  175. /**
  176. * We had received a jid for the lobby room.
  177. *
  178. * @param jid the lobby room jid to join.
  179. */
  180. setLobbyRoomJid(jid) {
  181. this.lobbyRoomJid = jid;
  182. }
  183. /**
  184. * Checks the state of mainRoom, lobbyRoom and current user role to decide whether to join lobby room.
  185. * @private
  186. */
  187. _maybeJoinLobbyRoom() {
  188. if (!this.isSupported()) {
  189. return;
  190. }
  191. const isModerator = this.mainRoom.joined && this.mainRoom.isModerator();
  192. if (isModerator && this.mainRoom.membersOnlyEnabled && !this.lobbyRoom) {
  193. // join the lobby
  194. this.join()
  195. .then(() => logger.info('Joined lobby room'))
  196. .catch(e => logger.error('Failed joining lobby', e));
  197. }
  198. }
  199. /**
  200. * Joins a lobby room setting display name and eventually avatar(using the email provided).
  201. *
  202. * @param {string} username is required.
  203. * @param {string} email is optional.
  204. * @returns {Promise} resolves once we join the room.
  205. */
  206. join(displayName, email) {
  207. const isModerator = this.mainRoom.joined && this.mainRoom.isModerator();
  208. if (!this.lobbyRoomJid) {
  209. return Promise.reject(new Error('Missing lobbyRoomJid, cannot join lobby room.'));
  210. }
  211. const roomName = Strophe.getNodeFromJid(this.lobbyRoomJid);
  212. const customDomain = Strophe.getDomainFromJid(this.lobbyRoomJid);
  213. this.lobbyRoom = this.xmpp.createRoom(
  214. roomName, {
  215. customDomain,
  216. disableDiscoInfo: true,
  217. disableFocus: true,
  218. enableLobby: false
  219. }
  220. );
  221. if (displayName) {
  222. // remove previously set nickname
  223. this.lobbyRoom.addOrReplaceInPresence('nick', {
  224. attributes: { xmlns: 'http://jabber.org/protocol/nick' },
  225. value: displayName
  226. });
  227. }
  228. if (isModerator) {
  229. this.lobbyRoom.addPresenceListener(EMAIL_COMMAND, (node, from) => {
  230. this.mainRoom.eventEmitter.emit(XMPPEvents.MUC_LOBBY_MEMBER_UPDATED, from, { email: node.value });
  231. });
  232. this.lobbyRoom.addEventListener(
  233. XMPPEvents.MUC_MEMBER_JOINED,
  234. // eslint-disable-next-line max-params
  235. (from, nick, role, isHiddenDomain, statsID, status, identity, botType, jid) => {
  236. // we need to ignore joins on lobby for participants that are already in the main room
  237. if (Object.values(this.mainRoom.members).find(m => m.jid === jid)) {
  238. return;
  239. }
  240. // Check if the user is a member if any breakout room.
  241. for (const room of Object.values(this.mainRoom.getBreakoutRooms()._rooms)) {
  242. if (Object.values(room.participants).find(p => p.jid === jid)) {
  243. return;
  244. }
  245. }
  246. // we emit the new event on the main room so we can propagate
  247. // events to the conference
  248. this.mainRoom.eventEmitter.emit(
  249. XMPPEvents.MUC_LOBBY_MEMBER_JOINED,
  250. Strophe.getResourceFromJid(from),
  251. nick,
  252. identity ? identity.avatar : undefined
  253. );
  254. });
  255. this.lobbyRoom.addEventListener(
  256. XMPPEvents.MUC_MEMBER_LEFT, from => {
  257. // we emit the new event on the main room so we can propagate
  258. // events to the conference
  259. this.mainRoom.eventEmitter.emit(
  260. XMPPEvents.MUC_LOBBY_MEMBER_LEFT,
  261. Strophe.getResourceFromJid(from)
  262. );
  263. });
  264. this.lobbyRoom.addEventListener(
  265. XMPPEvents.MUC_DESTROYED,
  266. () => {
  267. // let's make sure we emit that all lobby users had left
  268. Object.keys(this.lobbyRoom.members)
  269. .forEach(j => this.mainRoom.eventEmitter.emit(
  270. XMPPEvents.MUC_LOBBY_MEMBER_LEFT, Strophe.getResourceFromJid(j)));
  271. this.lobbyRoom.clean();
  272. this.lobbyRoom = undefined;
  273. logger.info('Lobby room left(destroyed)!');
  274. });
  275. } else {
  276. // this should only be handled by those waiting in lobby
  277. this.lobbyRoom.addEventListener(XMPPEvents.KICKED, isSelfPresence => {
  278. if (isSelfPresence) {
  279. this.mainRoom.eventEmitter.emit(XMPPEvents.MUC_DENIED_ACCESS);
  280. this.lobbyRoom.clean();
  281. }
  282. });
  283. // As there is still reference of the main room
  284. // the invite will be detected and addressed to its eventEmitter, even though we are not in it
  285. // the invite message should be received directly to the xmpp conn in general
  286. this.mainRoom.addEventListener(
  287. XMPPEvents.INVITE_MESSAGE_RECEIVED,
  288. (roomJid, from, txt, invitePassword) => {
  289. logger.debug(`Received approval to join ${roomJid} ${from} ${txt}`);
  290. if (roomJid === this.mainRoom.roomjid) {
  291. // we are now allowed, so let's join
  292. this.mainRoom.join(invitePassword);
  293. }
  294. });
  295. this.lobbyRoom.addEventListener(
  296. XMPPEvents.MUC_DESTROYED,
  297. (reason, jid) => {
  298. this.lobbyRoom?.clean();
  299. this.lobbyRoom = undefined;
  300. logger.info('Lobby room left(destroyed)!');
  301. // we are receiving the jid of the main room
  302. // means we are invited to join, maybe lobby was disabled
  303. if (jid) {
  304. this.mainRoom.join();
  305. return;
  306. }
  307. this.mainRoom.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
  308. });
  309. // If participant retries joining shared password while waiting in the lobby
  310. // and succeeds make sure we leave lobby
  311. this.mainRoom.addEventListener(
  312. XMPPEvents.MUC_JOINED,
  313. () => {
  314. this.leave().catch(() => {
  315. // this may happen if the room has been destroyed.
  316. });
  317. });
  318. }
  319. return new Promise((resolve, reject) => {
  320. this.lobbyRoom.addEventListener(XMPPEvents.MUC_JOINED, () => {
  321. resolve();
  322. // send our email, as we do not handle this on initial presence we need a second one
  323. if (email && !isModerator) {
  324. this.lobbyRoom.addOrReplaceInPresence(EMAIL_COMMAND, { value: email })
  325. && this.lobbyRoom.sendPresence();
  326. }
  327. });
  328. this.lobbyRoom.addEventListener(XMPPEvents.ROOM_JOIN_ERROR, reject);
  329. this.lobbyRoom.addEventListener(XMPPEvents.ROOM_CONNECT_NOT_ALLOWED_ERROR, reject);
  330. this.lobbyRoom.addEventListener(XMPPEvents.ROOM_CONNECT_ERROR, reject);
  331. this.lobbyRoom.join();
  332. });
  333. }
  334. /**
  335. * Should be possible only for moderators.
  336. * @param id
  337. */
  338. denyAccess(id) {
  339. if (!this.isSupported() || !this.mainRoom.isModerator()) {
  340. return;
  341. }
  342. const jid = Object.keys(this.lobbyRoom.members)
  343. .find(j => Strophe.getResourceFromJid(j) === id);
  344. if (jid) {
  345. this.lobbyRoom.kick(jid);
  346. } else {
  347. logger.error(`Not found member for ${id} in lobby room.`);
  348. }
  349. }
  350. /**
  351. * Should be possible only for moderators.
  352. * @param param or an array of ids.
  353. */
  354. approveAccess(param) {
  355. if (!this.isSupported() || !this.mainRoom.isModerator()) {
  356. return;
  357. }
  358. // Get the main room JID. If we are in a breakout room we'll use the main
  359. // room's lobby.
  360. let mainRoomJid = this.mainRoom.roomjid;
  361. if (this.mainRoom.getBreakoutRooms().isBreakoutRoom()) {
  362. mainRoomJid = this.mainRoom.getBreakoutRooms().getMainRoomJid();
  363. }
  364. const membersToApprove = [];
  365. let ids = param;
  366. if (!Array.isArray(param)) {
  367. ids = [ param ];
  368. }
  369. ids.forEach(id => {
  370. const memberRoomJid = Object.keys(this.lobbyRoom.members)
  371. .find(j => Strophe.getResourceFromJid(j) === id);
  372. if (memberRoomJid) {
  373. membersToApprove.push(this.lobbyRoom.members[memberRoomJid].jid);
  374. } else {
  375. logger.error(`Not found member for ${memberRoomJid} in lobby room.`);
  376. }
  377. });
  378. if (membersToApprove.length > 0) {
  379. const msgToSend
  380. = $msg({ to: mainRoomJid })
  381. .c('x', { xmlns: 'http://jabber.org/protocol/muc#user' });
  382. membersToApprove.forEach(jid => {
  383. msgToSend.c('invite', { to: jid }).up();
  384. });
  385. this.xmpp.connection.sendIQ(msgToSend,
  386. () => { }, // eslint-disable-line no-empty-function
  387. e => {
  388. logger.error(`Error sending invite for ${membersToApprove}`, e);
  389. });
  390. }
  391. }
  392. }