66 lines
3.0 KiB
Go
66 lines
3.0 KiB
Go
package notify
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/base64"
|
||
|
|
"fmt"
|
||
|
|
"os/exec"
|
||
|
|
)
|
||
|
|
|
||
|
|
// toastAppID is the AppUserModelID the toasts are shown under. Unpackaged
|
||
|
|
// programs need a registered one; PowerShell's own is present on every
|
||
|
|
// Windows and lets the toast show reliably (it then appears as coming from
|
||
|
|
// Windows PowerShell). A talk-specific identity would need a Start Menu
|
||
|
|
// shortcut; left for later.
|
||
|
|
const toastAppID = `{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe`
|
||
|
|
|
||
|
|
// toastGroup groups all talk toasts so they can be removed by tag.
|
||
|
|
const toastGroup = "talk"
|
||
|
|
|
||
|
|
// announceWindows shows a toast with the text and a Cancel button that opens
|
||
|
|
// a.CancelURI (protocol activation; Windows then starts `talk uri <link>`,
|
||
|
|
// which tells the daemon to cancel). The returned close removes the toast by
|
||
|
|
// its tag when the announcement ends.
|
||
|
|
func announceWindows(ctx context.Context, a Announcement) (func(), error) {
|
||
|
|
tag := "talk-" + a.Tag
|
||
|
|
script := fmt.Sprintf(`$ErrorActionPreference = 'Stop'
|
||
|
|
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||
|
|
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
||
|
|
$dec = { param($b) [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b)) }
|
||
|
|
$esc = { param($s) [System.Security.SecurityElement]::Escape($s) }
|
||
|
|
$body = & $esc (& $dec '%s')
|
||
|
|
$label = & $esc (& $dec '%s')
|
||
|
|
$uri = & $esc (& $dec '%s')
|
||
|
|
$xmlText = "<toast><visual><binding template='ToastGeneric'><text>talk</text><text>$body</text></binding></visual><actions><action content='$label' arguments='$uri' activationType='protocol'/></actions></toast>"
|
||
|
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||
|
|
$xml.LoadXml($xmlText)
|
||
|
|
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||
|
|
$toast.Tag = '%s'
|
||
|
|
$toast.Group = '%s'
|
||
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('%s').Show($toast)
|
||
|
|
`, b64(a.Text), b64(a.CancelLabel), b64(a.CancelURI), tag, toastGroup, toastAppID)
|
||
|
|
if err := runPowerShell(ctx, script); err != nil {
|
||
|
|
return func() {}, err
|
||
|
|
}
|
||
|
|
return func() {
|
||
|
|
remove := fmt.Sprintf(`[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||
|
|
[Windows.UI.Notifications.ToastNotificationManager]::History.Remove('%s', '%s', '%s')
|
||
|
|
`, tag, toastGroup, toastAppID)
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||
|
|
defer cancel()
|
||
|
|
_ = runPowerShell(ctx, remove)
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// runPowerShell runs a script via -EncodedCommand.
|
||
|
|
func runPowerShell(ctx context.Context, script string) error {
|
||
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||
|
|
defer cancel()
|
||
|
|
cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", psEncode(script))
|
||
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||
|
|
return fmt.Errorf("notify: powershell failed: %w: %s", err, string(out))
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func b64(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }
|