[CmdletBinding()] param( [string]$Endpoint = 'https://assistant.cin.su/delivery/v1/receipts?limit=100', [string]$KeyRecordPath = "$env:LOCALAPPDATA\Domovoy\notification-receiver-read-key.dpapi.json", [string]$StatePath = "$env:LOCALAPPDATA\Domovoy\notification-receiver-state.json", [ValidateRange(1, 5)] [int]$MaximumDisplays = 3, [switch]$NoDisplay ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest function Write-AtomicJson { param([Parameter(Mandatory)]$Value, [Parameter(Mandatory)][string]$Path) $directory = Split-Path -Parent $Path New-Item -ItemType Directory -Force -Path $directory | Out-Null $temporary = Join-Path $directory ('.{0}.{1}.tmp' -f (Split-Path -Leaf $Path), [guid]::NewGuid().ToString('N')) try { [IO.File]::WriteAllText($temporary, (($Value | ConvertTo-Json -Depth 8) + "`n"), [Text.UTF8Encoding]::new($false)) Move-Item -LiteralPath $temporary -Destination $Path -Force } finally { if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force } } } $uri = [Uri]$Endpoint if ($uri.Scheme -ne 'https' -or $uri.Host -ne 'assistant.cin.su' -or $uri.AbsolutePath -ne '/delivery/v1/receipts') { throw 'Notification receiver endpoint is outside the fixed HTTPS binding' } if (-not (Test-Path -LiteralPath $KeyRecordPath -PathType Leaf)) { throw "Notification receiver key not found: $KeyRecordPath" } $keyRecord = Get-Content -LiteralPath $KeyRecordPath -Raw | ConvertFrom-Json if ($keyRecord.version -ne 1 -or $keyRecord.project -ne 'domovoy' -or $keyRecord.purpose -ne 'notification receiver read') { throw 'Unsupported notification key record' } $secure = ConvertTo-SecureString ([string]$keyRecord.protected_value) $pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { $token = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) } if ([string]::IsNullOrWhiteSpace($token)) { throw 'Notification receiver key is empty' } try { $response = Invoke-RestMethod -Uri $uri.AbsoluteUri -Headers @{ Authorization = "Bearer $token" } -TimeoutSec 15 } finally { $token = $null; $secure.Dispose() } $seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) if (Test-Path -LiteralPath $StatePath -PathType Leaf) { $previous = Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json if ($previous.version -eq 1) { foreach ($id in @($previous.displayed_receipt_ids)) { [void]$seen.Add([string]$id) } } } $candidates = @($response.items | Where-Object { $_.receipt_id -match '^receipt-[A-Za-z0-9_-]{1,128}$' -and $_.action_intent_id -match '^[A-Za-z0-9_-]{1,128}$' -and $_.request_digest -match '^[0-9a-f]{64}$' -and $_.accepted_at_unix_millis -gt 0 -and $_.payload.template_code -eq 'prospective-delivered-v1' -and -not [string]::IsNullOrWhiteSpace([string]$_.payload.title) -and -not [string]::IsNullOrWhiteSpace([string]$_.payload.message) -and -not $seen.Contains([string]$_.receipt_id) } | Sort-Object accepted_at_unix_millis | Select-Object -First $MaximumDisplays) if (-not $NoDisplay -and $candidates.Count -gt 0) { Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $notification = [System.Windows.Forms.NotifyIcon]::new() try { $notification.Icon = [System.Drawing.SystemIcons]::Information $notification.Visible = $true $notification.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Info foreach ($receipt in $candidates) { $notification.BalloonTipTitle = ([string]$receipt.payload.title).Substring(0, [Math]::Min(63, ([string]$receipt.payload.title).Length)) $notification.BalloonTipText = ([string]$receipt.payload.message).Substring(0, [Math]::Min(255, ([string]$receipt.payload.message).Length)) $notification.ShowBalloonTip(6000) Start-Sleep -Seconds 7 [void]$seen.Add([string]$receipt.receipt_id) } } finally { $notification.Dispose() } } if (-not $NoDisplay) { $bounded = @($seen | Select-Object -Last 1000) Write-AtomicJson -Path $StatePath -Value ([ordered]@{ version = 1; displayed_receipt_ids = $bounded; last_poll_at_utc = [DateTime]::UtcNow.ToString('o') }) } Write-Output "notification_receiver=poll_ok new=$($candidates.Count) displayed=$(if ($NoDisplay) { 0 } else { $candidates.Count })"