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 '' } try { $type = $Object.GetType() if ($null -eq $type) { return '' } return [string]$type.FullName } catch { return '' } } function Convert-ToSafeString { param($Value) if ($null -eq $Value) { return '' } try { return [string]$Value } catch { return '' } } 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=' } $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=' } 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=' } $parts = @() $payloadTypeName = Get-SafeTypeName -Object $Payload $parts += ("payloadType={0}" -f $payloadTypeName) if ($payloadTypeName -eq '') { 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=' 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 { } }