Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

functions.ts 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938
  1. import { IReduxState } from '../app/types';
  2. import { IStateful } from '../base/app/types';
  3. import { getRoomName } from '../base/conference/functions';
  4. import { getInviteURL } from '../base/connection/functions';
  5. import { isIosMobileBrowser } from '../base/environment/utils';
  6. import i18next from '../base/i18n/i18next';
  7. import { isJwtFeatureEnabled } from '../base/jwt/functions';
  8. import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
  9. import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants/functions';
  10. import { toState } from '../base/redux/functions';
  11. import { doGetJSON } from '../base/util/httpUtils';
  12. import { parseURLParams } from '../base/util/parseURLParams';
  13. import {
  14. StatusCode,
  15. appendURLParam,
  16. parseURIString
  17. } from '../base/util/uri';
  18. import { isVpaasMeeting } from '../jaas/functions';
  19. import { getActiveSession } from '../recording/functions';
  20. import { getDialInConferenceID, getDialInNumbers } from './_utils';
  21. import {
  22. DIAL_IN_INFO_PAGE_PATH_NAME,
  23. INVITE_TYPES,
  24. SIP_ADDRESS_REGEX,
  25. UPGRADE_OPTIONS_TEXT
  26. } from './constants';
  27. import logger from './logger';
  28. declare let $: any;
  29. export const sharingFeatures = {
  30. email: 'email',
  31. url: 'url',
  32. dialIn: 'dial-in',
  33. embed: 'embed'
  34. };
  35. /**
  36. * Sends an ajax request to check if the phone number can be called.
  37. *
  38. * @param {string} dialNumber - The dial number to check for validity.
  39. * @param {string} dialOutAuthUrl - The endpoint to use for checking validity.
  40. * @returns {Promise} - The promise created by the request.
  41. */
  42. export function checkDialNumber(
  43. dialNumber: string,
  44. dialOutAuthUrl: string
  45. ): Promise<{ allow?: boolean; country?: string; phone?: string; }> {
  46. const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`;
  47. return new Promise((resolve, reject) => {
  48. $.getJSON(fullUrl)
  49. .then(resolve)
  50. .catch(reject);
  51. });
  52. }
  53. /**
  54. * Sends an ajax request to check if the outbound call is permitted.
  55. *
  56. * @param {string} dialOutRegionUrl - The config endpoint.
  57. * @param {string} jwt - The jwt token.
  58. * @param {string} appId - The customer id.
  59. * @param {string} phoneNumber - The destination phone number.
  60. * @returns {Promise} - The promise created by the request.
  61. */
  62. export function checkOutboundDestination(
  63. dialOutRegionUrl: string,
  64. jwt: string,
  65. appId: string,
  66. phoneNumber: string
  67. ): Promise<any> {
  68. return doGetJSON(dialOutRegionUrl, true, {
  69. body: JSON.stringify({
  70. appId,
  71. phoneNumber
  72. }),
  73. method: 'POST',
  74. headers: {
  75. 'Authorization': `Bearer ${jwt}`,
  76. 'Content-Type': 'application/json'
  77. }
  78. });
  79. }
  80. /**
  81. * Removes all non-numeric characters from a string.
  82. *
  83. * @param {string} text - The string from which to remove all characters except
  84. * numbers.
  85. * @returns {string} A string with only numbers.
  86. */
  87. export function getDigitsOnly(text = ''): string {
  88. return text.replace(/\D/g, '');
  89. }
  90. /**
  91. * Type of the options to use when sending a search query.
  92. */
  93. export type GetInviteResultsOptions = {
  94. /**
  95. * Whether or not to search for people.
  96. */
  97. addPeopleEnabled: boolean;
  98. /**
  99. * The customer id.
  100. */
  101. appId: string;
  102. /**
  103. * The endpoint to use for checking phone number validity.
  104. */
  105. dialOutAuthUrl: string;
  106. /**
  107. * Whether or not to check phone numbers.
  108. */
  109. dialOutEnabled: boolean;
  110. /**
  111. * The endpoint to use for checking dial permission to an outbound destination.
  112. */
  113. dialOutRegionUrl: string;
  114. /**
  115. * The jwt token to pass to the search service.
  116. */
  117. jwt: string;
  118. /**
  119. * Array with the query types that will be executed -
  120. * "conferenceRooms" | "user" | "room".
  121. */
  122. peopleSearchQueryTypes: Array<string>;
  123. /**
  124. * The url to query for people.
  125. */
  126. peopleSearchUrl: string;
  127. /**
  128. * Whether or not to check sip invites.
  129. */
  130. sipInviteEnabled: boolean;
  131. };
  132. /**
  133. * Combines directory search with phone number validation to produce a single
  134. * set of invite search results.
  135. *
  136. * @param {string} query - Text to search.
  137. * @param {GetInviteResultsOptions} options - Options to use when searching.
  138. * @returns {Promise<*>}
  139. */
  140. export function getInviteResultsForQuery(
  141. query: string,
  142. options: GetInviteResultsOptions
  143. ): Promise<any> {
  144. const text = query.trim();
  145. const {
  146. addPeopleEnabled,
  147. appId,
  148. dialOutAuthUrl,
  149. dialOutRegionUrl,
  150. dialOutEnabled,
  151. peopleSearchQueryTypes,
  152. peopleSearchUrl,
  153. sipInviteEnabled,
  154. jwt
  155. } = options;
  156. let peopleSearchPromise;
  157. if (addPeopleEnabled && text) {
  158. peopleSearchPromise = searchDirectory(
  159. peopleSearchUrl,
  160. jwt,
  161. text,
  162. peopleSearchQueryTypes);
  163. } else {
  164. peopleSearchPromise = Promise.resolve([]);
  165. }
  166. let hasCountryCode = text.startsWith('+');
  167. let phoneNumberPromise;
  168. // Phone numbers are handled a specially to enable both cases of restricting
  169. // numbers to telephone number-y numbers and accepting any arbitrary string,
  170. // which may be valid for SIP (jigasi) calls. If the dialOutAuthUrl is
  171. // defined, then it is assumed the call is to a telephone number and
  172. // some validation of the number is completed, with the + sign used as a way
  173. // for the UI to detect and enforce the usage of a country code. If the
  174. // dialOutAuthUrl is not defined, accept anything because this is assumed
  175. // to be the SIP (jigasi) case.
  176. if (dialOutEnabled && dialOutAuthUrl && isMaybeAPhoneNumber(text)) {
  177. let numberToVerify = text;
  178. // When the number to verify does not start with a +, we assume no
  179. // proper country code has been entered. In such a case, prepend 1 for
  180. // the country code. The service currently takes care of prepending the
  181. // +.
  182. if (!hasCountryCode && !text.startsWith('1')) {
  183. numberToVerify = `1${numberToVerify}`;
  184. }
  185. // The validation service works properly when the query is digits only
  186. // so ensure only digits get sent.
  187. numberToVerify = getDigitsOnly(numberToVerify);
  188. phoneNumberPromise = checkDialNumber(numberToVerify, dialOutAuthUrl);
  189. } else if (dialOutEnabled && !dialOutAuthUrl) {
  190. // fake having a country code to hide the country code reminder
  191. hasCountryCode = true;
  192. // With no auth url, let's say the text is a valid number
  193. phoneNumberPromise = Promise.resolve({
  194. allow: true,
  195. country: '',
  196. phone: text
  197. });
  198. } else {
  199. phoneNumberPromise = Promise.resolve<{ allow?: boolean; country?: string; phone?: string; }>({});
  200. }
  201. return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
  202. .then(async ([ peopleResults, phoneResults ]) => {
  203. const results: any[] = [
  204. ...peopleResults
  205. ];
  206. /**
  207. * This check for phone results is for the day the call to searching
  208. * people might return phone results as well. When that day comes
  209. * this check will make it so the server checks are honored and the
  210. * local appending of the number is not done. The local appending of
  211. * the phone number can then be cleaned up when convenient.
  212. */
  213. const hasPhoneResult
  214. = peopleResults.find(result => result.type === INVITE_TYPES.PHONE);
  215. if (!hasPhoneResult && typeof phoneResults.allow === 'boolean') {
  216. const result = {
  217. allowed: phoneResults.allow,
  218. country: phoneResults.country,
  219. type: INVITE_TYPES.PHONE,
  220. number: phoneResults.phone,
  221. originalEntry: text,
  222. showCountryCodeReminder: !hasCountryCode
  223. };
  224. if (!phoneResults.allow) {
  225. try {
  226. const response = await checkOutboundDestination(dialOutRegionUrl, jwt, appId, text);
  227. result.allowed = response.allowed;
  228. } catch (error) {
  229. logger.error('Error checking permission to dial to outbound destination', error);
  230. }
  231. }
  232. results.push(result);
  233. }
  234. if (sipInviteEnabled && isASipAddress(text)) {
  235. results.push({
  236. type: INVITE_TYPES.SIP,
  237. address: text
  238. });
  239. }
  240. return results;
  241. });
  242. }
  243. /**
  244. * Creates a custom no new lines message for iOS default mail describing how to dial in to the conference.
  245. *
  246. * @returns {string}
  247. */
  248. export function getInviteTextiOS({
  249. state,
  250. phoneNumber,
  251. t
  252. }: { phoneNumber?: string | null; state: IReduxState; t?: Function; }) {
  253. if (!isIosMobileBrowser()) {
  254. return '';
  255. }
  256. const dialIn = state['features/invite'];
  257. const inviteUrl = getInviteURL(state);
  258. const localParticipant = getLocalParticipant(state);
  259. const localParticipantName = localParticipant?.name;
  260. const inviteURL = _decodeRoomURI(inviteUrl);
  261. let invite = localParticipantName
  262. ? t?.('info.inviteTextiOSPersonal', { name: localParticipantName })
  263. : t?.('info.inviteURLFirstPartGeneral');
  264. invite += ' ';
  265. invite += t?.('info.inviteTextiOSInviteUrl', { inviteUrl });
  266. invite += ' ';
  267. if (shouldDisplayDialIn(dialIn) && isSharingEnabled(sharingFeatures.dialIn)) {
  268. invite += t?.('info.inviteTextiOSPhone', {
  269. number: phoneNumber,
  270. conferenceID: dialIn.conferenceID,
  271. didUrl: getDialInfoPageURL(state)
  272. });
  273. }
  274. invite += ' ';
  275. invite += t?.('info.inviteTextiOSJoinSilent', { silentUrl: `${inviteURL}#config.startSilent=true` });
  276. return invite;
  277. }
  278. /**
  279. * Creates a message describing how to dial in to the conference.
  280. *
  281. * @returns {string}
  282. */
  283. export function getInviteText({
  284. state,
  285. phoneNumber,
  286. t
  287. }: { phoneNumber?: string | null; state: IReduxState; t?: Function; }) {
  288. const dialIn = state['features/invite'];
  289. const inviteUrl = getInviteURL(state);
  290. const currentLiveStreamingSession = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
  291. const liveStreamViewURL = currentLiveStreamingSession?.liveStreamViewURL;
  292. const localParticipant = getLocalParticipant(state);
  293. const localParticipantName = localParticipant?.name;
  294. const inviteURL = _decodeRoomURI(inviteUrl);
  295. let invite = localParticipantName
  296. ? t?.('info.inviteURLFirstPartPersonal', { name: localParticipantName })
  297. : t?.('info.inviteURLFirstPartGeneral');
  298. invite += t?.('info.inviteURLSecondPart', {
  299. url: inviteURL
  300. });
  301. if (liveStreamViewURL) {
  302. const liveStream = t?.('info.inviteLiveStream', {
  303. url: liveStreamViewURL
  304. });
  305. invite = `${invite}\n${liveStream}`;
  306. }
  307. if (shouldDisplayDialIn(dialIn) && isSharingEnabled(sharingFeatures.dialIn)) {
  308. const dial = t?.('info.invitePhone', {
  309. number: phoneNumber,
  310. conferenceID: dialIn.conferenceID
  311. });
  312. const moreNumbers = t?.('info.invitePhoneAlternatives', {
  313. url: getDialInfoPageURL(state),
  314. silentUrl: `${inviteURL}#config.startSilent=true`
  315. });
  316. invite = `${invite}\n${dial}\n${moreNumbers}`;
  317. }
  318. return invite;
  319. }
  320. /**
  321. * Helper for determining how many of each type of user is being invited. Used
  322. * for logging and sending analytics related to invites.
  323. *
  324. * @param {Array} inviteItems - An array with the invite items, as created in
  325. * {@link _parseQueryResults}.
  326. * @returns {Object} An object with keys as user types and values as the number
  327. * of invites for that type.
  328. */
  329. export function getInviteTypeCounts(inviteItems: Array<{ type: string; }> = []) {
  330. const inviteTypeCounts: any = {};
  331. inviteItems.forEach(({ type }) => {
  332. if (!inviteTypeCounts[type]) {
  333. inviteTypeCounts[type] = 0;
  334. }
  335. inviteTypeCounts[type]++;
  336. });
  337. return inviteTypeCounts;
  338. }
  339. /**
  340. * Sends a post request to an invite service.
  341. *
  342. * @param {string} inviteServiceUrl - The invite service that generates the
  343. * invitation.
  344. * @param {string} inviteUrl - The url to the conference.
  345. * @param {string} jwt - The jwt token to pass to the search service.
  346. * @param {Immutable.List} inviteItems - The list of the "user" or "room" type
  347. * items to invite.
  348. * @returns {Promise} - The promise created by the request.
  349. */
  350. export function invitePeopleAndChatRooms(
  351. inviteServiceUrl: string,
  352. inviteUrl: string,
  353. jwt: string,
  354. inviteItems: Array<Object>
  355. ): Promise<any> {
  356. if (!inviteItems || inviteItems.length === 0) {
  357. return Promise.resolve();
  358. }
  359. return fetch(
  360. `${inviteServiceUrl}?token=${jwt}`,
  361. {
  362. body: JSON.stringify({
  363. 'invited': inviteItems,
  364. 'url': inviteUrl
  365. }),
  366. method: 'POST',
  367. headers: {
  368. 'Content-Type': 'application/json'
  369. }
  370. }
  371. );
  372. }
  373. /**
  374. * Determines if adding people is currently enabled.
  375. *
  376. * @param {IReduxState} state - Current state.
  377. * @returns {boolean} Indication of whether adding people is currently enabled.
  378. */
  379. export function isAddPeopleEnabled(state: IReduxState): boolean {
  380. const { peopleSearchUrl } = state['features/base/config'];
  381. return Boolean(state['features/base/jwt'].jwt && Boolean(peopleSearchUrl) && !isVpaasMeeting(state));
  382. }
  383. /**
  384. * Determines if dial out is currently enabled or not.
  385. *
  386. * @param {IReduxState} state - Current state.
  387. * @returns {boolean} Indication of whether dial out is currently enabled.
  388. */
  389. export function isDialOutEnabled(state: IReduxState): boolean {
  390. const { conference } = state['features/base/conference'];
  391. return isLocalParticipantModerator(state)
  392. && conference && conference.isSIPCallingSupported();
  393. }
  394. /**
  395. * Determines if inviting sip endpoints is enabled or not.
  396. *
  397. * @param {IReduxState} state - Current state.
  398. * @returns {boolean} Indication of whether dial out is currently enabled.
  399. */
  400. export function isSipInviteEnabled(state: IReduxState): boolean {
  401. const { sipInviteUrl } = state['features/base/config'];
  402. return isJwtFeatureEnabled(state, 'sip-outbound-call') && Boolean(sipInviteUrl);
  403. }
  404. /**
  405. * Checks whether a string looks like it could be for a phone number.
  406. *
  407. * @param {string} text - The text to check whether or not it could be a phone
  408. * number.
  409. * @private
  410. * @returns {boolean} True if the string looks like it could be a phone number.
  411. */
  412. function isMaybeAPhoneNumber(text: string): boolean {
  413. if (!isPhoneNumberRegex().test(text)) {
  414. return false;
  415. }
  416. const digits = getDigitsOnly(text);
  417. return Boolean(digits.length);
  418. }
  419. /**
  420. * Checks whether a string matches a sip address format.
  421. *
  422. * @param {string} text - The text to check.
  423. * @returns {boolean} True if provided text matches a sip address format.
  424. */
  425. function isASipAddress(text: string): boolean {
  426. return SIP_ADDRESS_REGEX.test(text);
  427. }
  428. /**
  429. * RegExp to use to determine if some text might be a phone number.
  430. *
  431. * @returns {RegExp}
  432. */
  433. function isPhoneNumberRegex(): RegExp {
  434. let regexString = '^[0-9+()-\\s]*$';
  435. if (typeof interfaceConfig !== 'undefined') {
  436. regexString = interfaceConfig.PHONE_NUMBER_REGEX || regexString;
  437. }
  438. return new RegExp(regexString);
  439. }
  440. /**
  441. * Sends an ajax request to a directory service.
  442. *
  443. * @param {string} serviceUrl - The service to query.
  444. * @param {string} jwt - The jwt token to pass to the search service.
  445. * @param {string} text - Text to search.
  446. * @param {Array<string>} queryTypes - Array with the query types that will be
  447. * executed - "conferenceRooms" | "user" | "room".
  448. * @returns {Promise} - The promise created by the request.
  449. */
  450. export function searchDirectory( // eslint-disable-line max-params
  451. serviceUrl: string,
  452. jwt: string,
  453. text: string,
  454. queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
  455. ): Promise<Array<{ type: string; }>> {
  456. const query = encodeURIComponent(text);
  457. const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
  458. return fetch(`${serviceUrl}?query=${query}&queryTypes=${
  459. queryTypesString}&jwt=${jwt}`)
  460. .then(response => {
  461. const jsonify = response.json();
  462. if (response.ok) {
  463. return jsonify;
  464. }
  465. return jsonify
  466. .then(result => Promise.reject(result));
  467. })
  468. .catch(error => {
  469. logger.error(
  470. 'Error searching directory:', error);
  471. return Promise.reject(error);
  472. });
  473. }
  474. /**
  475. * Returns descriptive text that can be used to invite participants to a meeting
  476. * (share via mobile or use it for calendar event description).
  477. *
  478. * @param {IReduxState} state - The current state.
  479. * @param {string} inviteUrl - The conference/location URL.
  480. * @param {boolean} useHtml - Whether to return html text.
  481. * @returns {Promise<string>} A {@code Promise} resolving with a
  482. * descriptive text that can be used to invite participants to a meeting.
  483. */
  484. export function getShareInfoText(state: IReduxState, inviteUrl: string, useHtml?: boolean): Promise<string> {
  485. let roomUrl = _decodeRoomURI(inviteUrl);
  486. const includeDialInfo = state['features/base/config'] !== undefined;
  487. if (useHtml) {
  488. roomUrl = `<a href="${roomUrl}">${roomUrl}</a>`;
  489. }
  490. let infoText = i18next.t('share.mainText', { roomUrl });
  491. if (includeDialInfo) {
  492. const { room } = parseURIString(inviteUrl);
  493. let numbersPromise;
  494. let hasPaymentError = false;
  495. if (state['features/invite'].numbers
  496. && state['features/invite'].conferenceID) {
  497. numbersPromise = Promise.resolve(state['features/invite']);
  498. } else {
  499. // we are requesting numbers and conferenceId directly
  500. // not using updateDialInNumbers, because custom room
  501. // is specified and we do not want to store the data
  502. // in the state
  503. const { dialInConfCodeUrl, dialInNumbersUrl, hosts }
  504. = state['features/base/config'];
  505. const { locationURL = {} } = state['features/base/connection'];
  506. const mucURL = hosts?.muc;
  507. if (!dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
  508. // URLs for fetching dial in numbers not defined
  509. return Promise.resolve(infoText);
  510. }
  511. numbersPromise = Promise.all([
  512. getDialInNumbers(dialInNumbersUrl, room, mucURL), // @ts-ignore
  513. getDialInConferenceID(dialInConfCodeUrl, room, mucURL, locationURL)
  514. ]).then(([ numbers, {
  515. conference, id, message } ]) => {
  516. if (!conference || !id) {
  517. return Promise.reject(message);
  518. }
  519. return {
  520. numbers,
  521. conferenceID: id
  522. };
  523. });
  524. }
  525. return numbersPromise.then(
  526. ({ conferenceID, numbers }) => {
  527. const phoneNumber = _getDefaultPhoneNumber(numbers) || '';
  528. return `${
  529. i18next.t('info.dialInNumber')} ${
  530. phoneNumber} ${
  531. i18next.t('info.dialInConferenceID')} ${
  532. conferenceID}#\n\n`;
  533. })
  534. .catch(error => {
  535. logger.error('Error fetching numbers or conferenceID', error);
  536. hasPaymentError = error?.status === StatusCode.PaymentRequired;
  537. })
  538. .then(defaultDialInNumber => {
  539. if (hasPaymentError) {
  540. infoText += `${
  541. i18next.t('info.dialInNumber')} ${i18next.t('info.reachedLimit')} ${
  542. i18next.t('info.upgradeOptions')} ${UPGRADE_OPTIONS_TEXT}`;
  543. return infoText;
  544. }
  545. let dialInfoPageUrl = getDialInfoPageURL(state, room);
  546. if (useHtml) {
  547. dialInfoPageUrl
  548. = `<a href="${dialInfoPageUrl}">${dialInfoPageUrl}</a>`;
  549. }
  550. infoText += i18next.t('share.dialInfoText', {
  551. defaultDialInNumber,
  552. dialInfoPageUrl });
  553. return infoText;
  554. });
  555. }
  556. return Promise.resolve(infoText);
  557. }
  558. /**
  559. * Generates the URL for the static dial in info page.
  560. *
  561. * @param {IReduxState} state - The state from the Redux store.
  562. * @param {string?} roomName - The conference name. Optional name, if missing will be extracted from state.
  563. * @returns {string}
  564. */
  565. export function getDialInfoPageURL(state: IReduxState, roomName?: string) {
  566. const { didPageUrl } = state['features/dynamic-branding'];
  567. const conferenceName = roomName ?? getRoomName(state);
  568. const { locationURL } = state['features/base/connection'];
  569. const { href = '' } = locationURL ?? {};
  570. const room = _decodeRoomURI(conferenceName ?? '');
  571. const url = didPageUrl || `${href.substring(0, href.lastIndexOf('/'))}/${DIAL_IN_INFO_PAGE_PATH_NAME}`;
  572. return appendURLParam(url, 'room', room);
  573. }
  574. /**
  575. * Generates the URL for the static dial in info page.
  576. *
  577. * @param {string} uri - The conference URI string.
  578. * @returns {string}
  579. */
  580. export function getDialInfoPageURLForURIString(
  581. uri?: string) {
  582. if (!uri) {
  583. return undefined;
  584. }
  585. const { protocol, host, contextRoot, room } = parseURIString(uri);
  586. let url = `${protocol}//${host}${contextRoot}${DIAL_IN_INFO_PAGE_PATH_NAME}`;
  587. url = appendURLParam(url, 'room', room);
  588. const { release } = parseURLParams(uri, true, 'search');
  589. release && (url = appendURLParam(url, 'release', release));
  590. return url;
  591. }
  592. /**
  593. * Returns whether or not dial-in related UI should be displayed.
  594. *
  595. * @param {Object} dialIn - Dial in information.
  596. * @returns {boolean}
  597. */
  598. export function shouldDisplayDialIn(dialIn: any) {
  599. const { conferenceID, numbers, numbersEnabled } = dialIn;
  600. const phoneNumber = _getDefaultPhoneNumber(numbers);
  601. return Boolean(
  602. conferenceID
  603. && numbers
  604. && numbersEnabled
  605. && phoneNumber);
  606. }
  607. /**
  608. * Returns if multiple dial-in numbers are available.
  609. *
  610. * @param {Array<string>|Object} dialInNumbers - The array or object of
  611. * numbers to check.
  612. * @private
  613. * @returns {boolean}
  614. */
  615. export function hasMultipleNumbers(dialInNumbers?: { numbers: Object; } | string[]) {
  616. if (!dialInNumbers) {
  617. return false;
  618. }
  619. if (Array.isArray(dialInNumbers)) {
  620. return dialInNumbers.length > 1;
  621. }
  622. // deprecated and will be removed
  623. const { numbers } = dialInNumbers;
  624. // eslint-disable-next-line no-confusing-arrow
  625. return Boolean(numbers && Object.values(numbers).map(a => Array.isArray(a) ? a.length : 0)
  626. .reduce((a, b) => a + b) > 1);
  627. }
  628. /**
  629. * Sets the internal state of which dial-in number to display.
  630. *
  631. * @param {Array<string>|Object} dialInNumbers - The array or object of
  632. * numbers to choose a number from.
  633. * @private
  634. * @returns {string|null}
  635. */
  636. export function _getDefaultPhoneNumber(
  637. dialInNumbers?: { numbers: any; } | Array<{ default: string; formattedNumber: string; }>): string | null {
  638. if (!dialInNumbers) {
  639. return null;
  640. }
  641. if (Array.isArray(dialInNumbers)) {
  642. // new syntax follows
  643. // find the default country inside dialInNumbers, US one
  644. // or return the first one
  645. const defaultNumber = dialInNumbers.find(number => number.default);
  646. if (defaultNumber) {
  647. return defaultNumber.formattedNumber;
  648. }
  649. return dialInNumbers.length > 0
  650. ? dialInNumbers[0].formattedNumber : null;
  651. }
  652. const { numbers } = dialInNumbers;
  653. if (numbers && Object.keys(numbers).length > 0) {
  654. // deprecated and will be removed
  655. const firstRegion = Object.keys(numbers)[0];
  656. return firstRegion && numbers[firstRegion][0];
  657. }
  658. return null;
  659. }
  660. /**
  661. * Decodes URI only if doesn't contain a space(' ').
  662. *
  663. * @param {string} url - The string to decode.
  664. * @returns {string} - It the string contains space, encoded value is '%20' returns
  665. * same string, otherwise decoded one.
  666. * @private
  667. */
  668. export function _decodeRoomURI(url: string) {
  669. let roomUrl = url;
  670. // we want to decode urls when the do not contain space, ' ', which url encoded is %20
  671. if (roomUrl && !roomUrl.includes('%20')) {
  672. roomUrl = decodeURI(roomUrl);
  673. }
  674. // Handles a special case where the room name has % encoded, the decoded will have
  675. // % followed by a char (non-digit) which is not a valid URL and room name ... so we do not
  676. // want to show this decoded
  677. if (roomUrl.match(/.*%[^\d].*/)) {
  678. return url;
  679. }
  680. return roomUrl;
  681. }
  682. /**
  683. * Returns the stored conference id.
  684. *
  685. * @param {IStateful} stateful - The Object or Function that can be
  686. * resolved to a Redux state object with the toState function.
  687. * @returns {string}
  688. */
  689. export function getConferenceId(stateful: IStateful) {
  690. return toState(stateful)['features/invite'].conferenceID;
  691. }
  692. /**
  693. * Returns the default dial in number from the store.
  694. *
  695. * @param {IStateful} stateful - The Object or Function that can be
  696. * resolved to a Redux state object with the toState function.
  697. * @returns {string | null}
  698. */
  699. export function getDefaultDialInNumber(stateful: IStateful) {
  700. // @ts-ignore
  701. return _getDefaultPhoneNumber(toState(stateful)['features/invite'].numbers);
  702. }
  703. /**
  704. * Executes the dial out request.
  705. *
  706. * @param {string} url - The url for dialing out.
  707. * @param {Object} body - The body of the request.
  708. * @param {string} reqId - The unique request id.
  709. * @returns {Object}
  710. */
  711. export async function executeDialOutRequest(url: string, body: Object, reqId: string) {
  712. const res = await fetch(url, {
  713. method: 'POST',
  714. headers: {
  715. 'Content-Type': 'application/json',
  716. 'request-id': reqId
  717. },
  718. body: JSON.stringify(body)
  719. });
  720. const json = await res.json();
  721. return res.ok ? json : Promise.reject(json);
  722. }
  723. /**
  724. * Executes the dial out status request.
  725. *
  726. * @param {string} url - The url for dialing out.
  727. * @param {string} reqId - The unique request id used on the dial out request.
  728. * @returns {Object}
  729. */
  730. export async function executeDialOutStatusRequest(url: string, reqId: string) {
  731. const res = await fetch(url, {
  732. method: 'GET',
  733. headers: {
  734. 'Content-Type': 'application/json',
  735. 'request-id': reqId
  736. }
  737. });
  738. const json = await res.json();
  739. return res.ok ? json : Promise.reject(json);
  740. }
  741. /**
  742. * Returns true if a specific sharing feature is enabled in interface configuration.
  743. *
  744. * @param {string} sharingFeature - The sharing feature to check.
  745. * @returns {boolean}
  746. */
  747. export function isSharingEnabled(sharingFeature: string) {
  748. return typeof interfaceConfig === 'undefined'
  749. || typeof interfaceConfig.SHARING_FEATURES === 'undefined'
  750. || (interfaceConfig.SHARING_FEATURES.length && interfaceConfig.SHARING_FEATURES.indexOf(sharingFeature) > -1);
  751. }
  752. /**
  753. * Sends a post request to an invite service.
  754. *
  755. * @param {Array} inviteItems - The list of the "sip" type items to invite.
  756. * @param {URL} locationURL - The URL of the location.
  757. * @param {string} sipInviteUrl - The invite service that generates the invitation.
  758. * @param {string} jwt - The jwt token.
  759. * @param {string} roomName - The name to the conference.
  760. * @param {string} roomPassword - The password of the conference.
  761. * @param {string} displayName - The user display name.
  762. * @returns {Promise} - The promise created by the request.
  763. */
  764. export function inviteSipEndpoints( // eslint-disable-line max-params
  765. inviteItems: Array<{ address: string; }>,
  766. locationURL: URL,
  767. sipInviteUrl: string,
  768. jwt: string,
  769. roomName: string,
  770. roomPassword: String,
  771. displayName: string
  772. ): Promise<any> {
  773. if (inviteItems.length === 0) {
  774. return Promise.resolve();
  775. }
  776. const regex = new RegExp(`/${roomName}`, 'i');
  777. const baseUrl = Object.assign(new URL(locationURL.toString()), {
  778. pathname: locationURL.pathname.replace(regex, ''),
  779. hash: '',
  780. search: ''
  781. });
  782. return fetch(
  783. sipInviteUrl,
  784. {
  785. body: JSON.stringify({
  786. callParams: {
  787. callUrlInfo: {
  788. baseUrl,
  789. callName: roomName
  790. },
  791. passcode: roomPassword
  792. },
  793. sipClientParams: {
  794. displayName,
  795. sipAddress: inviteItems.map(item => item.address)
  796. }
  797. }),
  798. method: 'POST',
  799. headers: {
  800. 'Authorization': `Bearer ${jwt}`,
  801. 'Content-Type': 'application/json'
  802. }
  803. }
  804. );
  805. }