diff --git a/README.md b/README.md index 08c726f..8fb519c 100644 --- a/README.md +++ b/README.md @@ -188,41 +188,183 @@ $ npm run listener An easy way to test the plugin is to use a local policy. In this case we designed a local policy for testing. It will allow every VMR with 2 or 6 digits -and will use the pins 1234 for the hosts and 4321 for the guests. +and will use the pins `1234` for the hosts and `4321` for the guests. + +The process is as follows: + +- We will use a local participant policy that adds a `callTag` to the + participant in the **main room**. To obtain it, we will follow this process: + 1. Concatenate the following parameters: + - `local_alias` + - `vendor` + - `display_name` + 2. Apply `pex_hash` to the concatenated string. + 3. Take only the last 20 digits of the result. + +- The plugin will retrieve the user's `callTag` and use it to generate the `pin` + for the **interpretation room**. To do so, we will follow this process: + 1. Concatenate the following parameters: + - The previous `callTag` + - `role` (e.g., `interpreter` or `listener`) + +- The service configuration policy will attempt to replicate the same `pin` + using the same process. It will choose the role `interpreter` for the `pin` + and `listener` for the `guest_pin`. + +```mermaid +sequenceDiagram +participant Webapp3 +participant Plugin +participant Infinity + +Webapp3 ->> Infinity: Join main conference {pin: } +Infinity -->> Webapp3: Joined { call_tag: pexHash(local_alias + vendor + display_name).tail(20) } +Plugin ->> Infinity: Join interpretation {pin: pexHash(call_tag + role).tail(20)} +Infinity ->> Infinity: Check PIN:
pin: pexHash(call_tag + 'interpreter').tail(20),
guest_pin: pexHash(call_tag + 'listener').tail(20) +Infinity -->> Plugin: Joined +``` + +### Service configuration policy ```python -{ - {% if (call_info.local_alias | pex_regex_replace('^(\d{2}|\d{6})$', '') == '') %} +{# Static PINs but we can define a function to generate a PIN per VMR #} +{% set pin = "1234" %} +{% set guest_pin = "4321" %} + +{% set callTag = "" %} + +{% if (call_info.local_alias | pex_regex_replace('^(\d{2})$', '') == '') %} + {# Main rooms for 2-digit VMRs #} + + { "action": "continue", "result": { - "service_type": "conference", - "name": "{{call_info.local_alias}}", - "service_tag": "pexip-interpreter", - "description": "", - "call_tag": "", - "pin": "1234", - "guest_pin": "4321", - "guests_can_present": true, - "allow_guests": true, - "view": "four_mains_zero_pips", - "ivr_theme_name": "visitor_normal", - "locked": false, - "automatic_participants": [] - } - {% elif service_config %} - { - "action" : "continue", - "result" : {{service_config | pex_to_json}} + "service_type": "conference", + "name": "{{call_info.local_alias}}", + "service_tag": "pexip-interpreter", + "pin": "{{pin}}", + "guest_pin": "{{guest_pin}}", + "guests_can_present": true, + "allow_guests": true, + "view": "four_mains_zero_pips" } - {% else %} - { - "action" : "reject", - "result" : {} + } + +{% elif (call_info.local_alias | pex_regex_replace('^(\d{6})$', '') == '') %} + {# Interpretation rooms for 6-digit VMRs #} + + {% set callTag = ((call_info.local_alias | pex_regex_replace('^(\d{2})(\d{4})$', '\\1') ) + call_info.vendor + (call_info.remote_display_name | pex_regex_replace('^(.*)\ -\ (Interpreter|Listener)$', '\\1') )) | pex_hash | pex_tail(20) %} + + {% set pin = (callTag + "interpreter") | pex_hash | pex_tail(20) %} + {% set guest_pin = (callTag + "listener") | pex_hash | pex_tail(20) %} + + { + "action": "continue", + "result": { + "service_type": "conference", + "name": "{{call_info.local_alias}}", + "service_tag": "pexip-interpreter", + "pin": "{{pin}}", + "guest_pin": "{{guest_pin}}", + "allow_guests": true } - {% endif %} -} + } + +{% elif service_config %} + + { + "action" : "continue", + "result" : {{service_config | pex_to_json}} + } + +{% else %} + + { + "action" : "reject", + "result" : {} + } + +{% endif %} ``` +### Participant policy + +```python +{% set callTag = "" %} + +{# Remove the Webapp3 suffix from the vendor string, e.g. " Webapp3/11.0.0+c29a9d064" #} +{% set vendor = call_info.vendor | pex_regex_replace(" Webapp3.*$", "") %} + +{% if (call_info.local_alias | pex_regex_replace('^(\d{2})$', '') == '') %} + {% set callTag = (call_info.local_alias + vendor + call_info.remote_display_name) | pex_hash | pex_tail(20) %} + + { + "status": "success", + "action": "continue", + "result": { + "call_tag": "{{callTag}}" + } + } + +{% else %} + + { + "status": "success", + "action": "continue", + "result": {} + } + +{% endif %} +``` + +### Join with a SIP device to a interpretation room (testing only) + +To join to an interpretation room through a SIP device you need to follow these +steps: + +#### Create a new Call Routing Rule + +- Click on `Services > Call Routing`. +- Click on `Add Call Routing Rule`. +- Define the following parameters (leave the rest as default): + - **Name:** Interpretation SIP Join + - **Priority:** 1 + - **Destination alias regex match** .\* +- Click on `Save`. + +#### Create a VMR for interpretation + +- Click on `Services > Virtual Meeting Rooms`. +- Click on `Add Virtual Meeting Room`. +- Define the following parameters (leave the rest as default): + - **Name:** French room for 01 + - **Host PIN:** 5678 + - **Allow Guests:** true + - **Guest PIN:** 8765 + - **Alias:** 010033 + - **Advanced options**: + - **Guest can present:** false + - **Enable chat:** false + - **Conference capabilities:** Audio-only +- Click on `Save`. + +#### Install a SIP softphone in your computer + +- You need to install a SIP softphone in your computer. You can use + [Zoiper](https://www.zoiper.com/en/voip-softphone/download/current) or any + other softphone that you like. + +- Configure the SIP softphone with a SIP account that can reach your Infinity + deployment. + +- Make a call to the VMR created in the previous step (e.g. + 010033@192.168.1.101). Use the dialpad to enter the PIN for the host or guest. + +- In this case we will use the static VMR created before, so the host PIN is + `5678` and the guest PIN is `8765`. The reason is that the `local_alias` is + `sip:010033` and it doesn't match the regex for 2 or 6 digit VMRs from the + local policy. + ## Build for production To create a package you need to install first all the dependencies: diff --git a/src/InterpretationContext/InterpretationContext.tsx b/src/InterpretationContext/InterpretationContext.tsx index 8e1a4d2..a5f3932 100644 --- a/src/InterpretationContext/InterpretationContext.tsx +++ b/src/InterpretationContext/InterpretationContext.tsx @@ -23,6 +23,7 @@ import { getMainConferenceAlias } from '../conference' import { MainRoom } from '../main-room' import { setButtonActive } from '../button' import { logger } from '../logger' +import { capitalizeFirstLetter, pexHash } from '../utils' const batchScheduleTimeoutMS = 500 const batchBufferSize = 10 @@ -84,8 +85,7 @@ export const InterpretationContextProvider = (props: { infinityClient = createInfinityClient(clientSignals, callSignals) const username = getUser().displayName ?? getUser().uuid - let roleTag = '' - let callType: ClientCallType = ClientCallType.Audio + const roleTag = capitalizeFirstLetter(state.role) if (!state.connected) { dispatch({ @@ -96,45 +96,28 @@ export const InterpretationContextProvider = (props: { }) } - const { Audio, AudioSendOnly, AudioRecvOnly } = ClientCallType - if (state.role === Role.Interpreter) { - roleTag = 'Interpreter' + const callType = getCallType(state.role) + + let mediaStream: MediaStream | undefined = undefined + if (callType !== ClientCallType.AudioRecvOnly) { const constraints = MainRoom.getMediaConstraints() mediaStream = await getMediaStream(constraints) - const { interpreter } = config - let shouldSendReceive = false - if (interpreter != null) { - const { allowChangeDirection } = interpreter - shouldSendReceive = allowChangeDirection - } - if (shouldSendReceive) { - callType = Audio - } else { - callType = AudioSendOnly - } - } else { - roleTag = 'Listener' - mediaStream = undefined - const { listener } = config - let shouldSendReceive = false - if (listener != null) { - const { speakToInterpretationRoom } = listener - shouldSendReceive = speakToInterpretationRoom - } - if (shouldSendReceive) { - const constraints = MainRoom.getMediaConstraints() - mediaStream = await getMediaStream(constraints) - callType = Audio - } else { - callType = AudioRecvOnly - } } - const displayName = `${username} - ${roleTag}` try { const conferenceAlias = getMainConferenceAlias() + language.code const bandwidth = 0 + + const { rawData } = getUser() + const { call_tag: callTag } = rawData + + if (callTag != null) { + const maxSize = 20 + const input = `${callTag}${state.role}` + pin = (await pexHash(input))?.slice(-maxSize) ?? pin + } + if (pin != null) { await infinityClient.call({ conferenceAlias, @@ -261,6 +244,40 @@ export const InterpretationContextProvider = (props: { }) } + const getCallType = (role: Role): ClientCallType => { + const { Audio, AudioSendOnly, AudioRecvOnly } = ClientCallType + + let callType: ClientCallType = Audio + + if (role === Role.Interpreter) { + const { interpreter } = config + let shouldSendReceive = false + if (interpreter != null) { + const { allowChangeDirection } = interpreter + shouldSendReceive = allowChangeDirection + } + if (shouldSendReceive) { + callType = Audio + } else { + callType = AudioSendOnly + } + } else { + const { listener } = config + let shouldSendReceive = false + if (listener != null) { + const { speakToInterpretationRoom } = listener + shouldSendReceive = speakToInterpretationRoom + } + if (shouldSendReceive) { + callType = Audio + } else { + callType = AudioRecvOnly + } + } + + return callType + } + const getMediaStream = async ( constraints?: MediaTrackConstraints ): Promise => { diff --git a/src/utils.ts b/src/utils.ts index 76ab374..708ea05 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import { logger } from './logger' + export const isSameDomain = (): boolean => { // Check if the plugin is served from the same domain as Web App 3 let sameDomain = true @@ -17,3 +19,32 @@ export const capitalizeFirstLetter = (value: string): string => { } export const isIOS = (): boolean => /iPad|iPhone|iPod/.test(navigator.userAgent) + +export const pexHash = async (input: string): Promise => { + try { + const hexRadix = 16 + const decRadix = 10 + const pad = 2 + + // Convert to byte + const encoder = new TextEncoder() + const data = encoder.encode(input) + + // Use crypto of navigator + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + + // Convert to hex + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray + .map((b) => b.toString(hexRadix).padStart(pad, '0')) + .join('') + + // Convert to dec + const decimal = BigInt('0x' + hashHex).toString(decRadix) + + return decimal + } catch (error) { + logger.error(error) + return undefined + } +}