Files
Assistant/deploy/windows/Invoke-DomovoyNotificationReceiver.ps1
m 069c3837f3
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 16s
DEV-000067 correct notification receiver endpoint
2026-08-17 18:15:32 +03:00

84 lines
4.4 KiB
PowerShell

[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 })"