From 192bf73779864e643170c70d71496167121be58c Mon Sep 17 00:00:00 2001 From: Marcos Cereijo Date: Wed, 10 Dec 2025 18:03:33 +0100 Subject: [PATCH 1/6] feat: avoid using pins for interpretation rooms --- README.md | 142 ++++++++++++++---- docs/call_tag.puml | 1 + docs/call_tag.svg | 1 + .../InterpretationContext.tsx | 84 +++++++---- src/utils.ts | 31 ++++ 5 files changed, 199 insertions(+), 60 deletions(-) create mode 100644 docs/call_tag.puml create mode 100644 docs/call_tag.svg diff --git a/README.md b/README.md index 08c726f..7e3ec93 100644 --- a/README.md +++ b/README.md @@ -188,39 +188,127 @@ $ 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: + - `pin` + - `guest_pin` + - `local_alias` + 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`. + +
+ +![Call Tag Sequence Diagram](./docs/call_tag.svg) + +
+ +### 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 = (pin + guest_pin + (call_info.local_alias | pex_regex_replace('^(\d{2})(\d{4})$', '\\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 +{# 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})$', '') == '') %} + {% set callTag = (pin + guest_pin + call_info.local_alias) | pex_hash | pex_tail(20) %} + + { + "status": "success", + "action": "continue", + "result": { + "call_tag": "{{callTag}}" + } + } + +{% else %} + + { + "status": "success", + "action": "continue", + "result": {} + } + +{% endif %} ``` ## Build for production diff --git a/docs/call_tag.puml b/docs/call_tag.puml new file mode 100644 index 0000000..a2b91a4 --- /dev/null +++ b/docs/call_tag.puml @@ -0,0 +1 @@ +@startuml participant Webapp3 participant Plugin participant Infinity Webapp3 -> Infinity: Join main conference {pin: } Infinity -> Webapp3: Joined { call_tag: pexHash(pin + guest_pin + local_alias).tail(20) } Plugin -> Infinity: Join interpretation {pin: pexHash(call_tag + role).tail(20)} Infinity -> Infinity: Check PIN:\npin: pexHash(call_tag + 'interpreter').tail(20),\nguest_pin: pexHash(call_tag + 'listener').tail(20) Infinity -> Plugin: Joined @enduml \ No newline at end of file diff --git a/docs/call_tag.svg b/docs/call_tag.svg new file mode 100644 index 0000000..e7dca37 --- /dev/null +++ b/docs/call_tag.svg @@ -0,0 +1 @@ +Webapp3PluginInfinityWebapp3Webapp3PluginPluginInfinityInfinityJoin main conference {pin: <vmr_pin>}Joined { call_tag: pexHash(pin + guest_pin + local_alias).tail(20) }Join interpretation {pin: pexHash(call_tag + role).tail(20)}Check PIN:pin: pexHash(call_tag + 'interpreter').tail(20),guest_pin: pexHash(call_tag + 'listener').tail(20)Joined \ No newline at end of file diff --git a/src/InterpretationContext/InterpretationContext.tsx b/src/InterpretationContext/InterpretationContext.tsx index 8e1a4d2..9c4042b 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,29 @@ 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 + logger.info(getUser().rawData.call_tag) + + 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 +245,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 + } +} From db017a5f9c753e13b2dbcd16ecf118980b494222 Mon Sep 17 00:00:00 2001 From: Marcos Cereijo Date: Fri, 12 Dec 2025 16:39:07 +0100 Subject: [PATCH 2/6] docs: make some corrections to the local policy in the readme --- README.md | 58 ++++++++++++++++++++++++++++++++++++++++------ docs/call_tag.puml | 13 ++++++++++- docs/call_tag.svg | 2 +- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7e3ec93..8cc939c 100644 --- a/README.md +++ b/README.md @@ -243,10 +243,10 @@ The process is as follows: } } -{% elif (call_info.local_alias | pex_regex_replace('^(\d{6})$', '') == '') %} +{% elif (call_info.local_alias | pex_regex_replace('^(\d{6})$', '') == '') %} {# Interpretation rooms for 6-digit VMRs #} - {% set callTag = (pin + guest_pin + (call_info.local_alias | pex_regex_replace('^(\d{2})(\d{4})$', '\\1') )) | pex_hash | pex_tail(20) %} + {% set callTag = ((call_info.local_alias | pex_regex_replace('^(\d{2})(\d{4})$', '\\1') ) + (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) %} @@ -283,14 +283,10 @@ The process is as follows: ### Participant policy ```python -{# 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})$', '') == '') %} - {% set callTag = (pin + guest_pin + call_info.local_alias) | pex_hash | pex_tail(20) %} + {% set callTag = (call_info.local_alias + call_info.remote_display_name) | pex_hash | pex_tail(20) %} { "status": "success", @@ -311,6 +307,54 @@ The process is as follows: {% 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/docs/call_tag.puml b/docs/call_tag.puml index a2b91a4..2300aff 100644 --- a/docs/call_tag.puml +++ b/docs/call_tag.puml @@ -1 +1,12 @@ -@startuml participant Webapp3 participant Plugin participant Infinity Webapp3 -> Infinity: Join main conference {pin: } Infinity -> Webapp3: Joined { call_tag: pexHash(pin + guest_pin + local_alias).tail(20) } Plugin -> Infinity: Join interpretation {pin: pexHash(call_tag + role).tail(20)} Infinity -> Infinity: Check PIN:\npin: pexHash(call_tag + 'interpreter').tail(20),\nguest_pin: pexHash(call_tag + 'listener').tail(20) Infinity -> Plugin: Joined @enduml \ No newline at end of file +@startuml + +participant Webapp3 +participant Plugin +participant Infinity + +Webapp3 -> Infinity: Join main conference {pin: } +Infinity -> Webapp3: Joined { call_tag: pexHash(local_alias + display_name).tail(20) } +Plugin -> Infinity: Join interpretation {pin: pexHash(call_tag + role).tail(20)} +Infinity -> Infinity: Check PIN:\npin: pexHash(call_tag + 'interpreter').tail(20),\nguest_pin: pexHash(call_tag + 'listener').tail(20) +Infinity -> Plugin: Joined +@enduml \ No newline at end of file diff --git a/docs/call_tag.svg b/docs/call_tag.svg index e7dca37..64cdd30 100644 --- a/docs/call_tag.svg +++ b/docs/call_tag.svg @@ -1 +1 @@ -Webapp3PluginInfinityWebapp3Webapp3PluginPluginInfinityInfinityJoin main conference {pin: <vmr_pin>}Joined { call_tag: pexHash(pin + guest_pin + local_alias).tail(20) }Join interpretation {pin: pexHash(call_tag + role).tail(20)}Check PIN:pin: pexHash(call_tag + 'interpreter').tail(20),guest_pin: pexHash(call_tag + 'listener').tail(20)Joined \ No newline at end of file +Webapp3PluginInfinityWebapp3Webapp3PluginPluginInfinityInfinityJoin main conference {pin: <vmr_pin>}Joined { call_tag: pexHash(local_alias + display_name).tail(20) }Join interpretation {pin: pexHash(call_tag + role).tail(20)}Check PIN:pin: pexHash(call_tag + 'interpreter').tail(20),guest_pin: pexHash(call_tag + 'listener').tail(20)Joined \ No newline at end of file From e9c2b076da48f27d61f8739a3a53133407aaa5cc Mon Sep 17 00:00:00 2001 From: Marcos Cereijo Date: Fri, 12 Dec 2025 16:45:56 +0100 Subject: [PATCH 3/6] style: remove logging --- src/InterpretationContext/InterpretationContext.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/InterpretationContext/InterpretationContext.tsx b/src/InterpretationContext/InterpretationContext.tsx index 9c4042b..a5f3932 100644 --- a/src/InterpretationContext/InterpretationContext.tsx +++ b/src/InterpretationContext/InterpretationContext.tsx @@ -108,7 +108,6 @@ export const InterpretationContextProvider = (props: { try { const conferenceAlias = getMainConferenceAlias() + language.code const bandwidth = 0 - logger.info(getUser().rawData.call_tag) const { rawData } = getUser() const { call_tag: callTag } = rawData From fffb17284bcd237feb2a2077b1a5dde4d35de1ad Mon Sep 17 00:00:00 2001 From: Marcos Cereijo Date: Tue, 20 Jan 2026 10:29:28 +0100 Subject: [PATCH 4/6] docs: add mermaid diagram directly to the README --- README.md | 17 ++++++++++++----- docs/call_tag.puml | 12 ------------ docs/call_tag.svg | 1 - 3 files changed, 12 insertions(+), 18 deletions(-) delete mode 100644 docs/call_tag.puml delete mode 100644 docs/call_tag.svg diff --git a/README.md b/README.md index 8cc939c..2d867ea 100644 --- a/README.md +++ b/README.md @@ -211,11 +211,18 @@ The process is as follows: using the same process. It will choose the role `interpreter` for the `pin` and `listener` for the `guest_pin`. -
- -![Call Tag Sequence Diagram](./docs/call_tag.svg) - -
+```mermaid +sequenceDiagram +participant Webapp3 +participant Plugin +participant Infinity + +Webapp3 ->> Infinity: Join main conference {pin: } +Infinity -->> Webapp3: Joined { call_tag: pexHash(local_alias + 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 diff --git a/docs/call_tag.puml b/docs/call_tag.puml deleted file mode 100644 index 2300aff..0000000 --- a/docs/call_tag.puml +++ /dev/null @@ -1,12 +0,0 @@ -@startuml - -participant Webapp3 -participant Plugin -participant Infinity - -Webapp3 -> Infinity: Join main conference {pin: } -Infinity -> Webapp3: Joined { call_tag: pexHash(local_alias + display_name).tail(20) } -Plugin -> Infinity: Join interpretation {pin: pexHash(call_tag + role).tail(20)} -Infinity -> Infinity: Check PIN:\npin: pexHash(call_tag + 'interpreter').tail(20),\nguest_pin: pexHash(call_tag + 'listener').tail(20) -Infinity -> Plugin: Joined -@enduml \ No newline at end of file diff --git a/docs/call_tag.svg b/docs/call_tag.svg deleted file mode 100644 index 64cdd30..0000000 --- a/docs/call_tag.svg +++ /dev/null @@ -1 +0,0 @@ -Webapp3PluginInfinityWebapp3Webapp3PluginPluginInfinityInfinityJoin main conference {pin: <vmr_pin>}Joined { call_tag: pexHash(local_alias + display_name).tail(20) }Join interpretation {pin: pexHash(call_tag + role).tail(20)}Check PIN:pin: pexHash(call_tag + 'interpreter').tail(20),guest_pin: pexHash(call_tag + 'listener').tail(20)Joined \ No newline at end of file From de60bd5ee2ab255bdd86856cd7d43f9eb117f2eb Mon Sep 17 00:00:00 2001 From: Marcos Cereijo Date: Thu, 22 Jan 2026 17:31:12 +0100 Subject: [PATCH 5/6] docs: correct explanation about how to create the callTag --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 2d867ea..a98da85 100644 --- a/README.md +++ b/README.md @@ -195,9 +195,8 @@ 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: - - `pin` - - `guest_pin` - `local_alias` + - `display_name` 2. Apply `pex_hash` to the concatenated string. 3. Take only the last 20 digits of the result. From 95ad915b0ff1639c8f0570f65789f1353730b76a Mon Sep 17 00:00:00 2001 From: Marcos Cereijo Date: Fri, 30 Jan 2026 14:40:12 +0100 Subject: [PATCH 6/6] chore(auth): add the vendor when generating the callTag --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a98da85..8fb519c 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,7 @@ The process is as follows: 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. @@ -217,7 +218,7 @@ participant Plugin participant Infinity Webapp3 ->> Infinity: Join main conference {pin: } -Infinity -->> Webapp3: Joined { call_tag: pexHash(local_alias + display_name).tail(20) } +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 @@ -252,7 +253,7 @@ Infinity -->> Plugin: Joined {% 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.remote_display_name | pex_regex_replace('^(.*)\ -\ (Interpreter|Listener)$', '\\1') )) | pex_hash | pex_tail(20) %} + {% 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) %} @@ -291,8 +292,11 @@ Infinity -->> Plugin: Joined ```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 + call_info.remote_display_name) | pex_hash | pex_tail(20) %} + {% set callTag = (call_info.local_alias + vendor + call_info.remote_display_name) | pex_hash | pex_tail(20) %} { "status": "success",