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.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. // @flow
  2. import { getAppProp } from '../base/app';
  3. import { i18next } from '../base/i18n';
  4. import { isLocalParticipantModerator } from '../base/participants';
  5. import { doGetJSON, parseURIString } from '../base/util';
  6. declare var $: Function;
  7. declare var interfaceConfig: Object;
  8. const logger = require('jitsi-meet-logger').getLogger(__filename);
  9. /**
  10. * Sends an ajax request to check if the phone number can be called.
  11. *
  12. * @param {string} dialNumber - The dial number to check for validity.
  13. * @param {string} dialOutAuthUrl - The endpoint to use for checking validity.
  14. * @returns {Promise} - The promise created by the request.
  15. */
  16. export function checkDialNumber(
  17. dialNumber: string,
  18. dialOutAuthUrl: string
  19. ): Promise<Object> {
  20. const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`;
  21. return new Promise((resolve, reject) => {
  22. $.getJSON(fullUrl)
  23. .then(resolve)
  24. .catch(reject);
  25. });
  26. }
  27. /**
  28. * Sends a GET request to obtain the conference ID necessary for identifying
  29. * which conference to join after diaing the dial-in service.
  30. *
  31. * @param {string} baseUrl - The url for obtaining the conference ID (pin) for
  32. * dialing into a conference.
  33. * @param {string} roomName - The conference name to find the associated
  34. * conference ID.
  35. * @param {string} mucURL - In which MUC the conference exists.
  36. * @returns {Promise} - The promise created by the request.
  37. */
  38. export function getDialInConferenceID(
  39. baseUrl: string,
  40. roomName: string,
  41. mucURL: string
  42. ): Promise<Object> {
  43. const conferenceIDURL = `${baseUrl}?conference=${roomName}@${mucURL}`;
  44. return doGetJSON(conferenceIDURL);
  45. }
  46. /**
  47. * Sends a GET request for phone numbers used to dial into a conference.
  48. *
  49. * @param {string} url - The service that returns confernce dial-in numbers.
  50. * @returns {Promise} - The promise created by the request. The returned numbers
  51. * may be an array of numbers or an object with countries as keys and arrays of
  52. * phone number strings.
  53. */
  54. export function getDialInNumbers(url: string): Promise<*> {
  55. return doGetJSON(url);
  56. }
  57. /**
  58. * Removes all non-numeric characters from a string.
  59. *
  60. * @param {string} text - The string from which to remove all characters except
  61. * numbers.
  62. * @returns {string} A string with only numbers.
  63. */
  64. export function getDigitsOnly(text: string = ''): string {
  65. return text.replace(/\D/g, '');
  66. }
  67. /**
  68. * Type of the options to use when sending a search query.
  69. */
  70. export type GetInviteResultsOptions = {
  71. /**
  72. * The endpoint to use for checking phone number validity.
  73. */
  74. dialOutAuthUrl: string,
  75. /**
  76. * Whether or not to search for people.
  77. */
  78. addPeopleEnabled: boolean,
  79. /**
  80. * Whether or not to check phone numbers.
  81. */
  82. dialOutEnabled: boolean,
  83. /**
  84. * Array with the query types that will be executed -
  85. * "conferenceRooms" | "user" | "room".
  86. */
  87. peopleSearchQueryTypes: Array<string>,
  88. /**
  89. * The url to query for people.
  90. */
  91. peopleSearchUrl: string,
  92. /**
  93. * The jwt token to pass to the search service.
  94. */
  95. jwt: string
  96. };
  97. /**
  98. * Combines directory search with phone number validation to produce a single
  99. * set of invite search results.
  100. *
  101. * @param {string} query - Text to search.
  102. * @param {GetInviteResultsOptions} options - Options to use when searching.
  103. * @returns {Promise<*>}
  104. */
  105. export function getInviteResultsForQuery(
  106. query: string,
  107. options: GetInviteResultsOptions
  108. ): Promise<*> {
  109. const text = query.trim();
  110. const {
  111. dialOutAuthUrl,
  112. addPeopleEnabled,
  113. dialOutEnabled,
  114. peopleSearchQueryTypes,
  115. peopleSearchUrl,
  116. jwt
  117. } = options;
  118. let peopleSearchPromise;
  119. if (addPeopleEnabled && text) {
  120. peopleSearchPromise = searchDirectory(
  121. peopleSearchUrl,
  122. jwt,
  123. text,
  124. peopleSearchQueryTypes);
  125. } else {
  126. peopleSearchPromise = Promise.resolve([]);
  127. }
  128. let hasCountryCode = text.startsWith('+');
  129. let phoneNumberPromise;
  130. // Phone numbers are handled a specially to enable both cases of restricting
  131. // numbers to telephone number-y numbers and accepting any arbitrary string,
  132. // which may be valid for SIP (jigasi) calls. If the dialOutAuthUrl is
  133. // defined, then it is assumed the call is to a telephone number and
  134. // some validation of the number is completed, with the + sign used as a way
  135. // for the UI to detect and enforce the usage of a country code. If the
  136. // dialOutAuthUrl is not defined, accept anything because this is assumed
  137. // to be the SIP (jigasi) case.
  138. if (dialOutEnabled && dialOutAuthUrl && isMaybeAPhoneNumber(text)) {
  139. let numberToVerify = text;
  140. // When the number to verify does not start with a +, we assume no
  141. // proper country code has been entered. In such a case, prepend 1 for
  142. // the country code. The service currently takes care of prepending the
  143. // +.
  144. if (!hasCountryCode && !text.startsWith('1')) {
  145. numberToVerify = `1${numberToVerify}`;
  146. }
  147. // The validation service works properly when the query is digits only
  148. // so ensure only digits get sent.
  149. numberToVerify = getDigitsOnly(numberToVerify);
  150. phoneNumberPromise = checkDialNumber(numberToVerify, dialOutAuthUrl);
  151. } else if (dialOutEnabled && !dialOutAuthUrl) {
  152. // fake having a country code to hide the country code reminder
  153. hasCountryCode = true;
  154. // With no auth url, let's say the text is a valid number
  155. phoneNumberPromise = Promise.resolve({
  156. allow: true,
  157. country: '',
  158. phone: text
  159. });
  160. } else {
  161. phoneNumberPromise = Promise.resolve({});
  162. }
  163. return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
  164. .then(([ peopleResults, phoneResults ]) => {
  165. const results = [
  166. ...peopleResults
  167. ];
  168. /**
  169. * This check for phone results is for the day the call to searching
  170. * people might return phone results as well. When that day comes
  171. * this check will make it so the server checks are honored and the
  172. * local appending of the number is not done. The local appending of
  173. * the phone number can then be cleaned up when convenient.
  174. */
  175. const hasPhoneResult
  176. = peopleResults.find(result => result.type === 'phone');
  177. if (!hasPhoneResult && typeof phoneResults.allow === 'boolean') {
  178. results.push({
  179. allowed: phoneResults.allow,
  180. country: phoneResults.country,
  181. type: 'phone',
  182. number: phoneResults.phone,
  183. originalEntry: text,
  184. showCountryCodeReminder: !hasCountryCode
  185. });
  186. }
  187. return results;
  188. });
  189. }
  190. /**
  191. * Helper for determining how many of each type of user is being invited. Used
  192. * for logging and sending analytics related to invites.
  193. *
  194. * @param {Array} inviteItems - An array with the invite items, as created in
  195. * {@link _parseQueryResults}.
  196. * @returns {Object} An object with keys as user types and values as the number
  197. * of invites for that type.
  198. */
  199. export function getInviteTypeCounts(inviteItems: Array<Object> = []) {
  200. const inviteTypeCounts = {};
  201. inviteItems.forEach(({ type }) => {
  202. if (!inviteTypeCounts[type]) {
  203. inviteTypeCounts[type] = 0;
  204. }
  205. inviteTypeCounts[type]++;
  206. });
  207. return inviteTypeCounts;
  208. }
  209. /**
  210. * Sends a post request to an invite service.
  211. *
  212. * @param {string} inviteServiceUrl - The invite service that generates the
  213. * invitation.
  214. * @param {string} inviteUrl - The url to the conference.
  215. * @param {string} jwt - The jwt token to pass to the search service.
  216. * @param {Immutable.List} inviteItems - The list of the "user" or "room" type
  217. * items to invite.
  218. * @returns {Promise} - The promise created by the request.
  219. */
  220. export function invitePeopleAndChatRooms( // eslint-disable-line max-params
  221. inviteServiceUrl: string,
  222. inviteUrl: string,
  223. jwt: string,
  224. inviteItems: Array<Object>
  225. ): Promise<void> {
  226. if (!inviteItems || inviteItems.length === 0) {
  227. return Promise.resolve();
  228. }
  229. return new Promise((resolve, reject) => {
  230. $.post(
  231. `${inviteServiceUrl}?token=${jwt}`,
  232. JSON.stringify({
  233. 'invited': inviteItems,
  234. 'url': inviteUrl
  235. }),
  236. resolve,
  237. 'json')
  238. .fail((jqxhr, textStatus, error) => reject(error));
  239. });
  240. }
  241. /**
  242. * Determines if adding people is currently enabled.
  243. *
  244. * @param {boolean} state - Current state.
  245. * @returns {boolean} Indication of whether adding people is currently enabled.
  246. */
  247. export function isAddPeopleEnabled(state: Object): boolean {
  248. const { isGuest } = state['features/base/jwt'];
  249. if (!isGuest) {
  250. // XXX The mobile/react-native app is capable of disabling the
  251. // adding/inviting of people in the current conference. Anyway, the
  252. // Web/React app does not have that capability so default appropriately.
  253. const addPeopleEnabled = getAppProp(state, 'addPeopleEnabled');
  254. return (
  255. (typeof addPeopleEnabled === 'undefined')
  256. || Boolean(addPeopleEnabled));
  257. }
  258. return false;
  259. }
  260. /**
  261. * Determines if dial out is currently enabled or not.
  262. *
  263. * @param {boolean} state - Current state.
  264. * @returns {boolean} Indication of whether dial out is currently enabled.
  265. */
  266. export function isDialOutEnabled(state: Object): boolean {
  267. const { conference } = state['features/base/conference'];
  268. let dialOutEnabled = isLocalParticipantModerator(state)
  269. && conference
  270. && conference.isSIPCallingSupported();
  271. if (dialOutEnabled) {
  272. // XXX The mobile/react-native app is capable of disabling of dial-out.
  273. // Anyway, the Web/React app does not have that capability so default
  274. // appropriately.
  275. dialOutEnabled = getAppProp(state, 'dialOutEnabled');
  276. return (
  277. (typeof dialOutEnabled === 'undefined') || Boolean(dialOutEnabled));
  278. }
  279. return false;
  280. }
  281. /**
  282. * Checks whether a string looks like it could be for a phone number.
  283. *
  284. * @param {string} text - The text to check whether or not it could be a phone
  285. * number.
  286. * @private
  287. * @returns {boolean} True if the string looks like it could be a phone number.
  288. */
  289. function isMaybeAPhoneNumber(text: string): boolean {
  290. if (!isPhoneNumberRegex().test(text)) {
  291. return false;
  292. }
  293. const digits = getDigitsOnly(text);
  294. return Boolean(digits.length);
  295. }
  296. /**
  297. * RegExp to use to determine if some text might be a phone number.
  298. *
  299. * @returns {RegExp}
  300. */
  301. function isPhoneNumberRegex(): RegExp {
  302. let regexString = '^[0-9+()-\\s]*$';
  303. if (typeof interfaceConfig !== 'undefined') {
  304. regexString = interfaceConfig.PHONE_NUMBER_REGEX || regexString;
  305. }
  306. return new RegExp(regexString);
  307. }
  308. /**
  309. * Sends an ajax request to a directory service.
  310. *
  311. * @param {string} serviceUrl - The service to query.
  312. * @param {string} jwt - The jwt token to pass to the search service.
  313. * @param {string} text - Text to search.
  314. * @param {Array<string>} queryTypes - Array with the query types that will be
  315. * executed - "conferenceRooms" | "user" | "room".
  316. * @returns {Promise} - The promise created by the request.
  317. */
  318. export function searchDirectory( // eslint-disable-line max-params
  319. serviceUrl: string,
  320. jwt: string,
  321. text: string,
  322. queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
  323. ): Promise<Array<Object>> {
  324. const query = encodeURIComponent(text);
  325. const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
  326. return fetch(`${serviceUrl}?query=${query}&queryTypes=${
  327. queryTypesString}&jwt=${jwt}`)
  328. .then(response => {
  329. const jsonify = response.json();
  330. if (response.ok) {
  331. return jsonify;
  332. }
  333. return jsonify
  334. .then(result => Promise.reject(result));
  335. })
  336. .catch(error => {
  337. logger.error(
  338. 'Error searching directory:', error);
  339. return Promise.reject(error);
  340. });
  341. }
  342. /**
  343. * Returns descriptive text that can be used to invite participants to a meeting
  344. * (share via mobile or use it for calendar event description).
  345. *
  346. * @param {Object} state - The current state.
  347. * @param {string} inviteUrl - The conference/location URL.
  348. * @param {boolean} useHtml - Whether to return html text.
  349. * @returns {Promise<string>} A {@code Promise} resolving with a
  350. * descriptive text that can be used to invite participants to a meeting.
  351. */
  352. export function getShareInfoText(
  353. state: Object, inviteUrl: string, useHtml: ?boolean): Promise<string> {
  354. let roomUrl = inviteUrl;
  355. const includeDialInfo = state['features/base/config'] !== undefined;
  356. if (useHtml) {
  357. roomUrl = `<a href="${roomUrl}">${roomUrl}</a>`;
  358. }
  359. let infoText = i18next.t('share.mainText', { roomUrl });
  360. if (includeDialInfo) {
  361. const { room } = parseURIString(inviteUrl);
  362. let numbersPromise;
  363. if (state['features/invite'].numbers
  364. && state['features/invite'].conferenceID) {
  365. numbersPromise = Promise.resolve(state['features/invite']);
  366. } else {
  367. // we are requesting numbers and conferenceId directly
  368. // not using updateDialInNumbers, because custom room
  369. // is specified and we do not want to store the data
  370. // in the state
  371. const { dialInConfCodeUrl, dialInNumbersUrl, hosts }
  372. = state['features/base/config'];
  373. const mucURL = hosts && hosts.muc;
  374. if (!dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
  375. // URLs for fetching dial in numbers not defined
  376. return Promise.resolve(infoText);
  377. }
  378. numbersPromise = Promise.all([
  379. getDialInNumbers(dialInNumbersUrl),
  380. getDialInConferenceID(dialInConfCodeUrl, room, mucURL)
  381. ]).then(([ { defaultCountry, numbers }, {
  382. conference, id, message } ]) => {
  383. if (!conference || !id) {
  384. return Promise.reject(message);
  385. }
  386. return {
  387. defaultCountry,
  388. numbers,
  389. conferenceID: id
  390. };
  391. });
  392. }
  393. return numbersPromise.then(
  394. ({ conferenceID, defaultCountry, numbers }) => {
  395. const phoneNumber
  396. = _getDefaultPhoneNumber(numbers, defaultCountry) || '';
  397. return `${
  398. i18next.t('info.dialInNumber')} ${
  399. phoneNumber} ${
  400. i18next.t('info.dialInConferenceID')} ${
  401. conferenceID}#\n\n`;
  402. })
  403. .catch(error =>
  404. logger.error('Error fetching numbers or conferenceID', error))
  405. .then(defaultDialInNumber => {
  406. let dialInfoPageUrl = getDialInfoPageURL(
  407. room,
  408. state['features/base/connection'].locationURL);
  409. if (useHtml) {
  410. dialInfoPageUrl
  411. = `<a href="${dialInfoPageUrl}">${dialInfoPageUrl}</a>`;
  412. }
  413. infoText += i18next.t('share.dialInfoText', {
  414. defaultDialInNumber,
  415. dialInfoPageUrl });
  416. return infoText;
  417. });
  418. }
  419. return Promise.resolve(infoText);
  420. }
  421. /**
  422. * Generates the URL for the static dial in info page.
  423. *
  424. * @param {string} conferenceName - The conference name.
  425. * @param {Object} locationURL - The current location URL, the object coming
  426. * from state ['features/base/connection'].locationURL.
  427. * @returns {string}
  428. */
  429. export function getDialInfoPageURL(
  430. conferenceName: string,
  431. locationURL: Object) {
  432. const origin = locationURL.origin;
  433. const pathParts = locationURL.pathname.split('/');
  434. pathParts.length = pathParts.length - 1;
  435. const newPath = pathParts.reduce((accumulator, currentValue) => {
  436. if (currentValue) {
  437. return `${accumulator}/${currentValue}`;
  438. }
  439. return accumulator;
  440. }, '');
  441. return `${origin}${newPath}/static/dialInInfo.html?room=${conferenceName}`;
  442. }
  443. /**
  444. * Sets the internal state of which dial-in number to display.
  445. *
  446. * @param {Array<string>|Object} dialInNumbers - The array or object of
  447. * numbers to choose a number from.
  448. * @param {string} defaultCountry - The country code for the country
  449. * whose phone number should display.
  450. * @private
  451. * @returns {string|null}
  452. */
  453. export function _getDefaultPhoneNumber(
  454. dialInNumbers: Object,
  455. defaultCountry: string = 'US') {
  456. if (Array.isArray(dialInNumbers)) {
  457. // Dumbly return the first number if an array.
  458. return dialInNumbers[0];
  459. } else if (Object.keys(dialInNumbers).length > 0) {
  460. const defaultNumbers = dialInNumbers[defaultCountry];
  461. if (defaultNumbers) {
  462. return defaultNumbers[0];
  463. }
  464. const firstRegion = Object.keys(dialInNumbers)[0];
  465. return firstRegion && firstRegion[0];
  466. }
  467. return null;
  468. }