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.

functions.ts 30KB

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