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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  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. $.post(
  238. `${inviteServiceUrl}?token=${jwt}`,
  239. JSON.stringify({
  240. 'invited': inviteItems,
  241. 'url': inviteUrl
  242. }),
  243. resolve,
  244. 'json')
  245. .fail((jqxhr, textStatus, error) => reject(error));
  246. });
  247. }
  248. /**
  249. * Determines if adding people is currently enabled.
  250. *
  251. * @param {boolean} state - Current state.
  252. * @returns {boolean} Indication of whether adding people is currently enabled.
  253. */
  254. export function isAddPeopleEnabled(state: Object): boolean {
  255. const { isGuest } = state['features/base/jwt'];
  256. return !isGuest;
  257. }
  258. /**
  259. * Determines if dial out is currently enabled or not.
  260. *
  261. * @param {boolean} state - Current state.
  262. * @returns {boolean} Indication of whether dial out is currently enabled.
  263. */
  264. export function isDialOutEnabled(state: Object): boolean {
  265. const { conference } = state['features/base/conference'];
  266. return isLocalParticipantModerator(state)
  267. && conference && conference.isSIPCallingSupported();
  268. }
  269. /**
  270. * Checks whether a string looks like it could be for a phone number.
  271. *
  272. * @param {string} text - The text to check whether or not it could be a phone
  273. * number.
  274. * @private
  275. * @returns {boolean} True if the string looks like it could be a phone number.
  276. */
  277. function isMaybeAPhoneNumber(text: string): boolean {
  278. if (!isPhoneNumberRegex().test(text)) {
  279. return false;
  280. }
  281. const digits = getDigitsOnly(text);
  282. return Boolean(digits.length);
  283. }
  284. /**
  285. * RegExp to use to determine if some text might be a phone number.
  286. *
  287. * @returns {RegExp}
  288. */
  289. function isPhoneNumberRegex(): RegExp {
  290. let regexString = '^[0-9+()-\\s]*$';
  291. if (typeof interfaceConfig !== 'undefined') {
  292. regexString = interfaceConfig.PHONE_NUMBER_REGEX || regexString;
  293. }
  294. return new RegExp(regexString);
  295. }
  296. /**
  297. * Sends an ajax request to a directory service.
  298. *
  299. * @param {string} serviceUrl - The service to query.
  300. * @param {string} jwt - The jwt token to pass to the search service.
  301. * @param {string} text - Text to search.
  302. * @param {Array<string>} queryTypes - Array with the query types that will be
  303. * executed - "conferenceRooms" | "user" | "room".
  304. * @returns {Promise} - The promise created by the request.
  305. */
  306. export function searchDirectory( // eslint-disable-line max-params
  307. serviceUrl: string,
  308. jwt: string,
  309. text: string,
  310. queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
  311. ): Promise<Array<Object>> {
  312. const query = encodeURIComponent(text);
  313. const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
  314. return fetch(`${serviceUrl}?query=${query}&queryTypes=${
  315. queryTypesString}&jwt=${jwt}`)
  316. .then(response => {
  317. const jsonify = response.json();
  318. if (response.ok) {
  319. return jsonify;
  320. }
  321. return jsonify
  322. .then(result => Promise.reject(result));
  323. })
  324. .catch(error => {
  325. logger.error(
  326. 'Error searching directory:', error);
  327. return Promise.reject(error);
  328. });
  329. }
  330. /**
  331. * Returns descriptive text that can be used to invite participants to a meeting
  332. * (share via mobile or use it for calendar event description).
  333. *
  334. * @param {Object} state - The current state.
  335. * @param {string} inviteUrl - The conference/location URL.
  336. * @param {boolean} useHtml - Whether to return html text.
  337. * @returns {Promise<string>} A {@code Promise} resolving with a
  338. * descriptive text that can be used to invite participants to a meeting.
  339. */
  340. export function getShareInfoText(
  341. state: Object, inviteUrl: string, useHtml: ?boolean): Promise<string> {
  342. let roomUrl = inviteUrl;
  343. const includeDialInfo = state['features/base/config'] !== undefined;
  344. if (useHtml) {
  345. roomUrl = `<a href="${roomUrl}">${roomUrl}</a>`;
  346. }
  347. let infoText = i18next.t('share.mainText', { roomUrl });
  348. if (includeDialInfo) {
  349. const { room } = parseURIString(inviteUrl);
  350. let numbersPromise;
  351. if (state['features/invite'].numbers
  352. && state['features/invite'].conferenceID) {
  353. numbersPromise = Promise.resolve(state['features/invite']);
  354. } else {
  355. // we are requesting numbers and conferenceId directly
  356. // not using updateDialInNumbers, because custom room
  357. // is specified and we do not want to store the data
  358. // in the state
  359. const { dialInConfCodeUrl, dialInNumbersUrl, hosts }
  360. = state['features/base/config'];
  361. const mucURL = hosts && hosts.muc;
  362. if (!dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
  363. // URLs for fetching dial in numbers not defined
  364. return Promise.resolve(infoText);
  365. }
  366. numbersPromise = Promise.all([
  367. getDialInNumbers(dialInNumbersUrl, room, mucURL),
  368. getDialInConferenceID(dialInConfCodeUrl, room, mucURL)
  369. ]).then(([ { defaultCountry, numbers }, {
  370. conference, id, message } ]) => {
  371. if (!conference || !id) {
  372. return Promise.reject(message);
  373. }
  374. return {
  375. defaultCountry,
  376. numbers,
  377. conferenceID: id
  378. };
  379. });
  380. }
  381. return numbersPromise.then(
  382. ({ conferenceID, defaultCountry, numbers }) => {
  383. const phoneNumber
  384. = _getDefaultPhoneNumber(numbers, defaultCountry) || '';
  385. return `${
  386. i18next.t('info.dialInNumber')} ${
  387. phoneNumber} ${
  388. i18next.t('info.dialInConferenceID')} ${
  389. conferenceID}#\n\n`;
  390. })
  391. .catch(error =>
  392. logger.error('Error fetching numbers or conferenceID', error))
  393. .then(defaultDialInNumber => {
  394. let dialInfoPageUrl = getDialInfoPageURL(
  395. room,
  396. state['features/base/connection'].locationURL);
  397. if (useHtml) {
  398. dialInfoPageUrl
  399. = `<a href="${dialInfoPageUrl}">${dialInfoPageUrl}</a>`;
  400. }
  401. infoText += i18next.t('share.dialInfoText', {
  402. defaultDialInNumber,
  403. dialInfoPageUrl });
  404. return infoText;
  405. });
  406. }
  407. return Promise.resolve(infoText);
  408. }
  409. /**
  410. * Generates the URL for the static dial in info page.
  411. *
  412. * @param {string} conferenceName - The conference name.
  413. * @param {Object} locationURL - The current location URL, the object coming
  414. * from state ['features/base/connection'].locationURL.
  415. * @returns {string}
  416. */
  417. export function getDialInfoPageURL(
  418. conferenceName: string,
  419. locationURL: Object) {
  420. const origin = locationURL.origin;
  421. const pathParts = locationURL.pathname.split('/');
  422. pathParts.length = pathParts.length - 1;
  423. const newPath = pathParts.reduce((accumulator, currentValue) => {
  424. if (currentValue) {
  425. return `${accumulator}/${currentValue}`;
  426. }
  427. return accumulator;
  428. }, '');
  429. return `${origin}${newPath}/static/dialInInfo.html?room=${conferenceName}`;
  430. }
  431. /**
  432. * Sets the internal state of which dial-in number to display.
  433. *
  434. * @param {Array<string>|Object} dialInNumbers - The array or object of
  435. * numbers to choose a number from.
  436. * @param {string} defaultCountry - The country code for the country
  437. * whose phone number should display.
  438. * @private
  439. * @returns {string|null}
  440. */
  441. export function _getDefaultPhoneNumber(
  442. dialInNumbers: Object,
  443. defaultCountry: string = 'US'): ?string {
  444. if (Array.isArray(dialInNumbers)) {
  445. // Dumbly return the first number if an array.
  446. return dialInNumbers[0];
  447. } else if (Object.keys(dialInNumbers).length > 0) {
  448. const defaultNumbers = dialInNumbers[defaultCountry];
  449. if (defaultNumbers) {
  450. return defaultNumbers[0];
  451. }
  452. const firstRegion = Object.keys(dialInNumbers)[0];
  453. return firstRegion && firstRegion[0];
  454. }
  455. return null;
  456. }