Skip to content

Instantly share code, notes, and snippets.

@scooterpsu
Last active December 30, 2022 04:36
Show Gist options
  • Save scooterpsu/d30f55a6efa69b30fc7415644fbe7658 to your computer and use it in GitHub Desktop.
Save scooterpsu/d30f55a6efa69b30fc7415644fbe7658 to your computer and use it in GitHub Desktop.
Gotify client for Windows Notifications
$domain = "p.domain.com"
$token = "AAAAAAAA"
$useHeaders = $false
. .\config.ps1
$script:sentNotifications = @()
Function Log($text){
Out-File -Append -encoding UTF8 -FilePath .\log.txt -InputObject $((get-date).ToString('T')+": "+$text.Trim().Replace("[char]39","'"))
}
Function Output($text){
$jsonData = convertfrom-json $text
$msgid = $jsonData.id
if($script:sentNotifications.Contains($msgid)){
Log("Notification already sent, skipping")
Return
}
$script:sentNotifications += $msgid
$jsonData.message = $jsonData.message.Replace("'","[char]39")
$jsonData.title = $jsonData.title.Replace("'","[char]39")
if($jsonData.message.Contains("base64")){
Log("Converting base64 encoded image")
$messageText = ($jsonData.message -split [regex]::Escape("![]("))[0].Trim()
$imageString = ($jsonData.message -split [regex]::Escape("![]("))[1].Split(")")[0]
$encodedImage = $imageString.Split(',')[1]
$decoded = [System.Convert]::FromBase64CharArray($encodedImage, 0, $encodedImage.Length)
$cacheImage = ".\cache\"+$msgid+".jpg"
Set-Content -Path $cacheImage -Value $decoded -AsByteStream
$jsonData.message = $messageText + " ![](" + $cacheImage + ")"
}
$text = ConvertTo-Json $jsonData -Compress -Depth 3
Log($text)
$cmd = "'$text' | .\toast.ps1"
$encodedcmd = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd))
$process = start-process pwsh -ArgumentList "-noexit -encodedcommand $encodedcmd" -PassThru -NoNewWindow
}
Function Init-Websocket(){
$recv_queue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[String]'
$ws = New-Object Net.WebSockets.ClientWebSocket
$cts = New-Object Threading.CancellationTokenSource
$ct = New-Object Threading.CancellationToken($false)
Log("Connecting...")
$connectTask = $ws.ConnectAsync("wss://$domain/stream?token=$token", $cts.Token)
do { Sleep(1) }
until ($connectTask.IsCompleted)
Log("Connected!")
$recv_job = {
param($ws, $recv_queue)
$buffer = [Net.WebSockets.WebSocket]::CreateClientBuffer(1024,1024)
$ct = [Threading.CancellationToken]::new($false)
$taskResult = $null
while ($ws.State -eq [Net.WebSockets.WebSocketState]::Open) {
$jsonResult = ""
do {
$taskResult = $ws.ReceiveAsync($buffer, $ct)
while (-not $taskResult.IsCompleted -and $ws.State -eq [Net.WebSockets.WebSocketState]::Open) {
[Threading.Thread]::Sleep(10)
}
$jsonResult += [Text.Encoding]::UTF8.GetString($buffer, 0, $taskResult.Result.Count)
} until (
$ws.State -ne [Net.WebSockets.WebSocketState]::Open -or $taskResult.Result.EndOfMessage
)
if (-not [string]::IsNullOrEmpty($jsonResult)) {
$recv_queue.Enqueue($jsonResult)
}
}
}
Log("Starting recv runspace")
$recv_runspace = [PowerShell]::Create()
$recv_runspace.AddScript($recv_job).
AddParameter("ws", $ws).
AddParameter("recv_queue", $recv_queue).BeginInvoke() | Out-Null
try {
do {
Start-Sleep -Milli 500
$msg = $null
while ($recv_queue.TryDequeue([ref] $msg)) {
if ($msg.StartsWith('{')){
Output($msg)
}else{
Log("Response: "+$msg)
}
}
} until ($ws.State -ne [Net.WebSockets.WebSocketState]::Open)
}
finally {
Log("Closing WS connection")
$closetask = $ws.CloseAsync(
[System.Net.WebSockets.WebSocketCloseStatus]::Empty,
"",
$ct
)
do { Sleep(1) }
until ($closetask.IsCompleted)
$ws.Dispose()
Log("Stopping runspace")
$recv_runspace.Stop()
$recv_runspace.Dispose()
Log("Reconnecting")
Poll-Server
Init-Websocket
}
}
Function Poll-Server(){
Log("Polling for missed notifications")
Invoke-WebRequest "https://$domain/message?token=$token" -SessionVariable session -ContentType "application/json; charset=utf-8" | %{ convertfrom-json $_.Content } | where-object {$_.messages.length -gt 0} | select -expand messages | %{ Output(ConvertTo-JSON -Compress -Depth 3 $_) }
}
if(Test-Path -Path ".\log.txt" -PathType Leaf){
Move-Item -Path ".\log.txt" -Destination ".\log.txt.old" -Force
}
if(!(Test-Path -Path ".\cache\" -PathType Leaf)){
New-Item -Path ".\cache\" -ItemType Directory -Force | Out-Null
}
Poll-Server
Init-Websocket
. .\config.ps1
$jsonData = convertfrom-json $input
$appid = $jsonData.appid
$msgid = $jsonData.id
$msgdate = $jsonData.date
$markdown = $false
$BigImage = $null
$clickurl = $null
if(Get-Member -inputobject $jsonData -name "extras"){
if(Get-Member -inputobject $jsonData.extras -name "client::notification"){
if(Get-Member -inputobject $jsonData.extras."client::notification" -name "bigImageUrl"){
$bigImageUrl = $jsonData.extras."client::notification".bigImageUrl
$BigImage = New-BTImage -Source $bigImageUrl
}
if(Get-Member -inputobject $jsonData.extras."client::notification" -name "click"){
if(Get-Member -inputobject $jsonData.extras."client::notification".click -name "url"){
$clickurl = $jsonData.extras."client::notification".click.url
}
}
}
if(Get-Member -inputobject $jsonData.extras -name "client::display"){
if(Get-Member -inputobject $jsonData.extras."client::display" -name "contentType"){
if($jsonData.extras."client::display".contentType -eq "text/markdown"){
$markdown = $true
}
}
}
}
Invoke-WebRequest https://$domain/application?token=$token |
%{ (convertfrom-json $_.content) } | where-object {$_.id -eq $appid} |
%{
$appname = $_.name
$icon = $_.image
}
if($useHeaders){
$Header = New-BTHeader -Id $appid -Title $appname
}
$AppLogo = New-BTImage -Source https://$domain/$icon -AppLogoOverride
$jsonData.title = $jsonData.title.Replace("[char]39","'")
$jsonData.message = $jsonData.message.Replace("[char]39","'")
$TextHeading = New-BTText -Text $jsonData.title
if (($markdown) -and ($jsonData.message.Contains("![]("))){
$messageText = ($jsonData.message -split [regex]::Escape("![]("))[0].Trim()
$TextBody = New-BTText -Text $messageText
$imageString = ($jsonData.message -split [regex]::Escape("![]("))[1].Split(")")[0]
$BigImage = New-BTImage -Source $imageString
}else{
$TextBody = New-BTText -Text $jsonData.message
}
if ($BigImage) {
$Binding = New-BTBinding -Children $TextHeading, $TextBody, $BigImage -AppLogoOverride $AppLogo
}else{
$Binding = New-BTBinding -Children $TextHeading, $TextBody -AppLogoOverride $AppLogo
}
$Visual = New-BTVisual -BindingGeneric $Binding
if($clickurl){
$UrlButton = New-BTButton -Content 'Details' -Arguments $clickurl
$Actions = New-BTAction -Buttons $UrlButton
$Content = New-BTContent -Visual $Visual -Header $Header -CustomTimestamp $msgdate -Actions $Actions
}else{
$Content = New-BTContent -Visual $Visual -Header $Header -CustomTimestamp $msgdate
}
$Dismissed = {
. .\config.ps1
if ($Event.SourceArgs[1].Reason -eq 'UserCanceled') {
$msgid = $Event.SourceArgs.Tag
if(Test-Path -Path ".\cache\$msgid.jpg" -PathType Leaf){
Remove-Item -Path ".\cache\$msgid.jpg"
}
Invoke-RestMethod "https://$domain/message/$msgid" -SessionVariable session -Method Delete -Headers @{"X-Gotify-Key" = $token}
Stop-Process -Id $PID
}
}
$Activated = {
. .\config.ps1
$msgid = $Event.SourceArgs.Tag
if(Test-Path -Path ".\cache\$msgid.jpg" -PathType Leaf){
Remove-Item -Path ".\cache\$msgid.jpg"
}
Invoke-RestMethod "https://$domain/message/$msgid" -SessionVariable session -Method Delete -Headers @{"X-Gotify-Key" = $token}
Stop-Process -Id $PID
}
Submit-BTNotification -Content $Content -UniqueIdentifier $msgid -AppId "scooterpsu!Gotify" -DismissedAction $Dismissed -ActivatedAction $Activated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment