84 lines
4.4 KiB
PowerShell
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 })"
|