The recent posts about setting Teams presence and being notified about Teams presence changes are the building blocks to synchronize the presence state between MS Teams and our 3CX PBX both ways.
- a call on Teams sets the 3CX presence state to "Away" if it was "Available" before
- if Teams becomes "Available" and 3CX state is "Away" => set 3CX to "Available"
- a call on 3CX tries to set the Teams presence "Busy" - there is some logic behind the scenes, we can't control about overriding "Offline" and "Away" in Teams which comes in handy in this case
In order to interact with the 3CX PBX there is an inoffical call flow API for v16 which has been adopted by the 3cx-web-api project. Apart from TAPI and CRM integrations this seems to be the only way to react to call states as well as set the presence state.
The API is written in C# using the .Net 5.0 framework since 3CX is a .Net application. It would have been nice to incorporate the presence functionality I coded in Bash into the 3cx-web-api. Unfortunately my coding skills are somewhat basic, especially when it comes to MVC design patterns which are used throughout all Microsoft identity sample projects.
I did manage to extend the 3cx-web-api to handle the notification webhook. That spares the use of the rudimentary webserver+socat of the previous post.
In order to direct traffic to the API I modified the /etc/nginx/sites-enabled/3cxpbx (normally a sym-link) and added an upstream and location setting:
... upstream webapi { server 127.0.0.1:1234; } ... server { ... location ~ ^/secret-url-teams-presence-notification { proxy_pass http://webapi; } ...
Note: in this setup the path-name is the only "secret", preventing anybody to tamper with your PBX!!!
After building the 3cx-web-api according to the README.md, the StdOut of the program can be used to handle presence changes in a Bash script:
#!/bin/bash declare -A users # listen port to start the WebApi on port=1234 # extension to O365 user ID mapping users["101"]='user1@example.org' users["102"]='user2@example.org' # status refresh time in seconds refresh=10 # regex to match state and extension busy_match="^\s*ID=.*;S=(Dialing|Connected);DN=(.*);Queue_Name=" # path to teams_presence_notification.sh TEAMS_NOTIFY="./teams_presence_notification.sh" # path to set_teams_presence.sh TEAMS_PRESENCE="./set_teams_presence.sh" # those paths are used to exchange data between teams_presence_notification.sh key_file="/tmp/azure.key" user_file="/tmp/azure.users" if [ ! -f "${user_file}" ] ; then touch "${user_file}" fi declare -A extensions declare -A statechange userlist="" # initialize some state data for i in "${!users[@]}" ; do statechange[$i]=0 extensions["${users[$i]}"]=$i userlist="${userlist} ${users[$i]}" done echo "You may now run:" echo " ${TEAMS_NOTIFY}${userlist}" update=$(date +%s) # loop over StdOutput of the call processing API while read line ; do now=$(date +%s) # exit if the API server received a /stop command if [[ "$line" =~ "Server Stop" ]] ; then exit 0 # process notification lifecycleEvent elif [[ "$line" =~ "subscriptionRemoved" ]] ; then echo "recreating subscription" ${TEAMS_NOTIFY} "${userlist}" # process notification lifecycleEvent elif [[ "$line" =~ "reauthorizationRequired" ]] ; then echo "reauthorizing subscription" ${TEAMS_NOTIFY} reauthorize $(echo $line | jq -r '.value[0].subscriptionId') # process notification lifecycleEvent elif [[ "$line" =~ "missed" ]] ; then echo $line # process notification webhook elif [[ "$line" =~ "subscriptionId" ]] ; then # decrypt encryption key with private key key=$(echo $line | jq -r '.value[0].encryptedContent.dataKey' | base64 -d | \ openssl rsautl -decrypt -inkey "${key_file}" -oaep | hexdump -e '16/1 "%02x"') # iv is first 32 bytes of encryption key iv=$(echo $key | head --bytes 32) # decrypt data payload json=$(echo $line | jq -r '.value[] | .encryptedContent.data' | base64 -d | \ openssl enc -d -aes-256-cbc -K $key -iv $iv) # extract Azure-UserId from json userid=$(echo "$json" | jq -r '.id') if [ -n "$userid" ] ; then # the user - userId should have been placed in a "user_file" when the notification was created # since the userId is static, it saves another API lookup call user=$(cat "${user_file}" | grep "${userid}" | cut -d' ' -f1) if [ -n "$user" ] ; then # if the user is configured with an extension if [ -n "${extensions[${user}]}" ] ; then # and a state-change did not occur within $refresh period if [ $((now - statechange[${extensions[${user}]}])) -gt $((15 * refresh / 10)) ] ; then # retrieve the Teams availability status status=$(echo "$json" | jq -r '.availability') # retrieve 3CX availability status localstatus=$(curl -s http://localhost:${port}/showstatus/${extensions[${user}]}) if [[ "$status" =~ "Available" ]] && [[ "${localstatus}" =~ "Away" ]] ; then echo "Setting extension <${extensions[${user}]}> Available (was ${localstatus##*=})" curl -s http://localhost:${port}/setstatus/${extensions[${user}]}/avail > /dev/null elif [[ "$status" =~ "Busy"|"DoNotDisturb"|"BeRightBack" ]] && [[ "${localstatus}" =~ "Available" ]] ; then echo "Setting extension <${extensions[${user}]}> Away (was ${localstatus##*=})" curl -s http://localhost:${port}/setstatus/${extensions[${user}]}/away > /dev/null else echo "UPN: ${user} - Ext: ${extensions[${user}]} - Teams: $status - 3cx: ${localstatus##*=}" fi fi else echo "User <${user}> is not configured with an extension" fi fi else echo "Error parsing response JSON:" echo $json fi # check if a call is being set up or already connected elif [[ "$line" =~ $busy_match ]] ; then ext=${BASH_REMATCH[2]} # process further if the extension is in the user mapping if [ -n "${users[$ext]}" ] ; then # has there been a state change during the last refresh period? if [ $((now - statechange[$ext])) -gt $((15 * refresh / 10)) ] ; then echo "Setting user <${users[$ext]}> busy" ${TEAMS_PRESENCE} "${users[$ext]}" busy fi statechange[$ext]=$now fi elif [[ "$line" =~ (<html>|Listening) ]] ; then # set any users to "available" who are still busy but not in the active call list for i in "${!statechange[@]}" ; do # extension is not currently in a call, but the time it was is less than 1.5 * refresh period if [ $((now - statechange[$i])) -gt 2 -a $((now - statechange[$i])) -lt $((15 * refresh / 10)) ] ; then echo "Setting user <${users[$i]}> available" ${TEAMS_PRESENCE} "${users[$i]}" available fi done # schedule an update on CallStatus (sleep $refresh ; curl -s http://localhost:${port}/showallcalls > /dev/null )& fi done < <(dotnet 3cx-web-API-Master/bin/Debug/net5.0/WebAPICore.dll $port)The files from the previous two posts handle the Microsoft communication.
Keine Kommentare:
Kommentar veröffentlichen