From ae21a09bd62814ab753cd4b63222b89a451a187b Mon Sep 17 00:00:00 2001 From: Tudor-Ovidiu Avram Date: Thu, 18 Mar 2021 15:32:14 +0200 Subject: [PATCH] feat(sipcall) implement sip invite --- lang/main.json | 1 + react/features/invite/actions.any.js | 21 ++++- .../AbstractAddPeopleDialog.js | 19 +++-- .../web/InviteContactsForm.js | 20 ++++- react/features/invite/constants.js | 6 ++ react/features/invite/functions.js | 83 +++++++++++++++++++ 6 files changed, 141 insertions(+), 9 deletions(-) diff --git a/lang/main.json b/lang/main.json index 07185711a..1a7cd4ede 100644 --- a/lang/main.json +++ b/lang/main.json @@ -28,6 +28,7 @@ "shareInvite": "Share meeting invitation", "shareLink": "Share the meeting link to invite others", "shareStream": "Share the live streaming link", + "sip": "SIP: {{address}}", "telephone": "Telephone: {{number}}", "title": "Invite people to this meeting", "yahooEmail": "Yahoo Email" diff --git a/react/features/invite/actions.any.js b/react/features/invite/actions.any.js index d128aa8af..5ef3f0c2b 100644 --- a/react/features/invite/actions.any.js +++ b/react/features/invite/actions.any.js @@ -3,7 +3,7 @@ import type { Dispatch } from 'redux'; import { getInviteURL } from '../base/connection'; -import { getParticipants } from '../base/participants'; +import { getLocalParticipant, getParticipants } from '../base/participants'; import { inviteVideoRooms } from '../videosipgw'; import { @@ -18,7 +18,8 @@ import { import { getDialInConferenceID, getDialInNumbers, - invitePeopleAndChatRooms + invitePeopleAndChatRooms, + inviteSipEndpoints } from './functions'; import logger from './logger'; @@ -102,7 +103,9 @@ export function invite( inviteServiceCallFlowsUrl } = state['features/base/config']; const inviteUrl = getInviteURL(state); + const { sipInviteUrl } = state['features/base/config']; const { jwt } = state['features/base/jwt']; + const { name: displayName } = getLocalParticipant(state); // First create all promises for dialing out. const phoneNumbers @@ -164,6 +167,20 @@ export function invite( invitesLeftToSend = invitesLeftToSend.filter(({ type }) => type !== 'videosipgw'); + const sipEndpoints + = invitesLeftToSend.filter(({ type }) => type === 'sip'); + + conference && inviteSipEndpoints( + sipEndpoints, + sipInviteUrl, + jwt, + conference.options.name, + displayName + ); + + invitesLeftToSend + = invitesLeftToSend.filter(({ type }) => type !== 'sip'); + return ( Promise.all(allInvitePromises) .then(() => invitesLeftToSend)); diff --git a/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.js b/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.js index fc97f814a..794d30311 100644 --- a/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.js +++ b/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.js @@ -12,7 +12,8 @@ import { getInviteResultsForQuery, getInviteTypeCounts, isAddPeopleEnabled, - isDialOutEnabled + isDialOutEnabled, + isSipInviteEnabled } from '../../functions'; import logger from '../../logger'; @@ -38,6 +39,11 @@ export type Props = { */ _dialOutEnabled: boolean, + /** + * Whether or not to allow sip invites. + */ + _sipInviteEnabled: boolean, + /** * The JWT token. */ @@ -96,7 +102,7 @@ export default class AbstractAddPeopleDialog /** * Invite people and numbers to the conference. The logic works by inviting - * numbers, people/rooms, and videosipgw in parallel. All invitees are + * numbers, people/rooms, sip endpoints and videosipgw in parallel. All invitees are * stored in an array. As each invite succeeds, the invitee is removed * from the array. After all invites finish, close the modal if there are * no invites left to send. If any are left, that means an invite failed @@ -214,7 +220,8 @@ export default class AbstractAddPeopleDialog _dialOutEnabled: dialOutEnabled, _jwt: jwt, _peopleSearchQueryTypes: peopleSearchQueryTypes, - _peopleSearchUrl: peopleSearchUrl + _peopleSearchUrl: peopleSearchUrl, + _sipInviteEnabled: sipInviteEnabled } = this.props; const options = { addPeopleEnabled, @@ -222,7 +229,8 @@ export default class AbstractAddPeopleDialog dialOutEnabled, jwt, peopleSearchQueryTypes, - peopleSearchUrl + peopleSearchUrl, + sipInviteEnabled }; return getInviteResultsForQuery(query, options); @@ -259,6 +267,7 @@ export function _mapStateToProps(state: Object) { _dialOutEnabled: isDialOutEnabled(state), _jwt: state['features/base/jwt'].jwt, _peopleSearchQueryTypes: peopleSearchQueryTypes, - _peopleSearchUrl: peopleSearchUrl + _peopleSearchUrl: peopleSearchUrl, + _sipInviteEnabled: isSipInviteEnabled(state) }; } diff --git a/react/features/invite/components/add-people-dialog/web/InviteContactsForm.js b/react/features/invite/components/add-people-dialog/web/InviteContactsForm.js index c20dd6045..fbeb46797 100644 --- a/react/features/invite/components/add-people-dialog/web/InviteContactsForm.js +++ b/react/features/invite/components/add-people-dialog/web/InviteContactsForm.js @@ -285,7 +285,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog { */ _parseQueryResults(response = []) { const { t, _dialOutEnabled } = this.props; - const users = response.filter(item => item.type !== 'phone'); + const users = response.filter(item => item.type !== 'phone' && item.type !== 'sip'); const userDisplayItems = []; for (const user of users) { @@ -348,9 +348,25 @@ class InviteContactsForm extends AbstractAddPeopleDialog { }; }); + + const sipAddresses = response.filter(item => item.type === 'sip'); + + const sipDisplayItems = sipAddresses.map(sip => { + return { + filterValues: [ + sip.address + ], + content: t('addPeople.sip', { address: sip.address }), + description: '', + item: sip, + value: sip.address + }; + }); + return [ ...userDisplayItems, - ...numberDisplayItems + ...numberDisplayItems, + ...sipDisplayItems ]; } diff --git a/react/features/invite/constants.js b/react/features/invite/constants.js index 10e396ac3..d894f34da 100644 --- a/react/features/invite/constants.js +++ b/react/features/invite/constants.js @@ -42,3 +42,9 @@ export const OUTGOING_CALL_RINGING_SOUND_ID = 'OUTGOING_CALL_RINGING_SOUND_ID'; * @type {string} */ export const OUTGOING_CALL_START_SOUND_ID = 'OUTGOING_CALL_START_SOUND_ID'; + +/** + * Regex for matching email addresses. + */ +// eslint-disable-next-line max-len +export const EMAIL_ADDRESS_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js index 948451398..26954a646 100644 --- a/react/features/invite/functions.js +++ b/react/features/invite/functions.js @@ -10,6 +10,7 @@ import { toState } from '../base/redux'; import { doGetJSON, parseURIString } from '../base/util'; import { isVpaasMeeting } from '../billing-counter/functions'; +import { EMAIL_ADDRESS_REGEX } from './constants'; import logger from './logger'; declare var $: Function; @@ -122,6 +123,11 @@ export type GetInviteResultsOptions = { */ peopleSearchUrl: string, + /** + * Whether or not to check sip invites. + */ + sipInviteEnabled: boolean, + /** * The jwt token to pass to the search service. */ @@ -149,6 +155,7 @@ export function getInviteResultsForQuery( dialOutEnabled, peopleSearchQueryTypes, peopleSearchUrl, + sipInviteEnabled, jwt } = options; @@ -233,6 +240,13 @@ export function getInviteResultsForQuery( }); } + if (sipInviteEnabled && isASipAddress(text)) { + results.push({ + type: 'sip', + address: text + }); + } + return results; }); } @@ -374,6 +388,21 @@ export function isDialOutEnabled(state: Object): boolean { && conference && conference.isSIPCallingSupported(); } +/** + * Determines if inviting sip endpoints is enabled or not. + * + * @param {Object} state - Current state. + * @returns {boolean} Indication of whether dial out is currently enabled. + */ +export function isSipInviteEnabled(state: Object): boolean { + const { sipInviteUrl } = state['features/base/config']; + const { features = {} } = getLocalParticipant(state); + + return state['features/base/jwt'].jwt + && Boolean(sipInviteUrl) + && String(features['sip-outbound-call']) === 'true'; +} + /** * Checks whether a string looks like it could be for a phone number. * @@ -392,6 +421,16 @@ function isMaybeAPhoneNumber(text: string): boolean { return Boolean(digits.length); } +/** + * Checks whether a string matches a sip address format. + * + * @param {string} text - The text to check. + * @returns {boolean} True if provided text matches a sip address format. + */ +function isASipAddress(text: string): boolean { + return EMAIL_ADDRESS_REGEX.test(text); +} + /** * RegExp to use to determine if some text might be a phone number. * @@ -764,3 +803,47 @@ export function isSharingEnabled(sharingFeature: string) { || typeof interfaceConfig.SHARING_FEATURES === 'undefined' || (interfaceConfig.SHARING_FEATURES.length && interfaceConfig.SHARING_FEATURES.indexOf(sharingFeature) > -1); } + +/** + * Sends a post request to an invite service. + * + * @param {Immutable.List} inviteItems - The list of the "sip" type items to invite. + * @param {string} sipInviteUrl - The invite service that generates the invitation. + * @param {string} jwt - The jwt token. + * @param {string} roomName - The name to the conference. + * @param {string} displayName - The user display name. + * @returns {Promise} - The promise created by the request. + */ +export function inviteSipEndpoints( // eslint-disable-line max-params + inviteItems: Array, + sipInviteUrl: string, + jwt: string, + roomName: string, + displayName: string +): Promise { + if (inviteItems.length === 0) { + return Promise.resolve(); + } + + return fetch( + `${sipInviteUrl}?token=${jwt}`, + { + body: JSON.stringify({ + callParams: { + callUrlInfo: { + baseUrl: window.location.origin, + callName: roomName + } + }, + sipClientParams: { + displayName, + sipAddress: inviteItems.map(item => item.address) + } + }), + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + } + ); +}