Donnerstag, 27. April 2023

Teams Presence synchronization for a 3CX on premises

 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 {
        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, the StdOut of the program can be used to handle presence changes in a Bash script:


declare -A users

# listen port to start the WebApi on

# extension to O365 user ID mapping

# status refresh time in seconds

# regex to match state and extension

# path to

# path to

# those paths are used to exchange data between

if [ ! -f "${user_file}" ] ; then
  touch "${user_file}"

declare -A extensions
declare -A statechange

# initialize some state data
for i in "${!users[@]}" ; do
  userlist="${userlist} ${users[$i]}"

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[] |' | 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
              echo "UPN: ${user} - Ext: ${extensions[${user}]} - Teams: $status - 3cx: ${localstatus##*=}"
          echo "User <${user}> is not configured with an extension"
      echo "Error parsing response JSON:"
      echo $json
  # check if a call is being set up or already connected
  elif [[ "$line" =~ $busy_match ]] ; then
    # 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
  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
    # schedule an update on CallStatus
    (sleep $refresh ; curl -s http://localhost:${port}/showallcalls > /dev/null )&
done < <(dotnet 3cx-web-API-Master/bin/Debug/net5.0/WebAPICore.dll $port)
The files from the previous two posts handle the Microsoft communication.

3 Kommentare:

  1. I have attempted to replicate your scripts and i think i have it setup correctly with the MS Teams auth files. When i run the script above it just immediately ends and no looping of the StdOutput takes place. I am running this on the 3cx machine itself on Debian. If you can give me some advice i would appreciate it. I want to do exactly what you describe for my users and i would love to get this working on my 3cx.

    1. Hi HP, could we possibly take this to Github:

      Comments here tend to get messy ;)

  2. Thank you for replying Alexander. I can do that but the issues I seem to be having aren't specifically related to the API, which works well and is able to run properly. I need help to figure out why StdOutput doesn't run when the script above is launched and likely with the MS Teams authentication part too. Okay for me to open an issue at Github on that basis?