netgescon-day0/scripts/ops/windows/watch-netgescon-panasonic-tapi-dotnet-events.ps1

1417 lines
44 KiB
PowerShell

param(
[string]$Extensions = "201,205,206",
[string]$LogFile = ".\netgescon-tapi-dotnet-events.log",
[int]$Seconds = 0,
[string]$InteropOutputDir = '',
[ValidateSet('none', 'dry-run', 'live')]
[string]$BridgeMode = 'none',
[string]$BaseUrl = '',
[string]$Token = '',
[switch]$IncludeDiagnosticEvents
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($InteropOutputDir)) {
if (-not [string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
$InteropOutputDir = Join-Path $env:LOCALAPPDATA 'NetGescon\tapi-interop'
}
else {
$InteropOutputDir = Join-Path $PSScriptRoot 'tapi-interop'
}
}
elseif (-not [System.IO.Path]::IsPathRooted($InteropOutputDir)) {
$InteropOutputDir = Join-Path $PSScriptRoot $InteropOutputDir
}
Add-Type -AssemblyName System.Runtime.InteropServices
$TapiMediaTypeAudio = 8
$TapiEventFilterCallNotification = 4
$TapiEventFilterCallState = 8
$TapiEventFilterCallMedia = 16
$TapiEventFilterCallHub = 32
$TapiEventFilterCallInfoChange = 64
$TapiEventFilterPrivate = 128
$CallStateIdle = 0
$CallStateInProgress = 1
$CallStateConnected = 2
$CallStateDisconnected = 3
$CallStateOffering = 4
$CallStateHold = 5
$CallStateQueued = 6
function Get-SafeProperty {
param(
[Parameter(Mandatory = $true)] $Object,
[Parameter(Mandatory = $true)] [string]$Name
)
if ($null -eq $Object) {
return $null
}
try {
return $Object.$Name
}
catch {
return $null
}
}
function Get-SafeTypeName {
param($Object)
if ($null -eq $Object) {
return '<null>'
}
try {
$type = $Object.GetType()
if ($null -eq $type) {
return '<unknown>'
}
return [string]$type.FullName
}
catch {
return '<type-error>'
}
}
function Convert-ToSafeString {
param($Value)
if ($null -eq $Value) {
return ''
}
try {
return [string]$Value
}
catch {
return '<string-error>'
}
}
function Get-FirstNonEmptyProperty {
param(
$Object,
[Parameter(Mandatory = $true)] [string[]]$Names
)
if ($null -eq $Object) {
return ''
}
foreach ($name in $Names) {
$value = Get-SafeProperty -Object $Object -Name $name
$valueString = Convert-ToSafeString -Value $value
if ($valueString -ne '') {
return $valueString
}
}
return ''
}
function Normalize-PhoneLikeValue {
param([string]$Value)
if ($null -eq $Value) {
return ''
}
$trimmed = $Value.Trim()
if ($trimmed -eq '') {
return ''
}
return ($trimmed -replace '[^\d\+]', '')
}
function Expand-ExtensionToken {
param([string]$Value)
$text = Convert-ToSafeString -Value $Value
if ($text -eq '') {
return @()
}
if ($text -match '^(\d+)-(\d+)$') {
$start = [int]$matches[1]
$end = [int]$matches[2]
if ($end -lt $start) {
$swap = $start
$start = $end
$end = $swap
}
$expanded = @()
for ($index = $start; $index -le $end; $index++) {
$expanded += [string]$index
}
return $expanded
}
return @($text)
}
function Parse-WatchList {
param([string]$ExtensionsValue)
$watchItems = New-Object System.Collections.Generic.List[string]
foreach ($rawToken in @($ExtensionsValue.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })) {
foreach ($expandedToken in @(Expand-ExtensionToken -Value $rawToken)) {
$token = Convert-ToSafeString -Value $expandedToken
if ($token -ne '' -and -not $watchItems.Contains($token)) {
$watchItems.Add($token)
}
}
}
return @($watchItems)
}
function Is-WatchedExtensionValue {
param(
[string]$Value,
[Parameter(Mandatory = $true)] [string[]]$WatchList
)
$token = Normalize-ExtensionToken -Value $Value
if ($token -eq '') {
return $false
}
return $WatchList -contains $token
}
function Select-ExternalPhoneCandidate {
param(
[string[]]$Candidates,
[Parameter(Mandatory = $true)] [string[]]$WatchList
)
if ($null -eq $Candidates) {
return ''
}
foreach ($candidate in $Candidates) {
$normalized = Normalize-PhoneLikeValue -Value $candidate
if ($normalized -eq '') {
continue
}
if (-not (Is-WatchedExtensionValue -Value $normalized -WatchList $WatchList)) {
return $normalized
}
}
return ''
}
function Convert-ComCollectionToArray {
param($Collection)
$items = @()
if ($null -eq $Collection) {
return $items
}
try {
foreach ($item in $Collection) {
$items += $item
}
}
catch {
}
return $items
}
function Normalize-ExtensionToken {
param([string]$Value)
if ($null -eq $Value) {
return ''
}
$trimmed = ([string]$Value).Trim()
if ($trimmed -eq '') {
return ''
}
if ($trimmed -match '(\d+)$') {
return $matches[1].TrimStart('0')
}
return $trimmed
}
function Get-AddressCategory {
param([string]$AddressName)
if ($null -eq $AddressName) {
return ''
}
$trimmed = $AddressName.Trim().ToUpperInvariant()
if ($trimmed -match '^([A-Z]+)') {
return $matches[1]
}
return ''
}
function Convert-ToNullableInt {
param([string]$Value)
if ($null -eq $Value) {
return $null
}
$digits = ($Value -replace '\D+', '')
if ($digits -eq '') {
return $null
}
try {
return [int]$digits
}
catch {
return $null
}
}
function Should-WatchAddress {
param(
[Parameter(Mandatory = $true)] $Address,
[Parameter(Mandatory = $true)] [string[]]$WatchList
)
$rawDial = Convert-ToSafeString -Value (Get-SafeProperty -Object $Address -Name 'DialableAddress')
$rawName = Convert-ToSafeString -Value (Get-SafeProperty -Object $Address -Name 'AddressName')
$dial = Normalize-ExtensionToken -Value $rawDial
$name = Normalize-ExtensionToken -Value $rawName
$category = Get-AddressCategory -AddressName $rawName
foreach ($watch in $WatchList) {
$watchRaw = Convert-ToSafeString -Value $watch
$token = Normalize-ExtensionToken -Value $watchRaw
if ($token -eq '') {
continue
}
if ($watchRaw -match '^0\d+$') {
if ($category -ne 'CO') {
continue
}
$watchInt = Convert-ToNullableInt -Value $watchRaw
$nameInt = Convert-ToNullableInt -Value $rawName
$dialInt = Convert-ToNullableInt -Value $rawDial
if (($null -ne $watchInt -and $null -ne $nameInt -and $watchInt -eq $nameInt) -or ($null -ne $watchInt -and $null -ne $dialInt -and $watchInt -eq $dialInt)) {
return $true
}
continue
}
if ($dial -eq $token -or $name -eq $token) {
return $true
}
}
return $false
}
function Get-BridgeWatchMode {
param([Parameter(Mandatory = $true)] [string[]]$WatchList)
foreach ($watch in $WatchList) {
$value = Convert-ToSafeString -Value $watch
if ($value -match '^6\d\d$') {
return 'group-first'
}
}
foreach ($watch in $WatchList) {
$value = Convert-ToSafeString -Value $watch
if ($value -match '^0\d+$') {
return 'co-first'
}
}
return 'extension-only'
}
function Should-EmitBridgeForSnapshot {
param([Parameter(Mandatory = $true)] $Snapshot)
if ($Snapshot.Direction -ne 'inbound') {
switch ($script:BridgeWatchMode) {
'co-first' {
return $Snapshot.AddressCategory -eq 'CO'
}
'group-first' {
return $Snapshot.AddressCategory -eq 'EXT' -or $Snapshot.AddressCategory -eq 'CO'
}
default {
return $Snapshot.AddressCategory -eq 'EXT'
}
}
}
switch ($script:BridgeWatchMode) {
'group-first' {
return $Snapshot.AddressCategory -eq 'GRP'
}
'co-first' {
return $Snapshot.AddressCategory -eq 'CO'
}
default {
return $Snapshot.AddressCategory -eq 'EXT'
}
}
}
function Should-LogDotNetEvent {
param([string]$EventType)
if ($script:IncludeDiagnosticEvents) {
return $true
}
return $EventType -eq 'TE_CALLNOTIFICATION' -or $EventType -eq 'TE_CALLSTATE'
}
function Resolve-BridgeEventId {
param([string]$EventId)
if ($EventId -eq '') {
return ''
}
if ($script:BridgeEventAliases.ContainsKey($EventId)) {
return [string]$script:BridgeEventAliases[$EventId]
}
return $EventId
}
function Test-BridgeCallRecentlyCompleted {
param([string]$EventId)
if ($EventId -eq '' -or -not $script:CompletedBridgeCalls.ContainsKey($EventId)) {
return $false
}
$completedAt = $script:CompletedBridgeCalls[$EventId]
if ($null -eq $completedAt) {
return $false
}
$ageSeconds = [Math]::Abs((New-TimeSpan -Start $completedAt -End (Get-Date)).TotalSeconds)
if ($ageSeconds -gt 30) {
[void]$script:CompletedBridgeCalls.Remove($EventId)
return $false
}
return $true
}
function Mark-BridgeCallCompleted {
param([string]$EventId)
if ($EventId -ne '') {
$script:CompletedBridgeCalls[$EventId] = Get-Date
}
}
function Find-BridgeCallMatchBySignature {
param([Parameter(Mandatory = $true)] $Snapshot)
if ($Snapshot.Direction -ne 'outbound') {
return $null
}
$snapshotPhone = Convert-ToSafeString -Value $Snapshot.ExternalPhone
$snapshotCallingExtension = Convert-ToSafeString -Value $Snapshot.CallingExtension
$now = Get-Date
foreach ($eventId in @($script:BridgeCalls.Keys)) {
$callRecord = $script:BridgeCalls[$eventId]
if ($null -eq $callRecord) {
continue
}
if ((Convert-ToSafeString -Value $callRecord.Direction) -ne 'outbound') {
continue
}
$ageSeconds = [Math]::Abs((New-TimeSpan -Start $callRecord.StartedAt -End $now).TotalSeconds)
if ($ageSeconds -gt 120) {
continue
}
$recordPhone = Convert-ToSafeString -Value $callRecord.ExternalPhone
if ($snapshotPhone -ne '' -and $recordPhone -ne '' -and $snapshotPhone -ne $recordPhone) {
continue
}
$recordCallingExtension = Convert-ToSafeString -Value $callRecord.CallingExtension
if ($snapshotCallingExtension -ne '' -and $recordCallingExtension -ne '' -and $snapshotCallingExtension -ne $recordCallingExtension) {
continue
}
return [pscustomobject]@{
EventId = [string]$eventId
CallRecord = $callRecord
}
}
return $null
}
function Write-LogLine {
param([string]$Text)
$line = "{0} | {1}" -f (Get-Date).ToString('yyyy-MM-dd HH:mm:ss'), $Text
Write-Host $line
for ($attempt = 1; $attempt -le 3; $attempt++) {
try {
$directory = Split-Path -Parent $script:LogFilePath
if ($directory -and -not (Test-Path $directory)) {
New-Item -ItemType Directory -Force -Path $directory | Out-Null
}
$fileMode = [System.IO.FileMode]::OpenOrCreate
$fileAccess = [System.IO.FileAccess]::Write
$fileShare = [System.IO.FileShare]::ReadWrite
$stream = New-Object System.IO.FileStream($script:LogFilePath, $fileMode, $fileAccess, $fileShare)
try {
$stream.Seek(0, [System.IO.SeekOrigin]::End) | Out-Null
$writer = New-Object System.IO.StreamWriter($stream, [System.Text.UTF8Encoding]::new($false))
try {
$writer.WriteLine($line)
$writer.Flush()
return
}
finally {
$writer.Dispose()
}
}
finally {
$stream.Dispose()
}
}
catch {
if ($attempt -eq 3) {
Write-Host ("[WARN] Log file non scrivibile: {0}" -f $_.Exception.Message)
return
}
Start-Sleep -Milliseconds 150
}
}
}
function Try-RegisterCallNotifications {
param(
[Parameter(Mandatory = $true)] $Tapi,
[Parameter(Mandatory = $true)] $Address
)
try {
$cookie = $Tapi.RegisterCallNotifications($Address, $true, $true, $TapiMediaTypeAudio, 0)
return [pscustomobject]@{
Ok = $true
Cookie = $cookie
Error = ''
}
}
catch {
return [pscustomobject]@{
Ok = $false
Cookie = $null
Error = $_.Exception.Message
}
}
}
function Get-SafeCallInfoString {
param(
$Call,
[Parameter(Mandatory = $true)] [int]$Id
)
if ($null -eq $Call) {
return ''
}
try {
return Convert-ToSafeString -Value ($Call.CallInfoString($Id))
}
catch {
return ''
}
}
function Get-PreferredCallInfoString {
param(
$Call,
[Parameter(Mandatory = $true)] [int[]]$Ids
)
foreach ($id in $Ids) {
$value = Get-SafeCallInfoString -Call $Call -Id $id
if ($value -ne '') {
return $value
}
}
return ''
}
function Get-TypeByName {
param(
[Parameter(Mandatory = $true)] [System.Reflection.Assembly]$Assembly,
[Parameter(Mandatory = $true)] [string]$Name
)
return $Assembly.GetTypes() | Where-Object { $_.Name -eq $Name } | Select-Object -First 1
}
function Get-InteropAssembly {
param([Parameter(Mandatory = $true)] [string]$OutputDir)
Add-Type -TypeDefinition @"
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
public static class NetGesconTapiWatcherNativeMethods
{
[DllImport("oleaut32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
public static extern void LoadTypeLib(string file, out ITypeLib typeLib);
}
public sealed class NetGesconTapiWatcherImporterSink : ITypeLibImporterNotifySink
{
public void ReportEvent(ImporterEventKind eventKind, int eventCode, string eventMsg)
{
}
public Assembly ResolveRef(object typeLib)
{
return null;
}
}
"@
$tapiDll = Join-Path $env:WINDIR 'System32\tapi3.dll'
if (-not (Test-Path $tapiDll)) {
throw "File non trovato: $tapiDll"
}
$outputDirectoryInfo = New-Item -ItemType Directory -Force -Path $OutputDir
$outputDirectory = $outputDirectoryInfo.FullName
$interopFileName = 'Tapi3.Interop.dll'
$interopDll = Join-Path $outputDirectory $interopFileName
$typeLib = $null
[NetGesconTapiWatcherNativeMethods]::LoadTypeLib($tapiDll, [ref]$typeLib)
$converter = New-Object System.Runtime.InteropServices.TypeLibConverter
$sink = New-Object NetGesconTapiWatcherImporterSink
Push-Location $outputDirectory
try {
$assemblyBuilder = $converter.ConvertTypeLibToAssembly(
$typeLib,
$interopFileName,
0,
$sink,
$null,
$null,
$null,
$null
)
try {
$assemblyBuilder.Save($interopFileName)
}
catch {
}
}
finally {
Pop-Location
}
if (Test-Path $interopDll) {
return [System.Reflection.Assembly]::LoadFrom($interopDll)
}
return [System.Reflection.Assembly]$assemblyBuilder
}
function Describe-Address {
param($Address)
if ($null -eq $Address) {
return ''
}
$name = Convert-ToSafeString -Value (Get-SafeProperty -Object $Address -Name 'AddressName')
$dial = Convert-ToSafeString -Value (Get-SafeProperty -Object $Address -Name 'DialableAddress')
$provider = Convert-ToSafeString -Value (Get-SafeProperty -Object $Address -Name 'ServiceProviderName')
return "address=$name dial=$dial provider=$provider"
}
function Describe-Call {
param($Call)
if ($null -eq $Call) {
return ''
}
$parts = @()
foreach ($name in @('CallState', 'Privilege', 'MediaType', 'CallerIDAddress', 'CalledIDAddress', 'ConnectedIDAddress', 'CallerIDName', 'CalledIDName', 'ConnectedIDName')) {
$value = Get-SafeProperty -Object $Call -Name $name
$valueString = Convert-ToSafeString -Value $value
if ($null -ne $value -and $valueString -ne '') {
$parts += ("{0}={1}" -f $name, $valueString)
}
}
$callAddress = Get-SafeProperty -Object $Call -Name 'Address'
if ($null -ne $callAddress) {
$parts += (Describe-Address -Address $callAddress)
}
return ($parts -join ' ')
}
function Get-PhoneDebugSummary {
param($Call)
if ($null -eq $Call) {
return 'call=<null>'
}
$candidates = @(
'CallerIDAddress',
'CalledIDAddress',
'ConnectedIDAddress',
'CallerIDName',
'CalledIDName',
'ConnectedIDName',
'CalledPartyID',
'CallingPartyID',
'OriginatingAddress',
'DestinationAddress',
'DisplayableAddress'
)
$parts = @()
foreach ($name in $candidates) {
$valueString = Convert-ToSafeString -Value (Get-SafeProperty -Object $Call -Name $name)
if ($valueString -ne '') {
$parts += ("{0}={1}" -f $name, $valueString)
}
}
$callInfo01 = Get-SafeCallInfoString -Call $Call -Id 1
$callInfo03 = Get-SafeCallInfoString -Call $Call -Id 3
$callInfo05 = Get-SafeCallInfoString -Call $Call -Id 5
$callInfo12 = Get-SafeCallInfoString -Call $Call -Id 12
$callInfo13 = Get-SafeCallInfoString -Call $Call -Id 13
$callInfo14 = Get-SafeCallInfoString -Call $Call -Id 14
$callInfo15 = Get-SafeCallInfoString -Call $Call -Id 15
$callInfo16 = Get-SafeCallInfoString -Call $Call -Id 16
if ($callInfo01 -ne '') {
$parts += ("CallInfoString1_CallerIdNumber={0}" -f $callInfo01)
}
if ($callInfo03 -ne '') {
$parts += ("CallInfoString3_CalledIdNumber={0}" -f $callInfo03)
}
if ($callInfo05 -ne '') {
$parts += ("CallInfoString5_ConnectedIdNumber={0}" -f $callInfo05)
}
if ($callInfo12 -ne '') {
$parts += ("CallInfoString12_DisplayableAddress={0}" -f $callInfo12)
}
if ($callInfo13 -ne '') {
$parts += ("CallInfoString13_CallingPartyId={0}" -f $callInfo13)
}
if ($callInfo14 -ne '') {
$parts += ("CallInfoString14={0}" -f $callInfo14)
}
if ($callInfo15 -ne '') {
$parts += ("CallInfoString15={0}" -f $callInfo15)
}
if ($callInfo16 -ne '') {
$parts += ("CallInfoString16={0}" -f $callInfo16)
}
if ($parts.Count -eq 0) {
return 'callProperties=<none-accessible>'
}
return ($parts -join ' ')
}
function Get-CallIdentity {
param($Call)
if ($null -eq $Call) {
return ''
}
try {
$pointer = [System.Runtime.InteropServices.Marshal]::GetIUnknownForObject($Call)
try {
if ($pointer -eq [IntPtr]::Zero) {
return ''
}
return ('0x{0}' -f $pointer.ToInt64().ToString('X'))
}
finally {
if ($pointer -ne [IntPtr]::Zero) {
[void][System.Runtime.InteropServices.Marshal]::Release($pointer)
}
}
}
catch {
return ''
}
}
function Get-CallSnapshot {
param(
[Parameter(Mandatory = $true)] [string]$EventType,
$Payload,
[Parameter(Mandatory = $true)] [string[]]$WatchList
)
$call = Get-SafeProperty -Object $Payload -Name 'Call'
$address = Get-SafeProperty -Object $Payload -Name 'Address'
if ($null -eq $address -and $null -ne $call) {
$address = Get-SafeProperty -Object $call -Name 'Address'
}
$addressName = Convert-ToSafeString -Value (Get-SafeProperty -Object $address -Name 'AddressName')
$dialableAddress = Convert-ToSafeString -Value (Get-SafeProperty -Object $address -Name 'DialableAddress')
$addressCategory = Get-AddressCategory -AddressName $addressName
$watchedExtension = Normalize-ExtensionToken -Value $dialableAddress
if ($watchedExtension -eq '') {
$watchedExtension = Normalize-ExtensionToken -Value $addressName
}
$callState = Convert-ToSafeString -Value (Get-SafeProperty -Object $Payload -Name 'State')
$cause = Convert-ToSafeString -Value (Get-SafeProperty -Object $Payload -Name 'Cause')
$callerIdAddress = Normalize-PhoneLikeValue -Value (Get-FirstNonEmptyProperty -Object $call -Names @('CallerIDAddress', 'CallingPartyID', 'CallerAddress', 'OriginatingAddress'))
$calledIdAddress = Normalize-PhoneLikeValue -Value (Get-FirstNonEmptyProperty -Object $call -Names @('CalledIDAddress', 'CalledPartyID', 'DestinationAddress'))
$connectedIdAddress = Normalize-PhoneLikeValue -Value (Get-FirstNonEmptyProperty -Object $call -Names @('ConnectedIDAddress', 'ConnectedAddress'))
if ($callerIdAddress -eq '') {
$callerIdAddress = Normalize-PhoneLikeValue -Value (Get-PreferredCallInfoString -Call $call -Ids @(1, 13, 12, 14))
}
if ($calledIdAddress -eq '') {
$calledIdAddress = Normalize-PhoneLikeValue -Value (Get-PreferredCallInfoString -Call $call -Ids @(3, 12, 15))
}
if ($connectedIdAddress -eq '') {
$connectedIdAddress = Normalize-PhoneLikeValue -Value (Get-PreferredCallInfoString -Call $call -Ids @(5, 12, 16))
}
$callerName = Get-FirstNonEmptyProperty -Object $call -Names @('CallerIDName', 'CallingPartyName')
if ($callerName -eq '') {
$callerName = Get-SafeCallInfoString -Call $call -Id 0
}
$rawCallerIdAddress = $callerIdAddress
$rawCalledIdAddress = $calledIdAddress
$rawConnectedIdAddress = $connectedIdAddress
$direction = 'inbound'
$externalPhone = Select-ExternalPhoneCandidate -Candidates @($callerIdAddress, $connectedIdAddress, $calledIdAddress) -WatchList $WatchList
$callerToken = Normalize-ExtensionToken -Value $callerIdAddress
$calledToken = Normalize-ExtensionToken -Value $calledIdAddress
$connectedToken = Normalize-ExtensionToken -Value $connectedIdAddress
$callingExtension = ''
$calledExtension = $watchedExtension
if ($callerToken -ne '' -and ($WatchList -contains $callerToken) -and $calledIdAddress -ne '' -and -not ($WatchList -contains $calledToken)) {
$direction = 'outbound'
$externalPhone = Select-ExternalPhoneCandidate -Candidates @($calledIdAddress, $connectedIdAddress, $callerIdAddress) -WatchList $WatchList
$callingExtension = $callerToken
$calledExtension = $watchedExtension
}
elseif ($connectedToken -ne '' -and ($WatchList -contains $connectedToken) -and $callerToken -eq '') {
$calledExtension = $connectedToken
}
elseif ($watchedExtension -eq '' -and $connectedToken -ne '' -and ($WatchList -contains $connectedToken)) {
$calledExtension = $connectedToken
}
$eventId = Get-CallIdentity -Call $call
if ($eventId -eq '') {
$eventId = "{0}:{1}:{2}" -f $addressName, $watchedExtension, $EventType
}
return [pscustomobject]@{
EventType = $EventType
EventId = $eventId
CallState = $callState
Cause = $cause
AddressName = $addressName
AddressCategory = $addressCategory
WatchedExtension = $watchedExtension
CalledExtension = $calledExtension
CallingExtension = $callingExtension
Direction = $direction
ExternalPhone = $externalPhone
RawCallerIdAddress = $rawCallerIdAddress
RawCalledIdAddress = $rawCalledIdAddress
RawConnectedIdAddress = $rawConnectedIdAddress
CallerName = $callerName
CallObject = $call
}
}
function Build-IncomingPayload {
param([Parameter(Mandatory = $true)] $CallRecord)
return [ordered]@{
phone = $CallRecord.ExternalPhone
name = $CallRecord.CallerName
direction = $CallRecord.Direction
outcome = $null
event_id = $CallRecord.EventId
called_extension = $CallRecord.CalledExtension
calling_extension = $CallRecord.CallingExtension
event_type = $CallRecord.LastEventType
note = 'bridge dry-run da watcher TAPI .NET'
called_at = $CallRecord.StartedAt.ToString('o')
is_test = $true
}
}
function Build-CallEndedPayload {
param([Parameter(Mandatory = $true)] $CallRecord)
$durationSeconds = [Math]::Max(0, [int][Math]::Round((New-TimeSpan -Start $CallRecord.StartedAt -End $CallRecord.EndedAt).TotalSeconds))
$outcome = 'terminata'
if ($CallRecord.Answered) {
$outcome = 'risposta'
}
return [ordered]@{
phone = $CallRecord.ExternalPhone
event_id = $CallRecord.EventId
outcome = $outcome
duration_seconds = $durationSeconds
ended_at = $CallRecord.EndedAt.ToString('o')
direction = $CallRecord.Direction
called_extension = $CallRecord.CalledExtension
calling_extension = $CallRecord.CallingExtension
event_type = $CallRecord.LastEventType
note = 'bridge dry-run da watcher TAPI .NET'
is_test = $true
}
}
function Invoke-BridgeRequest {
param(
[Parameter(Mandatory = $true)] [string]$Endpoint,
[Parameter(Mandatory = $true)] [System.Collections.Specialized.OrderedDictionary]$Payload
)
$json = $Payload | ConvertTo-Json -Depth 8 -Compress
if ($script:BridgeMode -eq 'dry-run') {
Write-LogLine ("BRIDGE DRYRUN endpoint={0} payload={1}" -f $Endpoint, $json)
return
}
if ($script:BridgeMode -ne 'live') {
return
}
if ($script:BaseUrl -eq '' -or $script:Token -eq '') {
Write-LogLine ("BRIDGE SKIP endpoint={0} reason=missing-baseurl-or-token payload={1}" -f $Endpoint, $json)
return
}
try {
$headers = @{ 'X-CTI-Token' = $script:Token }
$uri = ('{0}/api/v1/cti/panasonic/{1}' -f $script:BaseUrl.TrimEnd('/'), $Endpoint)
$response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Post -ContentType 'application/json' -Body $json
Write-LogLine ("BRIDGE LIVE OK endpoint={0} response={1}" -f $Endpoint, (($response | ConvertTo-Json -Depth 8 -Compress)))
}
catch {
Write-LogLine ("BRIDGE LIVE KO endpoint={0} error={1} payload={2}" -f $Endpoint, $_.Exception.Message, $json)
}
}
function Register-BridgeIncoming {
param([Parameter(Mandatory = $true)] $Snapshot)
if (-not (Should-EmitBridgeForSnapshot -Snapshot $Snapshot)) {
Write-LogLine ("BRIDGE IGNORE endpoint=incoming address={0} category={1} mode={2}" -f $Snapshot.AddressName, $Snapshot.AddressCategory, $script:BridgeWatchMode)
return
}
$resolvedEventId = Resolve-BridgeEventId -EventId $Snapshot.EventId
if ($resolvedEventId -eq '' -and $Snapshot.EventId -ne '') {
$resolvedEventId = $Snapshot.EventId
}
if ($resolvedEventId -eq '') {
$resolvedEventId = "{0}:{1}:{2}" -f $Snapshot.AddressName, $Snapshot.CalledExtension, $Snapshot.EventType
}
if (Test-BridgeCallRecentlyCompleted -EventId $resolvedEventId) {
Write-LogLine ("BRIDGE IGNORE endpoint=incoming reason=already-completed event_id={0} address={1}" -f $resolvedEventId, $Snapshot.AddressName)
return
}
if (-not $script:BridgeCalls.ContainsKey($resolvedEventId)) {
$matchedCall = Find-BridgeCallMatchBySignature -Snapshot $Snapshot
if ($null -ne $matchedCall) {
$resolvedEventId = $matchedCall.EventId
if ($Snapshot.EventId -ne '') {
$script:BridgeEventAliases[$Snapshot.EventId] = $resolvedEventId
}
}
}
if ($script:BridgeCalls.ContainsKey($resolvedEventId)) {
return
}
$callRecord = [ordered]@{
EventId = $resolvedEventId
StartedAt = Get-Date
EndedAt = $null
LastEventType = $Snapshot.EventType
CalledExtension = $Snapshot.CalledExtension
CallingExtension = $Snapshot.CallingExtension
Direction = $Snapshot.Direction
ExternalPhone = $Snapshot.ExternalPhone
CallerName = $Snapshot.CallerName
Answered = $false
IncomingSent = $false
}
if ($Snapshot.EventId -ne '' -and $Snapshot.EventId -ne $resolvedEventId) {
$script:BridgeEventAliases[$Snapshot.EventId] = $resolvedEventId
}
$script:BridgeCalls[$resolvedEventId] = $callRecord
if ($callRecord.ExternalPhone -eq '') {
Write-LogLine ("BRIDGE SKIP endpoint=incoming reason=phone-missing event_id={0} called_extension={1}" -f $callRecord.EventId, $callRecord.CalledExtension)
Write-LogLine ("BRIDGE DEBUG event_id={0} rawCaller={1} rawCalled={2} rawConnected={3}" -f $callRecord.EventId, $Snapshot.RawCallerIdAddress, $Snapshot.RawCalledIdAddress, $Snapshot.RawConnectedIdAddress)
Write-LogLine ("BRIDGE DEBUG event_id={0} {1}" -f $callRecord.EventId, (Get-PhoneDebugSummary -Call $Snapshot.CallObject))
return
}
Invoke-BridgeRequest -Endpoint 'incoming' -Payload (Build-IncomingPayload -CallRecord $callRecord)
$callRecord.IncomingSent = $true
}
function Register-BridgeStateTransition {
param([Parameter(Mandatory = $true)] $Snapshot)
if (-not (Should-EmitBridgeForSnapshot -Snapshot $Snapshot)) {
Write-LogLine ("BRIDGE IGNORE endpoint=state address={0} category={1} mode={2}" -f $Snapshot.AddressName, $Snapshot.AddressCategory, $script:BridgeWatchMode)
return
}
if ($Snapshot.Direction -eq 'outbound' -and $Snapshot.AddressCategory -eq 'CO' -and $Snapshot.CallingExtension -eq '') {
Write-LogLine ("BRIDGE WARN unresolved-outbound-source address={0} phone={1} rawCaller={2} rawCalled={3} rawConnected={4}" -f $Snapshot.AddressName, $Snapshot.ExternalPhone, $Snapshot.RawCallerIdAddress, $Snapshot.RawCalledIdAddress, $Snapshot.RawConnectedIdAddress)
}
$resolvedEventId = Resolve-BridgeEventId -EventId $Snapshot.EventId
if ($resolvedEventId -eq '' -and $Snapshot.EventId -ne '') {
$resolvedEventId = $Snapshot.EventId
}
if ($resolvedEventId -eq '') {
$resolvedEventId = "{0}:{1}:{2}" -f $Snapshot.AddressName, $Snapshot.CalledExtension, $Snapshot.EventType
}
if (Test-BridgeCallRecentlyCompleted -EventId $resolvedEventId) {
Write-LogLine ("BRIDGE IGNORE endpoint=state reason=already-completed event_id={0} address={1}" -f $resolvedEventId, $Snapshot.AddressName)
return
}
if (-not $script:BridgeCalls.ContainsKey($resolvedEventId)) {
$matchedCall = Find-BridgeCallMatchBySignature -Snapshot $Snapshot
if ($null -ne $matchedCall) {
$resolvedEventId = $matchedCall.EventId
if ($Snapshot.EventId -ne '') {
$script:BridgeEventAliases[$Snapshot.EventId] = $resolvedEventId
}
}
}
$callRecord = $null
if ($script:BridgeCalls.ContainsKey($resolvedEventId)) {
$callRecord = $script:BridgeCalls[$resolvedEventId]
}
else {
$callRecord = [ordered]@{
EventId = $resolvedEventId
StartedAt = Get-Date
EndedAt = $null
LastEventType = $Snapshot.EventType
CalledExtension = $Snapshot.CalledExtension
CallingExtension = $Snapshot.CallingExtension
Direction = $Snapshot.Direction
ExternalPhone = $Snapshot.ExternalPhone
CallerName = $Snapshot.CallerName
Answered = $false
IncomingSent = $false
}
if ($Snapshot.EventId -ne '' -and $Snapshot.EventId -ne $resolvedEventId) {
$script:BridgeEventAliases[$Snapshot.EventId] = $resolvedEventId
}
$script:BridgeCalls[$resolvedEventId] = $callRecord
}
if ($callRecord.ExternalPhone -eq '' -and $Snapshot.ExternalPhone -ne '') {
$callRecord.ExternalPhone = $Snapshot.ExternalPhone
}
if ($callRecord.CallerName -eq '' -and $Snapshot.CallerName -ne '') {
$callRecord.CallerName = $Snapshot.CallerName
}
if ($callRecord.CalledExtension -eq '' -and $Snapshot.CalledExtension -ne '') {
$callRecord.CalledExtension = $Snapshot.CalledExtension
}
if ($callRecord.CallingExtension -eq '' -and $Snapshot.CallingExtension -ne '') {
$callRecord.CallingExtension = $Snapshot.CallingExtension
}
if ($callRecord.Direction -eq '' -and $Snapshot.Direction -ne '') {
$callRecord.Direction = $Snapshot.Direction
}
if ($Snapshot.EventType -ne '') {
$callRecord.LastEventType = $Snapshot.EventType
}
if (-not $callRecord.IncomingSent -and $callRecord.ExternalPhone -ne '') {
Invoke-BridgeRequest -Endpoint 'incoming' -Payload (Build-IncomingPayload -CallRecord $callRecord)
$callRecord.IncomingSent = $true
}
switch ([int]$Snapshot.CallState) {
$CallStateConnected {
$callRecord.Answered = $true
Write-LogLine ("BRIDGE MARK answered event_id={0} called_extension={1}" -f $callRecord.EventId, $callRecord.CalledExtension)
break
}
$CallStateDisconnected {
$callRecord.EndedAt = Get-Date
if ($callRecord.ExternalPhone -eq '') {
Write-LogLine ("BRIDGE SKIP endpoint=call-ended reason=phone-missing event_id={0} called_extension={1}" -f $callRecord.EventId, $callRecord.CalledExtension)
Write-LogLine ("BRIDGE DEBUG event_id={0} rawCaller={1} rawCalled={2} rawConnected={3}" -f $callRecord.EventId, $Snapshot.RawCallerIdAddress, $Snapshot.RawCalledIdAddress, $Snapshot.RawConnectedIdAddress)
Write-LogLine ("BRIDGE DEBUG event_id={0} {1}" -f $callRecord.EventId, (Get-PhoneDebugSummary -Call $Snapshot.CallObject))
}
else {
Invoke-BridgeRequest -Endpoint 'call-ended' -Payload (Build-CallEndedPayload -CallRecord $callRecord)
}
Mark-BridgeCallCompleted -EventId $resolvedEventId
[void]$script:BridgeCalls.Remove($resolvedEventId)
if ($Snapshot.EventId -ne '' -and $script:BridgeEventAliases.ContainsKey($Snapshot.EventId)) {
[void]$script:BridgeEventAliases.Remove($Snapshot.EventId)
}
break
}
}
}
function Describe-EventPayload {
param($Payload)
if ($null -eq $Payload) {
return 'payload=<null>'
}
$parts = @()
$payloadTypeName = Get-SafeTypeName -Object $Payload
$parts += ("payloadType={0}" -f $payloadTypeName)
if ($payloadTypeName -eq '<type-error>') {
try {
if ([System.Runtime.InteropServices.Marshal]::IsComObject($Payload)) {
$parts += 'payloadComObject=true'
}
}
catch {
}
}
$eventValue = Get-SafeProperty -Object $Payload -Name 'Event'
if ($null -ne $eventValue) {
$parts += ("event={0}" -f (Convert-ToSafeString -Value $eventValue))
}
$stateValue = Get-SafeProperty -Object $Payload -Name 'State'
if ($null -ne $stateValue) {
$parts += ("state={0}" -f (Convert-ToSafeString -Value $stateValue))
}
$causeValue = Get-SafeProperty -Object $Payload -Name 'Cause'
if ($null -ne $causeValue) {
$parts += ("cause={0}" -f (Convert-ToSafeString -Value $causeValue))
}
$errorValue = Get-SafeProperty -Object $Payload -Name 'Error'
if ($null -ne $errorValue) {
$parts += ("error={0}" -f (Convert-ToSafeString -Value $errorValue))
}
$address = Get-SafeProperty -Object $Payload -Name 'Address'
if ($null -ne $address) {
$parts += (Describe-Address -Address $address)
}
$call = Get-SafeProperty -Object $Payload -Name 'Call'
if ($null -ne $call) {
$parts += (Describe-Call -Call $call)
}
if ($parts.Count -le 1) {
$parts += 'payloadDetails=non-accessible'
}
return ($parts -join ' ')
}
function Try-SubscribeDotNetEvent {
param(
[Parameter(Mandatory = $true)] $InputObject,
[Parameter(Mandatory = $true)] [string]$SourceIdentifier,
[Parameter(Mandatory = $true)] [string]$Label
)
try {
$events = @($InputObject.GetType().GetEvents() | Select-Object -ExpandProperty Name)
Write-LogLine ("DOTNET CHECK label={0} type={1} events={2}" -f $Label, $InputObject.GetType().FullName, ($events -join ','))
if (-not ($events -contains 'Event')) {
return [pscustomobject]@{
Ok = $false
Error = 'evento .NET Event non esposto'
}
}
Register-ObjectEvent -InputObject $InputObject -EventName Event -SourceIdentifier $SourceIdentifier | Out-Null
return [pscustomobject]@{
Ok = $true
Error = ''
}
}
catch {
return [pscustomobject]@{
Ok = $false
Error = $_.Exception.Message
}
}
}
function Process-ReceivedEvent {
param([Parameter(Mandatory = $true)] $EventRecord)
try {
$sourceIdentifier = Convert-ToSafeString -Value $EventRecord.SourceIdentifier
$sourceArgs = @($EventRecord.SourceArgs)
$argTypes = @()
foreach ($arg in $sourceArgs) {
$argTypes += (Get-SafeTypeName -Object $arg)
}
$eventType = ''
if ($sourceArgs.Count -ge 1 -and $null -ne $sourceArgs[0]) {
$eventType = Convert-ToSafeString -Value $sourceArgs[0]
}
if (Should-LogDotNetEvent -EventType $eventType) {
$payloadDescription = 'payload=<none>'
if ($sourceArgs.Count -ge 2) {
$payloadDescription = Describe-EventPayload -Payload $sourceArgs[1]
}
Write-LogLine ("DOTNET EVENT source={0} eventType={1} argTypes={2} {3}" -f $sourceIdentifier, $eventType, ($argTypes -join ','), $payloadDescription)
}
if ($script:BridgeMode -ne 'none' -and $sourceArgs.Count -ge 2 -and ($eventType -eq 'TE_CALLNOTIFICATION' -or $eventType -eq 'TE_CALLSTATE')) {
$snapshot = Get-CallSnapshot -EventType $eventType -Payload $sourceArgs[1] -WatchList $watchList
if ($eventType -eq 'TE_CALLNOTIFICATION') {
Register-BridgeIncoming -Snapshot $snapshot
}
elseif ($eventType -eq 'TE_CALLSTATE' -and $snapshot.CallState -ne '') {
Register-BridgeStateTransition -Snapshot $snapshot
}
}
}
catch {
Write-LogLine ("DOTNET EVENT ERROR source={0} error={1}" -f (Convert-ToSafeString -Value $EventRecord.SourceIdentifier), $_.Exception.Message)
}
}
$script:LogFilePath = $LogFile
$script:BridgeMode = $BridgeMode
$script:BaseUrl = $BaseUrl
$script:Token = $Token
$script:IncludeDiagnosticEvents = $IncludeDiagnosticEvents.IsPresent
$script:BridgeCalls = @{}
$script:BridgeEventAliases = @{}
$script:CompletedBridgeCalls = @{}
$watchList = @(Parse-WatchList -ExtensionsValue $Extensions)
$script:BridgeWatchMode = Get-BridgeWatchMode -WatchList $watchList
$subscriptions = @()
$wrappers = @()
$assembly = Get-InteropAssembly -OutputDir $InteropOutputDir
$tapiClassType = Get-TypeByName -Assembly $assembly -Name 'TAPIClass'
$typedEventInterfaceType = Get-TypeByName -Assembly $assembly -Name 'ITTAPIEventNotification_Event'
$dispatchEventInterfaceType = Get-TypeByName -Assembly $assembly -Name 'ITTAPIDispatchEventNotification_Event'
$tapi = New-Object -ComObject TAPI.TAPI
$tapi.Initialize()
$eventFilter = $TapiEventFilterCallNotification + $TapiEventFilterCallState + $TapiEventFilterPrivate
if ($script:IncludeDiagnosticEvents) {
$eventFilter += $TapiEventFilterCallMedia + $TapiEventFilterCallHub + $TapiEventFilterCallInfoChange
}
$tapi.EventFilter = $eventFilter
$allAddresses = Convert-ComCollectionToArray -Collection $tapi.Addresses
$addresses = @($allAddresses | Where-Object {
Should-WatchAddress -Address $_ -WatchList $watchList
})
Write-LogLine "=== AVVIO WATCH DOTNET TAPI ==="
Write-LogLine ("Assembly interop: {0}" -f $assembly.FullName)
Write-LogLine "Watch list: $Extensions"
Write-LogLine ("Watch list expanded: {0}" -f ($watchList -join ','))
Write-LogLine "Provider addresses totali: $($allAddresses.Count)"
Write-LogLine "Provider addresses filtrati: $($addresses.Count)"
Write-LogLine "Bridge mode: $BridgeMode"
Write-LogLine "Bridge watch mode: $($script:BridgeWatchMode)"
Write-LogLine ("Diagnostic events: {0}" -f ($(if ($script:IncludeDiagnosticEvents) { 'enabled' } else { 'disabled' })))
foreach ($address in $addresses) {
Write-LogLine ("ADDRESS READY {0}" -f (Describe-Address -Address $address))
$registration = Try-RegisterCallNotifications -Tapi $tapi -Address $address
if ($registration.Ok) {
Write-LogLine ("REGISTER OK mode=monitor+owner cookie={0} {1}" -f $registration.Cookie, (Describe-Address -Address $address))
}
else {
Write-LogLine ("REGISTER KO {0} error={1}" -f (Describe-Address -Address $address), $registration.Error)
}
}
if ($null -ne $tapiClassType) {
try {
$typedTapi = [System.Runtime.InteropServices.Marshal]::CreateWrapperOfType($tapi, $tapiClassType)
$wrappers += $typedTapi
$subscriptions += [pscustomobject]@{ Label = 'tapi-class'; Result = (Try-SubscribeDotNetEvent -InputObject $typedTapi -SourceIdentifier 'NetGescon.Tapi.DotNet.TapiClass' -Label 'tapi-class') }
}
catch {
Write-LogLine ("DOTNET WRAP KO label=tapi-class error={0}" -f $_.Exception.Message)
}
}
if ($null -ne $typedEventInterfaceType) {
try {
$typedEventSource = [System.Runtime.InteropServices.Marshal]::CreateWrapperOfType($tapi, $typedEventInterfaceType)
$wrappers += $typedEventSource
$subscriptions += [pscustomobject]@{ Label = 'typed-event-interface'; Result = (Try-SubscribeDotNetEvent -InputObject $typedEventSource -SourceIdentifier 'NetGescon.Tapi.DotNet.TypedEvent' -Label 'typed-event-interface') }
}
catch {
Write-LogLine ("DOTNET WRAP KO label=typed-event-interface error={0}" -f $_.Exception.Message)
}
}
if ($null -ne $dispatchEventInterfaceType) {
try {
$dispatchEventSource = [System.Runtime.InteropServices.Marshal]::CreateWrapperOfType($tapi, $dispatchEventInterfaceType)
$wrappers += $dispatchEventSource
$subscriptions += [pscustomobject]@{ Label = 'dispatch-event-interface'; Result = (Try-SubscribeDotNetEvent -InputObject $dispatchEventSource -SourceIdentifier 'NetGescon.Tapi.DotNet.DispatchEvent' -Label 'dispatch-event-interface') }
}
catch {
Write-LogLine ("DOTNET WRAP KO label=dispatch-event-interface error={0}" -f $_.Exception.Message)
}
}
foreach ($subscription in $subscriptions) {
if ($subscription.Result.Ok) {
Write-LogLine ("DOTNET SUBSCRIBE OK label={0}" -f $subscription.Label)
}
else {
Write-LogLine ("DOTNET SUBSCRIBE KO label={0} error={1}" -f $subscription.Label, $subscription.Result.Error)
}
}
Write-LogLine 'Fai ora chiamate reali su 201, 205 o 206.'
$end = $null
if ($Seconds -gt 0) {
$end = (Get-Date).AddSeconds($Seconds)
}
try {
while ($true) {
if ($null -ne $end -and (Get-Date) -ge $end) {
break
}
$eventRecord = Wait-Event -Timeout 1
if ($null -eq $eventRecord) {
continue
}
$sourceIdentifier = Convert-ToSafeString -Value $eventRecord.SourceIdentifier
if ($sourceIdentifier -notlike 'NetGescon.Tapi.DotNet.*') {
Remove-Event -EventIdentifier $eventRecord.EventIdentifier -ErrorAction SilentlyContinue
continue
}
Process-ReceivedEvent -EventRecord $eventRecord
Remove-Event -EventIdentifier $eventRecord.EventIdentifier -ErrorAction SilentlyContinue
}
}
finally {
foreach ($sourceIdentifier in @('NetGescon.Tapi.DotNet.TapiClass', 'NetGescon.Tapi.DotNet.TypedEvent', 'NetGescon.Tapi.DotNet.DispatchEvent')) {
Unregister-Event -SourceIdentifier $sourceIdentifier -ErrorAction SilentlyContinue
Get-Event -SourceIdentifier $sourceIdentifier -ErrorAction SilentlyContinue | Remove-Event -ErrorAction SilentlyContinue
}
try {
$tapi.Shutdown()
}
catch {
}
}