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.

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