Teams Anrufliste mit Graph

Diese Seite beschreibt den Zugang zu den Teams Call Logs über Graph. Anhand eines WebService und etwas Powershell bekomme ich die Daten in "neartime" eigene Auswertungen.

Muss es Graph sein?

Auf der Seite Teams Verbindungsdaten habe ich die verschiedenen Zugänge zu den Teams Anrufdatensätzen beschrieben. Früher konnte ich mit Get-CSUserSessoin die Aktivitäten eines Anwender über einen Zeitraum erfragen. Dieser Weg ist aber abgekündigt.


Quelle: https://docs.microsoft.com/en-us/powershell/module/skype/get-csusersession?view=skype-ps

Auch gibt es keinen direkten Weg per Teams PowerShell diese Daten zu erhalten, obwohl diese im Teams Admin Center pro Benutzer weiter erreichbar sind.

Die offizielle Lösung ist aktuell der Zugriff per Microsoft Graph.

Über den den "callRecord resource type" kann eine berechtigte App aktuell die PSTN-Anrufe und DirectRouting-Anrufe ermitteln um dann auch die Details dazu abzufragen.

Die passenden Graph-URLs sind dazu

GET https://graph.microsoft.com/v1.0/communications/callRecords/getPstnCalls(fromDateTime=2019-11-01,toDateTime=2019-12-01)
GET https://graph.microsoft.com/v1.0/communications/callRecords/getDirectRoutingCalls(fromDateTime=2019-11-01,toDateTime=2019-12-01)

Es kommen aber nur immer die ersten 1000 Elemente zurück und in der Antwort steht ein "@odata.NextLink"-Attribute, um die nächsten Elemente zu laden.

Achtung:
Allerdings sehen Sie über den Weg keine Meetings und keine Federation-Calls. Hierfür gibt es anscheinend noch keinen Endpunkt um diese Verbindungen in Erfahrung zu bringen

Der Zugriff auf diese Endpunkte kann nur eine Application bekommen.


Quelle: https://docs.microsoft.com/en-us/graph/api/callrecords-callrecord-get

Ihre App muss ich gegenüber Graph mit einer AppID und Credentials (Kennwort / Zertifikat) ausweisen und der Administrator die Berechtigungen gewährt haben.

Authentifizierung

Also habe ich mir meine PowerShell geschnappt, eine AzureApp-Registration mit Permissions und Client-Secret angelegt und hinterlegt:

$LoginUrl="https://login.microsoftonline.com"  # Graph API URLs
$ResourceUrl="https://graph.microsoft.com"     # Ressource API URLs
$TenantName="msxfaq.onmicrosoft.com"
$AppID="hier muss die GUID ihrer AppRegistration rein"
$Appkey="hier muss dann das Kennwort der AppRegistration rein"

Mit den Daten habe ich mir dann ein Authentication Token beschafft:

$Request = @{
    "api-version" ="1.0"
    client_id     = $AppID
    client_secret = $appkey
    scope         = "https://graph.microsoft.com/.default"
    grant_type    = "client_credentials"
}

$authresponse=Invoke-RestMethod `
           -Method POST `
           -Uri "$($LoginUrl)/$($tenantName)/oauth2/v2.0/token" `
           -ContentType "application/x-www-form-urlencoded" `
           -Body $RequestBody `
           -UseBasicParsing `
           -$body $body


$accesstoken= $authresponse.access_token

Wer mag, kann das "accesstoken" ja auf https://jwt.io decodieren lassen.

Details zu einer CallID

Ich habe mir dann aus dem Admin-Portal eine CallID geschnappt und diese direkt abgerufen.

$call=Invoke-RestMethod `
   -Method GET `
   -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
   -URI "https://graph.microsoft.com/v1.0/communications/<callRecords/hier-die-call-id-einsetzen>"

$call

@odata.context       : https://graph.microsoft.com/v1.0/$metadata#communications/callRecords/$entity
id                   : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
version              : 2
type                 : peerToPeer
modalities           : {audio}
lastModifiedDateTime : 21.09.2021 08:52:19
startDateTime        : 21.09.2021 08:35:33
endDateTime          : 21.09.2021 08:40:41
joinWebUrl           :
organizer            : @{acsUser=; spoolUser=; phone=; guest=; encrypted=; On-Premises=; acsApplicationInstance=; spoolApplicationInstance=; applicationInstance=;
                       application=; device=; user=}
participants         : {@{acsUser=; spoolUser=; phone=; guest=; encrypted=; On-Premises=; acsApplicationInstance=; spoolApplicationInstance=; applicationInstance=;
                       application=; device=; user=}, @{acsUser=; spoolUser=; phone=; guest=; encrypted=; On-Premises=; acsApplicationInstance=;
                       spoolApplicationInstance=; applicationInstance=; application=; device=; user=}}

$call.participants

acsUser                  :
spoolUser                :
phone                    :
guest                    :
encrypted                :
On-Premises               :
acsApplicationInstance   :
spoolApplicationInstance :
applicationInstance      :
application              :
device                   :
user                     : @{id=<userid1>; displayName=User1; tenantId=<tenantid>}

acsUser                  :
spoolUser                :
phone                    :
guest                    :
encrypted                :
On-Premises               :
acsApplicationInstance   :
spoolApplicationInstance :
applicationInstance      :
application              :
device                   :
user                     : @{id=<userid1>; displayName=User2; tenantId=<tenantid>}

Wenn ich eine CallID habe, kann ich so sehr einfach zumindest grundlegende Informationen abfragen. Über den gleichen Weg kann ich auch eine Konferenz abfragen.

Liste der Calls

Nun wird es aber kniffliger. Wie komme ich an die Liste der Anrufe? Im Teams Admin-Portal kann ich nur pro Benutzer die Calls der letzten 30 Tage ermitteln. Einen Weg die Daten per Teams Powershell gibt es wohl nicht und das frühere "Get-CSUserSession" ist abgekündigt. Es gibt aber zwei Aufrufe, die zumindest für PSTN-Calls und Direct Routing die Daten ermitteln.

Das App-Token habe ich ja schon, so dass der nächste Request schnell und einfach angefordert wurde.

$drcalls=Invoke-RestMethod `
    -Method GET `
    -Headers @{"Authorization" = "Bearer $($accesstoken)"} `
    -URI "https://graph.microsoft.com/v1.0/communications/callRecords/getDirectRoutingCalls(fromDateTime=2021-09-15,toDateTime=2021-09-18)"

$drcalls| fl

@odata.context : https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.callRecords.directRoutingLogRow)
@odata.count : 641
value : {@{xxxxxxxxxxxx}…}

Sie sehen hier eine Rückgabe von 641 Datensätze, die sich unter "values" verstecken. Hier mal die Details eines Calls.

$drcalls.value[1]

id                            : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
correlationId                 : yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
userId                        : zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz
userPrincipalName             : user1@msxfaq.de
userDisplayName               : User1
startDateTime                 : 15.09.2021 05:37:13
inviteDateTime                : 15.09.2021 05:37:09
failureDateTime               : 01.01.0001 00:00:00
endDateTime                   : 15.09.2021 05:55:38
duration                      : 1105
callType                      : ByotIn
successfulCall                : True
callerNumber                  : +4952513046
calleeNumber                  : +49525793880
mediaPathLocation             : EUNO
signalingLocation             : EUNO
finalSipCode                  : 0
callEndSubReason              : 540000
finalSipCodePhrase            : BYE
trunkFullyQualifiedDomainName : nawsbc.msxfaq.de
mediaBypassEnabled            : False

Dies ist ein Direct Routing Call. Sie können gut den Start und Endezeitpunkt sehen (UTC), die Rufnummern. Ich habe mir mal die Felder genauer anschaut, indem ich per "Group-Object" aus diesen 641 Anrufen zusammengefasst habe:

Feld Gefundene Werte Beschreibung

CallType

ByotIn
ByotInUcap
ByotOut
ByotOutUcap
ByotOutUserForwarding

Byot kürze ich als "Bring your own trunk" ab und zeigt die verschiedenen Status einer Verbindung.

mediaPathLocation

EUNO
EUWE

Ich hatte nur Anrufe aus "Europa und damit wurden nur die beiden Zugänge in Europa NO und WE als Mediarelay genutzt.

signalingLocation

EUNO
EUWE
JAEA

Die gleiche Information gibt es auch noch mal für die Signalisierung per SIP bei DirectRouting.

Alle JAEA-Calls zeichneten sich durch einen SIP-Error 504 und folgender Meldung aus:

finalSipCodePhrase: Unable to deliver INVITE: A connection attempt failed because the
connected party did not properly respond after a period of time,
or established connection failed because connected host has failed to respond.

finalSipCode

0
403
4008
480
486
487
500
504
603

Hier habe ich jede Menge unterschiedliche SIP-Statuscodes gefunden.

Auf diesen Daten kann man schon eine erste Auswertung für Telefonie-Anrufe starten, die per Microsoft Rufnummern (PSTN-Call) oder Direct Routing bzw. Operator Connect ankommen. Allerdings sehe ich damit natürlich keine internen oder per Federation geführten Teams-Anrufe oder Meetings.

Realtime?

Allerdings ist ein "Polling" immer mit einer gewissen Last und Verzögerung verbunden. Es ist natürlich die einfachste Art der Programmierung aber wenn Sie schnell informiert sein müssen, dann brauchen Sie einen Webhook, d.h. ihr Software bittet Graph um einen Rückruf. Dazu müssen Sie selbst erst einmal einen per HTTPS erreichbaren Webservice im Internet bereitstellen und dann die URL an Graph übergeben. Dann wird Microsoft Graph jeden neuen Datensatz als HTTP-Post mit der Call-ID an ihren Webservice melden, so dass Sie dann die Details dazu abholen können.

Organizations can subscribe to changes to call records using the Microsoft Graph webhook subscriptions capability, allowing them to build near-real-time reports from the data or to alert on certain scenarios like emergency calls. ...
... This push-model enables organizations and partners to build their own real-time reporting solutions.
Quelle: https://docs.microsoft.com/en-us/graph/cloud-communications-callrecords

Weitere Links